├── .gitignore ├── LICENSE ├── README.md ├── assets ├── Contents.json ├── back.png ├── example@3x.png ├── fileicon ├── folder.png ├── ic_launcher.png ├── ios.png └── shape.png ├── builder ├── flutter.js ├── ios.js └── screenshot.js ├── index.js ├── index_old.js ├── package-lock.json ├── package.json └── tools ├── file.js └── image.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | # fmaker folder icon 3 | Icon? 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mjl0602 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 | > 在《浅谈Flutter的优缺点》文章中,我指出了Flutter存在切图困难,资源管理困难的缺陷,所以我使用node.js编写了一个小工具,可以帮您快速生成低倍率图片,并为iOS与安卓生成各自平台的图标。 2 | 3 | ## 提前全局安装 4 | - flutter 5 | - node.js环境 下载:https://nodejs.org/zh-cn/ 下好安装即可,很简单 6 | - npm包管理工具(Node自带) 7 | 8 | # fmaker功能 9 | 10 | 11 | 12 | fmaker是一个flutter辅助图片处理工具,也可以用来给iOS或Android项目生成图标 13 | 14 | 指令帮助: 15 | ```bash 16 | fmaker -h 17 | Usage: fmaker [options] [command] 18 | 19 | Options: 20 | -V, --version output the version number 21 | -h, --help display help for command 22 | 23 | Commands: 24 | init 在一个Flutter项目中初始化tmaker,为你创建文件夹,添加示例文件和添加.gitignore参数 25 | build [parts] 创建资源,可指定创建指定部分,例: fmaker build ios,android,assets 26 | preview 仅创建资源的预览注释,也就是r.preview.dart文件 27 | folder 把app的图标渲染在本项目的文件夹上(仅mac) 28 | help [command] display help for command 29 | ``` 30 | 31 | ### 按倍率生成图片 32 | `fmaker`可以自动识别项目下`/assets/fmaker`中的多倍图,将多倍图按flutter格式递归转换为2.0x,3.0x,4.0x等文件夹,再将压缩后的低倍图保存到assets中,保证flutter可以自动识别低倍率的图片。例如,在文件夹下放置`example@3x.png`,会生成三倍图,两倍图和一倍图。 33 | 34 | > 为什么要这样做? 35 | 36 | 因为高分辨率的图片被缩小时,会产生不必要的锐化效果,偶尔会产生卡顿;小图被放大时,会变得很模糊,flutter提供一个功能,自动显示正确分辨率的图片。 37 | 但是使用这个功能困难重重,如果你的设计使用sketch切图,只能切出`image.png`,`image@2x.png`,`image@3x.png`这种图,但是flutter需要的图片目录格式是`image.png`,`2.0x/image.png`,`3.0x/image.png`,这种格式使用sketch是很难一次导出的(需要每一次都更改导出名称),很不好用。 38 | 39 | ### 生成App图标 40 | 41 | 如果`/assets/fmaker`文件夹下有名为`ios_icon.png`和`android_icon.png`的文件,那么`fmaker`会自动识别这两个文件,直接将图标生成到项目中,不需要额外的复制粘贴。 42 | 43 | > 注意:iOS的图标不可含有alpha通道,Android的图标可以包含。共同的一点是,图标必须是正方形,`fmaker`会帮你检查icon尺寸,并在log中输出错误。 44 | ### 生成文件夹图标 45 | 46 | 在项目目录下运行: 47 | 48 | ``` 49 | fmaker folder 50 | ``` 51 | 52 | 脚本会自动把Icon?加入.gitignore。 53 | 如下加入即可: 54 | ``` 55 | Icon? 56 | ``` 57 | ### 生成yaml引用与r.dart 58 | 59 | 为了方便`flutter`使用,现在会自动生成yaml的资源引用,你需要先添加: 60 | 61 | ```yaml 62 | flutter: 63 | uses-material-design: true 64 | assets: 65 | # 添加下面这一句 66 | # fmaker 67 | ``` 68 | 那么在运行`fmaker build`后,就会自动生成: 69 | ```yaml 70 | flutter: 71 | uses-material-design: true 72 | assets: 73 | # fmaker 74 | - assets/example.png 75 | # fmaker-end 76 | ``` 77 | 对应的,也会在lib目录下生成r.dart文件,变量名会自动转为驼峰形式 78 | ```dart 79 | class R { 80 | static final String aqweqAsqQweqDasQwr = 'assets/aqweq-asq_qweq-das_qwr.png'; 81 | static final String assfaAbAResize = 'assets/assfa(ab)a-resize.png'; 82 | static final String example = 'assets/example.png'; 83 | } 84 | ``` 85 | 86 | # 安装 87 | 88 | ```bash 89 | git clone https://github.com/mjl0602/flutter-assets-maker.git 90 | cd flutter-assets-maker 91 | npm install -g 92 | fmaker 93 | ``` 94 | 如果看到,“没有对应指令,fmaker已安装”的log,就已经安装成功。 95 | 96 | # 使用 97 | 先假定你的项目名叫yourFlutterProject。 98 | 99 | 需要准备icon文件,`ios_icon.png`和`ios_android.png`,放在yourFlutterProject/assets/fmaker下,其他的多倍图也可以放进去,例如example@3x.png。 100 | 101 | Tips:如果找不到合规的文件又想试一试,使用fmaker init来使用我的测试图片。 102 | 103 | ```bash 104 | cd yourFlutterProject 105 | fmaker init #如果暂时找不到图,就用我的图测试 106 | fmaker build 107 | ``` 108 | 然后安卓与iOS的App图标都已经被替换,你可以启动项目来查看。 109 | 110 | # 注意 111 | 112 | - 工具理论上只支持png。 113 | - 工具会产生两个一样的图,一个是最高倍图,一个是源图,一定程度上增加了项目大小。 114 | - 建议不要引用fmaker文件夹中的源图,因为他不能被自动切换倍率。 115 | - fmaker的重复图片不会增加产物大小,只要你不引入源图。 116 | 117 | # 示例 118 | 119 | //TODO 120 | 有空就整个例子 121 | 122 | > 如果有bug,欢迎提issue,pr更好哦。 123 | > 仓库地址:https://github.com/mjl0602/flutter-assets-maker 124 | 125 | #未经作者授权,本文禁止转载 126 | 127 | 128 | -------------------------------------------------------------------------------- /assets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /assets/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjl0602/flutter-assets-maker/fd1ab536c09496f8bf2599033f90b71cb2e24f18/assets/back.png -------------------------------------------------------------------------------- /assets/example@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjl0602/flutter-assets-maker/fd1ab536c09496f8bf2599033f90b71cb2e24f18/assets/example@3x.png -------------------------------------------------------------------------------- /assets/fileicon: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ### 4 | # Home page: https://github.com/mklement0/fileicon 5 | # Author: Michael Klement (http://same2u.net) 6 | # Invoke with: 7 | # --version for version information 8 | # --help for usage information 9 | ### 10 | 11 | # --- STANDARD SCRIPT-GLOBAL CONSTANTS 12 | 13 | kTHIS_NAME=${BASH_SOURCE##*/} 14 | kTHIS_HOMEPAGE='https://github.com/mklement0/fileicon' 15 | kTHIS_VERSION='v0.2.4' # NOTE: This assignment is automatically updated by `make version VER=` - DO keep the 'v' prefix. 16 | 17 | unset CDPATH # To prevent unpredictable `cd` behavior. 18 | 19 | # --- Begin: STANDARD HELPER FUNCTIONS 20 | 21 | die() { echo "$kTHIS_NAME: ERROR: ${1:-"ABORTING due to unexpected error."}" 1>&2; exit ${2:-1}; } 22 | dieSyntax() { echo "$kTHIS_NAME: ARGUMENT ERROR: ${1:-"Invalid argument(s) specified."} Use -h for help." 1>&2; exit 2; } 23 | 24 | # SYNOPSIS 25 | # openUrl 26 | # DESCRIPTION 27 | # Opens the specified URL in the system's default browser. 28 | openUrl() { 29 | local url=$1 platform=$(uname) cmd=() 30 | case $platform in 31 | 'Darwin') # OSX 32 | cmd=( open "$url" ) 33 | ;; 34 | 'CYGWIN_'*) # Cygwin on Windows; must call cmd.exe with its `start` builtin 35 | cmd=( cmd.exe /c start '' "$url " ) # !! Note the required trailing space. 36 | ;; 37 | 'MINGW32_'*) # MSYS or Git Bash on Windows; they come with a Unix `start` binary 38 | cmd=( start '' "$url" ) 39 | ;; 40 | *) # Otherwise, assume a Freedesktop-compliant OS, which includes many Linux distros, PC-BSD, OpenSolaris, ... 41 | cmd=( xdg-open "$url" ) 42 | ;; 43 | esac 44 | "${cmd[@]}" || { echo "Cannot locate or failed to open default browser; please go to '$url' manually." >&2; return 1; } 45 | } 46 | 47 | # Prints the embedded Markdown-formatted man-page source to stdout. 48 | printManPageSource() { 49 | /usr/bin/sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/ { s///; t\np;}' "$BASH_SOURCE" 50 | } 51 | 52 | # Opens the man page, if installed; otherwise, tries to display the embedded Markdown-formatted man-page source; if all else fails: tries to display the man page online. 53 | openManPage() { 54 | local pager embeddedText 55 | if ! man 1 "$kTHIS_NAME" 2>/dev/null; then 56 | # 2nd attempt: if present, display the embedded Markdown-formatted man-page source 57 | embeddedText=$(printManPageSource) 58 | if [[ -n $embeddedText ]]; then 59 | pager='more' 60 | command -v less &>/dev/null && pager='less' # see if the non-standard `less` is available, because it's preferable to the POSIX utility `more` 61 | printf '%s\n' "$embeddedText" | "$pager" 62 | else # 3rd attempt: open the the man page on the utility's website 63 | openUrl "${kTHIS_HOMEPAGE}/doc/${kTHIS_NAME}.md" 64 | fi 65 | fi 66 | } 67 | 68 | # Prints the contents of the synopsis chapter of the embedded Markdown-formatted man-page source for quick reference. 69 | printUsage() { 70 | local embeddedText 71 | # Extract usage information from the SYNOPSIS chapter of the embedded Markdown-formatted man-page source. 72 | embeddedText=$(/usr/bin/sed -n -e $'/^: <<\'EOF_MAN_PAGE\'/,/^EOF_MAN_PAGE/!d; /^## SYNOPSIS$/,/^#/{ s///; t\np; }' "$BASH_SOURCE") 73 | if [[ -n $embeddedText ]]; then 74 | # Print extracted synopsis chapter - remove backticks for uncluttered display. 75 | printf '%s\n\n' "$embeddedText" | tr -d '`' 76 | else # No SYNOPIS chapter found; fall back to displaying the man page. 77 | echo "WARNING: usage information not found; opening man page instead." >&2 78 | openManPage 79 | fi 80 | } 81 | 82 | # --- End: STANDARD HELPER FUNCTIONS 83 | 84 | # --- PROCESS STANDARD, OUTPUT-INFO-THEN-EXIT OPTIONS. 85 | case $1 in 86 | --version) 87 | # Output version number and exit, if requested. 88 | ver="v0.2.4"; echo "$kTHIS_NAME $kTHIS_VERSION"$'\nFor license information and more, visit '"$kTHIS_HOMEPAGE"; exit 0 89 | ;; 90 | -h|--help) 91 | # Print usage information and exit. 92 | printUsage; exit 93 | ;; 94 | --man) 95 | # Display the manual page and exit. 96 | openManPage; exit 97 | ;; 98 | --man-source) # private option, used by `make update-doc` 99 | # Print raw, embedded Markdown-formatted man-page source and exit 100 | printManPageSource; exit 101 | ;; 102 | --home) 103 | # Open the home page and exit. 104 | openUrl "$kTHIS_HOMEPAGE"; exit 105 | ;; 106 | esac 107 | 108 | # --- Begin: SPECIFIC HELPER FUNCTIONS 109 | 110 | # NOTE: The functions below operate on byte strings such as the one above: 111 | # A single single string of pairs of hex digits, without separators or line breaks. 112 | # Thus, a given byte position is easily calculated: to get byte $byteIndex, use 113 | # ${byteString:byteIndex*2:2} 114 | 115 | # Outputs the specified EXTENDED ATTRIBUTE VALUE as a byte string (a hex dump that is a single-line string of pairs of hex digits, without separators or line breaks, such as "000A2C". 116 | # IMPORTANT: Hex. digits > 9 use UPPPERCASE characters. 117 | # getAttribByteString 118 | getAttribByteString() { 119 | xattr -px "$2" "$1" | tr -d ' \n' 120 | return ${PIPESTATUS[0]} 121 | } 122 | 123 | # Outputs the specified file's RESOURCE FORK as a byte string (a hex dump that is a single-line string of pairs of hex digits, without separators or line breaks, such as "000a2c". 124 | # IMPORTANT: Hex. digits > 9 use *lowercase* characters. 125 | # Note: This function relies on `xxd -p /..namedfork/rsrc | tr -d '\n'` rather than the conceptually equivalent `getAttributeByteString com.apple.ResourceFork` 126 | # for PERFORMANCE reasons: getAttributeByteString() relies on `xattr`, which is a *Python* script and therefore quite slow due to Python's startup cost. 127 | # getAttribByteString 128 | getResourceByteString() { 129 | xxd -p "$1"/..namedfork/rsrc | tr -d '\n' 130 | } 131 | 132 | # Patches a single byte in the byte string provided via stdin. 133 | # patchByteInByteString ndx byteSpec 134 | # ndx is the 0-based byte index 135 | # - If has NO prefix: becomes the new byte 136 | # - If has prefix '|': "adds" the value: the result of a bitwise OR with the existing byte becomes the new byte 137 | # - If has prefix '~': "removes" the value: the result of a applying a bitwise AND with the bitwise complement of to the existing byte becomes the new byte 138 | patchByteInByteString() { 139 | local ndx=$1 byteSpec=$2 byteVal byteStr charPos op='' charsBefore='' charsAfter='' currByte 140 | byteStr=$( 0 && charPos < ${#byteStr} )) || return 1 159 | # Determine the target byte, and strings before and after the byte to patch. 160 | (( charPos >= 2 )) && charsBefore=${byteStr:0:charPos} 161 | charsAfter=${byteStr:charPos + 2} 162 | # Determine the new byte value 163 | if [[ -n $op ]]; then 164 | currByte=${byteStr:charPos:2} 165 | printf -v patchedByte '%02X' "$(( 0x${currByte} $op 0x${byteVal} ))" 166 | else 167 | patchedByte=$byteSpec 168 | fi 169 | printf '%s%s%s' "$charsBefore" "$patchedByte" "$charsAfter" 170 | } 171 | 172 | # hasAttrib 173 | hasAttrib() { 174 | xattr "$1" | /usr/bin/grep -Fqx "$2" 175 | } 176 | 177 | # hasIconsResource 178 | hasIconsResource() { 179 | local file=$1 180 | getResourceByteString "$file" | /usr/bin/grep -Fq "$kMAGICBYTES_ICNS_RESOURCE" 181 | } 182 | 183 | 184 | # setCustomIcon 185 | setCustomIcon() { 186 | 187 | local fileOrFolder=$1 imgFile=$2 188 | 189 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder && -w $fileOrFolder ]] || return 3 190 | [[ -f $imgFile ]] || return 3 191 | 192 | # !! 193 | # !! Sadly, Apple decided to remove the `-i` / `--addicon` option from the `sips` utility. 194 | # !! Therefore, use of *Cocoa* is required, which we do *via Python*, which has the added advantage 195 | # !! of creating a *set* of icons from the source image, scaling as necessary to create a 196 | # !! 512 x 512 top resolution icon (whereas sips -i created a single, 128 x 128 icon). 197 | # !! Thanks, https://apple.stackexchange.com/a/161984/28668 198 | # !! 199 | # !! Note: setIcon_forFile_options_() seemingly always indicates True, even with invalid image files, so 200 | # !! we attempt no error handling in the Python code. 201 | /usr/bin/python - "$imgFile" "$fileOrFolder" <<'EOF' || return 202 | import Cocoa 203 | import sys 204 | 205 | Cocoa.NSWorkspace.sharedWorkspace().setIcon_forFile_options_(Cocoa.NSImage.alloc().initWithContentsOfFile_(sys.argv[1].decode('utf-8')), sys.argv[2].decode('utf-8'), 0) 206 | EOF 207 | 208 | 209 | # Verify that a resource fork with icons was actually created. 210 | # For *files*, the resource fork is embedded in the file itself. 211 | # For *folders* a hidden file named $'Icon\r' is created *inside the folder*. 212 | [[ -d $fileOrFolder ]] && fileWithResourceFork=${fileOrFolder}/$kFILENAME_FOLDERCUSTOMICON || fileWithResourceFork=$fileOrFolder 213 | hasIconsResource "$fileWithResourceFork" || { 214 | cat >&2 < 226 | getCustomIcon() { 227 | 228 | local fileOrFolder=$1 icnsOutFile=$2 byteStr fileWithResourceFork byteOffset byteCount 229 | 230 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder ]] || return 3 231 | 232 | # Determine what file to extract the resource fork from. 233 | if [[ -d $fileOrFolder ]]; then 234 | fileWithResourceFork=${fileOrFolder}/$kFILENAME_FOLDERCUSTOMICON 235 | [[ -f $fileWithResourceFork ]] || { echo "Custom-icon file does not exist: '${fileWithResourceFork/$'\r'/\\r}'" >&2; return 1; } 236 | else 237 | fileWithResourceFork=$fileOrFolder 238 | fi 239 | 240 | # Determine (based on format description at https://en.wikipedia.org/wiki/Apple_Icon_Image_format): 241 | # - the byte offset at which the icns resource begins, via the magic literal identifying an icns resource 242 | # - the length of the resource, which is encoded in the 4 bytes right after the magic literal. 243 | read -r byteOffset byteCount < <(getResourceByteString "$fileWithResourceFork" | /usr/bin/awk -F "$kMAGICBYTES_ICNS_RESOURCE" '{ printf "%s %d", (length($1) + 2) / 2, "0x" substr($2, 0, 8) }') 244 | (( byteOffset > 0 && byteCount > 0 )) || { echo "Custom-icon file contains no icons resource: '${fileWithResourceFork/$'\r'/\\r}'" >&2; return 1; } 245 | 246 | # Extract the actual bytes using tail and head and save them to the output file. 247 | tail -c "+${byteOffset}" "$fileWithResourceFork/..namedfork/rsrc" | head -c $byteCount > "$icnsOutFile" || return 248 | 249 | return 0 250 | } 251 | 252 | # removeCustomIcon 253 | removeCustomIcon() { 254 | 255 | local fileOrFolder=$1 byteStr 256 | 257 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder && -w $fileOrFolder ]] || return 1 258 | 259 | # Step 1: Turn off the custom-icon flag in the com.apple.FinderInfo extended attribute. 260 | if hasAttrib "$fileOrFolder" com.apple.FinderInfo; then 261 | byteStr=$(getAttribByteString "$fileOrFolder" com.apple.FinderInfo | patchByteInByteString $kFI_BYTEOFFSET_CUSTOMICON '~'$kFI_VAL_CUSTOMICON) || return 262 | if [[ $byteStr == "$kFI_BYTES_BLANK" ]]; then # All bytes cleared? Remove the entire attribute. 263 | xattr -d com.apple.FinderInfo "$fileOrFolder" 264 | else # Update the attribute. 265 | xattr -wx com.apple.FinderInfo "$byteStr" "$fileOrFolder" || return 266 | fi 267 | fi 268 | 269 | # Step 2: Remove the resource fork (if target is a file) / hidden file with custom icon (if target is a folder) 270 | if [[ -d $fileOrFolder ]]; then 271 | rm -f "${fileOrFolder}/${kFILENAME_FOLDERCUSTOMICON}" 272 | else 273 | if hasIconsResource "$fileOrFolder"; then 274 | xattr -d com.apple.ResourceFork "$fileOrFolder" 275 | fi 276 | fi 277 | 278 | return 0 279 | } 280 | 281 | # testForCustomIcon 282 | testForCustomIcon() { 283 | 284 | local fileOrFolder=$1 byteStr byteVal fileWithResourceFork 285 | 286 | [[ (-f $fileOrFolder || -d $fileOrFolder) && -r $fileOrFolder ]] || return 3 287 | 288 | # Step 1: Check if the com.apple.FinderInfo extended attribute has the custom-icon 289 | # flag set. 290 | byteStr=$(getAttribByteString "$fileOrFolder" com.apple.FinderInfo 2>/dev/null) || return 1 291 | 292 | byteVal=${byteStr:2*kFI_BYTEOFFSET_CUSTOMICON:2} 293 | 294 | (( byteVal & kFI_VAL_CUSTOMICON )) || return 1 295 | 296 | # Step 2: Check if the resource fork of the relevant file contains an icns resource 297 | if [[ -d $fileOrFolder ]]; then 298 | fileWithResourceFork=${fileOrFolder}/${kFILENAME_FOLDERCUSTOMICON} 299 | else 300 | fileWithResourceFork=$fileOrFolder 301 | fi 302 | 303 | hasIconsResource "$fileWithResourceFork" || return 1 304 | 305 | return 0 306 | } 307 | 308 | # --- End: SPECIFIC HELPER FUNCTIONS 309 | 310 | # --- Begin: SPECIFIC SCRIPT-GLOBAL CONSTANTS 311 | 312 | kFILENAME_FOLDERCUSTOMICON=$'Icon\r' 313 | 314 | # The blank hex dump form (single string of pairs of hex digits) of the 32-byte data structure stored in extended attribute 315 | # com.apple.FinderInfo 316 | kFI_BYTES_BLANK='0000000000000000000000000000000000000000000000000000000000000000' 317 | 318 | # The hex dump form of the full 32 bytes that Finder assigns to the hidden $'Icon\r' 319 | # file whose com.apple.ResourceFork extended attribute contains the icon image data for the enclosing folder. 320 | # The first 8 bytes spell out the magic literal 'iconMACS'; they are followed by the invisibility flag, '40' in the 9th byte, and '10' (?? specifying what?) 321 | # in the 10th byte. 322 | # NOTE: Since file $'Icon\r' serves no other purpose than to store the icon, it is 323 | # safe to simply assign all 32 bytes blindly, without having to worry about 324 | # preserving existing values. 325 | kFI_BYTES_CUSTOMICONFILEFORFOLDER='69636F6E4D414353401000000000000000000000000000000000000000000000' 326 | 327 | # The hex dump form of the magic literal inside a resource fork that marks the 328 | # start of an icns (icons) resource. 329 | # NOTE: This will be used with `xxd -p .. | tr -d '\n'`, which uses *lowercase* 330 | # hex digits, so we must use lowercase here. 331 | kMAGICBYTES_ICNS_RESOURCE='69636e73' 332 | 333 | # The byte values (as hex strings) of the flags at the relevant byte position 334 | # of the com.apple.FinderInfo extended attribute. 335 | kFI_VAL_CUSTOMICON='04' 336 | 337 | # The custom-icon-flag byte offset in the com.apple.FinderInfo extended attribute. 338 | kFI_BYTEOFFSET_CUSTOMICON=8 339 | 340 | # --- End: SPECIFIC SCRIPT-GLOBAL CONSTANTS 341 | 342 | # Option defaults. 343 | force=0 quiet=0 344 | 345 | # --- Begin: OPTIONS PARSING 346 | allowOptsAfterOperands=1 operands=() i=0 optName= isLong=0 prefix= optArg= haveOptArgAttached=0 haveOptArgAsNextArg=0 acceptOptArg=0 needOptArg=0 347 | while (( $# )); do 348 | if [[ $1 =~ ^(-)[a-zA-Z0-9]+.*$ || $1 =~ ^(--)[a-zA-Z0-9]+.*$ ]]; then # an option: either a short option / multiple short options in compressed form or a long option 349 | prefix=${BASH_REMATCH[1]}; [[ $prefix == '--' ]] && isLong=1 || isLong=0 350 | for (( i = 1; i < (isLong ? 2 : ${#1}); i++ )); do 351 | acceptOptArg=0 needOptArg=0 haveOptArgAttached=0 haveOptArgAsNextArg=0 optArgAttached= optArgOpt= optArgReq= 352 | if (( isLong )); then # long option: parse into name and, if present, argument 353 | optName=${1:2} 354 | [[ $optName =~ ^([^=]+)=(.*)$ ]] && { optName=${BASH_REMATCH[1]}; optArgAttached=${BASH_REMATCH[2]}; haveOptArgAttached=1; } 355 | else # short option: *if* it takes an argument, the rest of the string, if any, is by definition the argument. 356 | optName=${1:i:1}; optArgAttached=${1:i+1}; (( ${#optArgAttached} >= 1 )) && haveOptArgAttached=1 357 | fi 358 | (( haveOptArgAttached )) && optArgOpt=$optArgAttached optArgReq=$optArgAttached || { (( $# > 1 )) && { optArgReq=$2; haveOptArgAsNextArg=1; }; } 359 | # ---- BEGIN: CUSTOMIZE HERE 360 | case $optName in 361 | f|force) 362 | force=1 363 | ;; 364 | q|quiet) 365 | quiet=1 366 | ;; 367 | *) 368 | dieSyntax "Unknown option: ${prefix}${optName}." 369 | ;; 370 | esac 371 | # ---- END: CUSTOMIZE HERE 372 | (( needOptArg )) && { (( ! haveOptArgAttached && ! haveOptArgAsNextArg )) && dieSyntax "Option ${prefix}${optName} is missing its argument." || (( haveOptArgAsNextArg )) && shift; } 373 | (( acceptOptArg || needOptArg )) && break 374 | done 375 | else # an operand 376 | if [[ $1 == '--' ]]; then 377 | shift; operands+=( "$@" ); break 378 | elif (( allowOptsAfterOperands )); then 379 | operands+=( "$1" ) # continue 380 | else 381 | operands=( "$@" ) 382 | break 383 | fi 384 | fi 385 | shift 386 | done 387 | (( "${#operands[@]}" > 0 )) && set -- "${operands[@]}"; unset allowOptsAfterOperands operands i optName isLong prefix optArgAttached haveOptArgAttached haveOptArgAsNextArg acceptOptArg needOptArg 388 | # --- End: OPTIONS PARSING: "$@" now contains all operands (non-option arguments). 389 | 390 | # Validate the command 391 | cmd=$(printf %s "$1" | tr '[:upper:]' '[:lower:]') # translate to all-lowercase - we don't want the command name to be case-sensitive 392 | [[ $cmd == 'remove' ]] && cmd='rm' # support alias 'remove' for 'rm' 393 | case $cmd in 394 | set|get|rm|remove|test) 395 | shift 396 | ;; 397 | *) 398 | dieSyntax "Unrecognized or missing command: '$cmd'." 399 | ;; 400 | esac 401 | 402 | # Validate file operands 403 | (( $# > 0 )) || dieSyntax "Missing operand(s)." 404 | 405 | # Target file or folder. 406 | targetFileOrFolder=$1 imgFile= outFile= 407 | [[ -f $targetFileOrFolder || -d $targetFileOrFolder ]] || die "Target not found or neither file nor folder: '$targetFileOrFolder'" 408 | # Make sure the target file/folder is readable, and, unless only getting or testing for an icon are requested, writeable too. 409 | [[ -r $targetFileOrFolder ]] || die "Cannot access '$targetFileOrFolder': you do not have read permissions." 410 | [[ $cmd == 'test' || $cmd == 'get' || -w $targetFileOrFolder ]] || die "Cannot modify '$targetFileOrFolder': you do not have write permissions." 411 | 412 | # Other operands, if any, and their number. 413 | valid=0 414 | case $cmd in 415 | 'set') 416 | (( $# <= 2 )) && { 417 | valid=1 418 | # If no image file was specified, the target file is assumed to be an image file itself whose image should be self-assigned as an icon. 419 | (( $# == 2 )) && imgFile=$2 || imgFile=$1 420 | # !! Apparently, a regular file is required - a process subsitution such 421 | # !! as `<(base64 -D ' 504 | # - All other headings should be level-2 headings in ALL-CAPS. 505 | # - TEXT 506 | # - Use NO indentation for regular chapter text; if you do, it will 507 | # be indented further than list items. 508 | # - Use 4-space indentation, as usual, for code blocks. 509 | # - Markup character-styling markup translates to ROFF rendering as follows: 510 | # `...` and **...** render as bolded (red) text 511 | # _..._ and *...* render as word-individually underlined text 512 | # - LISTS 513 | # - Indent list items by 2 spaces for better plain-text viewing, but note 514 | # that the ROFF generated by marked-man still renders them unindented. 515 | # - End every list item (bullet point) itself with 2 trailing spaces too so 516 | # that it renders on its own line. 517 | # - Avoid associating more than 1 paragraph with a list item, if possible, 518 | # because it requires the following trick, which hampers plain-text readability: 519 | # Use ' ' in lieu of an empty line. 520 | #### 521 | : <<'EOF_MAN_PAGE' 522 | # fileicon(1) - manage file and folder custom icons 523 | 524 | ## SYNOPSIS 525 | 526 | Manage custom icons for files and folders on macOS. 527 | 528 | SET a custom icon for a file or folder: 529 | 530 | fileicon set [] 531 | 532 | REMOVE a custom icon from a file or folder: 533 | 534 | fileicon rm 535 | 536 | GET a file or folder's custom icon: 537 | 538 | fileicon get [-f] [] 539 | 540 | -f ... force replacement of existing output file 541 | 542 | TEST if a file or folder has a custom icon: 543 | 544 | fileicon test 545 | 546 | All forms: option -q silences status output. 547 | 548 | Standard options: `--help`, `--man`, `--version`, `--home` 549 | 550 | ## DESCRIPTION 551 | 552 | `` is the file or folder whose custom icon should be managed. 553 | Note that symlinks are followed to their (ultimate target); that is, you 554 | can only assign custom icons to regular files and folders, not to symlinks 555 | to them. 556 | 557 | `` can be an image file of any format supported by the system. 558 | It is converted to an icon and assigned to ``. 559 | If you omit ``, `` must itself be an image file whose 560 | image should become its own icon. 561 | 562 | `` specifies the file to extract the custom icon to: 563 | Defaults to the filename of `` with extension `.icns` appended. 564 | If a value is specified, extension `.icns` is appended, unless already present. 565 | Either way, extraction fails if the target file already exists; use `-f` to 566 | override. 567 | Specify `-` to extract to stdout. 568 | 569 | Command `test` signals with its exit code whether a custom icon is set (0) 570 | or not (1); any other exit code signals an unexpected error. 571 | 572 | **Options**: 573 | 574 | * `-f`, `--force` 575 | When getting (extracting) a custom icon, forces replacement of the 576 | output file, if it already exists. 577 | 578 | * `-q`, `--quiet` 579 | Suppresses output of the status information that is by default output to 580 | stdout. 581 | Note that errors and warnings are still printed to stderr. 582 | 583 | ## NOTES 584 | 585 | Custom icons are stored in extended attributes of the HFS+ filesystem. 586 | Thus, if you copy files or folders to a different filesystem that doesn't 587 | support such attributes, custom icons are lost; for instance, custom icons 588 | cannot be stored in a Git repository. 589 | 590 | To determine if a give file or folder has extended attributes, use 591 | `ls -l@ `. 592 | 593 | When setting an image as a custom icon, a set of icons with several resolutions 594 | is created, with the highest resolution at 512 x 512 pixels. 595 | 596 | All icons created are square, so images with a non-square aspect ratio will 597 | appear distorted; for best results, use square imges. 598 | 599 | ## STANDARD OPTIONS 600 | 601 | All standard options provide information only. 602 | 603 | * `-h, --help` 604 | Prints the contents of the synopsis chapter to stdout for quick reference. 605 | 606 | * `--man` 607 | Displays this manual page, which is a helpful alternative to using `man`, 608 | if the manual page isn't installed. 609 | 610 | * `--version` 611 | Prints version information. 612 | 613 | * `--home` 614 | Opens this utility's home page in the system's default web browser. 615 | 616 | ## LICENSE 617 | 618 | For license information and more, visit the home page by running 619 | `fileicon --home` 620 | 621 | EOF_MAN_PAGE 622 | -------------------------------------------------------------------------------- /assets/folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjl0602/flutter-assets-maker/fd1ab536c09496f8bf2599033f90b71cb2e24f18/assets/folder.png -------------------------------------------------------------------------------- /assets/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjl0602/flutter-assets-maker/fd1ab536c09496f8bf2599033f90b71cb2e24f18/assets/ic_launcher.png -------------------------------------------------------------------------------- /assets/ios.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjl0602/flutter-assets-maker/fd1ab536c09496f8bf2599033f90b71cb2e24f18/assets/ios.png -------------------------------------------------------------------------------- /assets/shape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mjl0602/flutter-assets-maker/fd1ab536c09496f8bf2599033f90b71cb2e24f18/assets/shape.png -------------------------------------------------------------------------------- /builder/flutter.js: -------------------------------------------------------------------------------- 1 | const sharp = require("sharp"); 2 | const fs = require("fs"); 3 | const { 4 | file, 5 | resolve, 6 | find, 7 | savefile, 8 | mkdir, 9 | exists, 10 | copyFile, 11 | } = require("../tools/file"); 12 | const { resizeAndSave, deltaOf } = require("../tools/image"); 13 | const { makeios, makeAndroid } = require("./ios"); 14 | module.exports = { 15 | initFlutter, 16 | makePreview, 17 | makeflutter, 18 | make, 19 | makeFolder, 20 | }; 21 | 22 | async function initFlutter(flutterProjectPath = process.cwd()) { 23 | await mkdir(`${flutterProjectPath}/assets`); 24 | await mkdir(`${flutterProjectPath}/assets/fmaker`); 25 | 26 | let android = `${flutterProjectPath}/assets/fmaker/android_icon.png`; 27 | let ios = `${flutterProjectPath}/assets/fmaker/ios_icon.png`; 28 | let img = `${flutterProjectPath}/assets/fmaker/example@3x.png`; 29 | 30 | var files = fs.readdirSync(`${flutterProjectPath}/assets/fmaker/`); 31 | if (files.length == 0) { 32 | console.log("添加示例图 example@3x.png"); 33 | await copyFile(resolve("../assets/example@3x.png"), img); 34 | } else { 35 | console.log("fmaker文件夹非空,无需添加示例图"); 36 | } 37 | 38 | await copyFile(resolve("../assets/ic_launcher.png"), android); 39 | await copyFile(resolve("../assets/ios.png"), ios); 40 | 41 | addIgnoreIfNeed(); 42 | console.log( 43 | `已经增加示例资源:${android},\n${ios},\n${img}\n查看这些文件,最好替换他们,再来试试 fmaker build` 44 | ); 45 | } 46 | 47 | function addIgnoreIfNeed() { 48 | // 处理.gitignore 49 | let cmdPath = process.cwd(); 50 | console.log("\n检查 .gitignore"); 51 | if (!fs.existsSync(`${cmdPath}/.gitignore`)) { 52 | // fs.writeFileSync(`${cmdPath}/.gitignore`, ''); 53 | console.log("没有发现.gitignore文件,建议创建.gitignore文件"); 54 | return; 55 | } 56 | let gitignore = fs.readFileSync(`${cmdPath}/.gitignore`, { 57 | encoding: "utf-8", 58 | }); 59 | if (gitignore.indexOf("\nlib/r.preview.dart\n") == -1) { 60 | gitignore = 61 | gitignore + "\n\n# ignore assets preview file\nlib/r.preview.dart\n"; 62 | fs.writeFileSync(`${cmdPath}/.gitignore`, gitignore); 63 | console.log(".gitignore 添加完成"); 64 | } else { 65 | console.log("无需添加.gitignore"); 66 | } 67 | } 68 | 69 | const execSync = require("child_process").execSync; 70 | 71 | /// 创建图标 72 | async function makeFolder(flutterProjectPath = process.cwd()) { 73 | let isFlutter = await exists(`${flutterProjectPath}/pubspec.yaml`); 74 | if (!isFlutter) { 75 | console.log( 76 | `${flutterProjectPath}/pubspec.yaml 不存在`, 77 | "你必须在flutter目录下运行" 78 | ); 79 | return false; 80 | } 81 | let hasIcon = await exists( 82 | `${flutterProjectPath}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png` 83 | ); 84 | if (!hasIcon) { 85 | console.log( 86 | `ios icon 不存在`, 87 | "必须在flutter工程下运行,fmaker将自动获取iOS项目下的图标" 88 | ); 89 | return false; 90 | } 91 | 92 | // 设置图标的脚本的位置 93 | var shellPath = resolve("../assets/fileicon"); 94 | 95 | // 定义 96 | var size = 256; 97 | var iconSize = 120; 98 | var muti = 2; 99 | // 图包素材 100 | var folderIcon = sharp(resolve("../assets/folder.png")).resize( 101 | size * muti, 102 | size * muti 103 | ); 104 | var rawIcon = sharp( 105 | `${flutterProjectPath}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png` 106 | ).resize(iconSize * muti, iconSize * muti); 107 | 108 | var iconShape = sharp(resolve("../assets/shape.png")); 109 | var iconBack = sharp(resolve("../assets/back.png")); 110 | 111 | var icon = iconShape.resize(iconSize * muti, iconSize * muti).composite([ 112 | { 113 | input: await rawIcon.toBuffer(), 114 | left: 0, 115 | top: 0, 116 | blend: "in", 117 | }, 118 | ]); 119 | 120 | var result = await folderIcon 121 | .composite([ 122 | { 123 | input: await iconBack.resize(128 * muti, 128 * muti).toBuffer(), 124 | top: 71 * muti, 125 | left: 64 * muti, 126 | }, 127 | { 128 | input: await icon.toBuffer(), 129 | top: 75 * muti, 130 | left: 68 * muti, 131 | }, 132 | ]) 133 | .toBuffer(); 134 | 135 | var targetFilePath = `${flutterProjectPath}/_icon.png`; 136 | console.log("生成图标中..."); 137 | await savefile(targetFilePath, result); 138 | var res = execSync( 139 | `${shellPath} set ${flutterProjectPath} ${targetFilePath}` 140 | ).toString(); 141 | console.log("正在设置图标:", res); 142 | console.log("图标设置成功"); 143 | 144 | console.log("\n清理..."); 145 | fs.rmSync(targetFilePath); 146 | console.log("清理完成"); 147 | 148 | // 处理.gitignore 149 | console.log("\n尝试添加 .gitignore"); 150 | let gitignore = fs.readFileSync(`${flutterProjectPath}/.gitignore`, { 151 | encoding: "utf-8", 152 | }); 153 | // console.log(gitignore); 154 | if (gitignore.indexOf("\nIcon?\n") == -1) { 155 | gitignore = gitignore + "\n\n# fmaker folder icon\nIcon?\n"; 156 | fs.writeFileSync(`${flutterProjectPath}/.gitignore`, gitignore); 157 | // console.log(gitignore); 158 | console.log(".gitignore 添加完成"); 159 | } else { 160 | console.log("无需添加.gitignore"); 161 | } 162 | } 163 | 164 | async function makeflutter(flutterProjectPath = process.cwd(), config) { 165 | var { ios: _makeIOS, android: _buildAndroid, assets: _makeAssets } = config; 166 | addIgnoreIfNeed(); 167 | let isFlutter = await exists(`${flutterProjectPath}/pubspec.yaml`); 168 | if (!isFlutter) { 169 | console.log( 170 | `${flutterProjectPath}/pubspec.yaml 不存在`, 171 | "你必须在flutter目录下运行" 172 | ); 173 | return false; 174 | } 175 | let isInit = await exists(`${flutterProjectPath}/assets/fmaker`); 176 | if (!isInit) { 177 | await mkdir(`${flutterProjectPath}/assets`); 178 | await mkdir(`${flutterProjectPath}/assets/fmaker`); 179 | } 180 | let files = await find(`${flutterProjectPath}/assets/fmaker`); 181 | console.log(`读取到${files.length}个文件`); 182 | if (files.length == 0) { 183 | console.log("请先添加文件到fmaker目录"); 184 | } 185 | var allAvaliableFiles = []; 186 | for (const imgPath of files) { 187 | if (imgPath.indexOf(".png") < 1) { 188 | continue; 189 | } 190 | 191 | await make(imgPath, async (imageName, delta, isCheck) => { 192 | if (imageName == "ios_icon" && !!_makeIOS) { 193 | await makeios(imgPath, `${flutterProjectPath}/ios`); 194 | return ""; 195 | } 196 | if (imageName == "android_icon" && !!_buildAndroid) { 197 | await makeAndroid(imgPath, `${flutterProjectPath}/android`); 198 | return ""; 199 | } 200 | if (delta == 1) { 201 | if (!isCheck) { 202 | // console.log("创建资源图", imageName); 203 | allAvaliableFiles.push({ 204 | name: imageName, 205 | path: imgPath, 206 | }); 207 | } 208 | return `${flutterProjectPath}/assets/${imageName}.png`; 209 | } 210 | if (!!_makeAssets) 211 | await mkdir(`${flutterProjectPath}/assets/${delta}.0x/`); 212 | return `${flutterProjectPath}/assets/${delta}.0x/${imageName}.png`; 213 | }); 214 | } 215 | if (!_makeAssets) return; 216 | console.log("资源目录:", allAvaliableFiles); 217 | // 保存到yaml 218 | var assetsListString = allAvaliableFiles 219 | .map((img) => { 220 | return ` - assets/${img.name}.png`; 221 | }) 222 | .join("\n"); 223 | console.log(assetsListString); 224 | var replaceSuccess = replaceStringInFile( 225 | `${flutterProjectPath}/pubspec.yaml`, 226 | /(# fmaker)[\w\W]*(# fmaker-end)/g, 227 | "# fmaker\n # fmaker-end" 228 | ); 229 | var generateSuccess = replaceStringInFile( 230 | `${flutterProjectPath}/pubspec.yaml`, 231 | "# fmaker", 232 | "# fmaker\n" + 233 | assetsListString + 234 | (replaceSuccess ? "" : "\n # fmaker-end") 235 | ); 236 | 237 | if (!generateSuccess) { 238 | console.log( 239 | "\n在pubspec.yaml中没有找到生成标记,请添加‘# fmaker’标记!!\n" 240 | ); 241 | } 242 | 243 | /// 保存到r.dart 244 | await mkdir(`${flutterProjectPath}/lib`); 245 | 246 | var rContentListString = allAvaliableFiles 247 | .map((img) => { 248 | const name = img.name; 249 | var dartName = toHump(name); 250 | return ( 251 | ` /// {@macro fmaker.${dartName}.preview}\n` + 252 | ` static const String ${dartName} = 'assets/${name}.png';` 253 | ); 254 | }) 255 | .join("\n"); 256 | var rContent = `class R {\n${rContentListString}\n}`; 257 | fs.writeFileSync(`${flutterProjectPath}/lib/r.dart`, rContent); 258 | 259 | /// 保存到r.preview.dart 260 | var rPreviewContentListString = allAvaliableFiles 261 | .map((img) => { 262 | const name = img.name; 263 | var dartName = toHump(name); 264 | var stat = fs.statSync(`${img.path}`); 265 | var size = (stat.size / 1000).toFixed(1); 266 | return ( 267 | `/// {@template fmaker.${dartName}.preview}\n` + 268 | `/// R.${dartName}(${size}kb): ![](${flutterProjectPath}/assets/${name}.png) \n` + 269 | `/// \n` + 270 | `/// {@endtemplate}` 271 | ); 272 | }) 273 | .join("\n\n"); 274 | var rPreviewContent = `${rPreviewContentListString} \n\n// ignore_for_file: camel_case_types, unused_element \nclass _ {}`; 275 | fs.writeFileSync(`${flutterProjectPath}/lib/r.preview.dart`, rPreviewContent); 276 | } 277 | 278 | async function makePreview() { 279 | var flutterProjectPath = process.cwd(); 280 | addIgnoreIfNeed(); 281 | let isFlutter = await exists(`${flutterProjectPath}/pubspec.yaml`); 282 | if (!isFlutter) { 283 | console.log( 284 | `${flutterProjectPath}/pubspec.yaml 不存在`, 285 | "你必须在flutter目录下运行" 286 | ); 287 | return false; 288 | } 289 | let files = await find(`${flutterProjectPath}/assets/fmaker`); 290 | console.log(`读取到${files.length}个文件`); 291 | if (files.length == 0) { 292 | console.log("请先添加文件到fmaker目录"); 293 | return; 294 | } 295 | var allFileName = []; 296 | for (const imgPath of files) { 297 | if (imgPath.indexOf(".png") < 1) { 298 | continue; 299 | } 300 | // 获取文件名 301 | let fileName = imgPath.substring( 302 | imgPath.lastIndexOf("/") + 1, 303 | imgPath.length 304 | ); 305 | // 获取倍率 306 | let delta = deltaOf(imgPath); 307 | if (delta > 0) allFileName.push(fileName); 308 | } 309 | /// 保存到r.preview.dart 310 | var rPreviewContentListString = allFileName 311 | .map((name) => { 312 | var dartName = toHump(name); 313 | return ( 314 | `/// {@template fmaker.${dartName}.preview}\n` + 315 | `/// ![](${flutterProjectPath}/assets/${name}.png)\n` + 316 | `/// {@endtemplate}` 317 | ); 318 | }) 319 | .join("\n\n"); 320 | var rPreviewContent = `${rPreviewContentListString} \n\n// ignore_for_file: camel_case_types, unused_element \nclass _ {}`; 321 | fs.writeFileSync(`${flutterProjectPath}/lib/r.preview.dart`, rPreviewContent); 322 | } 323 | 324 | // 下划线转换驼峰 325 | function toHump(name) { 326 | return name.replace(/[\_\-\+:\(\)\[\] ](\w)/g, function (all, letter) { 327 | return letter.toUpperCase(); 328 | }); 329 | } 330 | 331 | function replaceStringInFile(file, target, replace) { 332 | var content = fs.readFileSync(file, { encoding: "UTF-8" }); 333 | var newContent = content.replace(target, replace); 334 | fs.writeFileSync(file, newContent); 335 | return content != newContent; 336 | } 337 | 338 | // 生成一张图片的低倍率版本 339 | async function make(filePath, filePathBuilder) { 340 | if (!filePathBuilder) { 341 | // 文件路径创建 342 | filePathBuilder = async (imageName, delta) => { 343 | console.log("采用默认生成"); 344 | return `${process.cwd()}/${imageName}@${delta}x.png`; 345 | }; 346 | } 347 | // 获取文件名 348 | let fileName = filePath.substring( 349 | filePath.lastIndexOf("/") + 1, 350 | filePath.length 351 | ); 352 | 353 | let imageName = fileName.replace(/@(\S*)[Xx]/g, "").replace(/\.\S*$/, ""); 354 | 355 | // 获取倍率 356 | let delta = deltaOf(filePath); 357 | console.log(`\n正在生成:${imageName} 倍率:${delta}`); 358 | // console.log("\n开始生成\n"); 359 | let image = sharp(filePath); 360 | let metadata = await image.metadata(); 361 | 362 | //先预先检查一下 363 | let precheck = await filePathBuilder(imageName, 1, true); 364 | if (!precheck) { 365 | return; 366 | } 367 | 368 | for (let i = delta; i > 0; i--) { 369 | let size = parseInt((metadata.width / delta) * i); 370 | let targetPath = await filePathBuilder(imageName, i); 371 | if (!targetPath) { 372 | console.log("中断生成"); 373 | return; 374 | } 375 | // console.log("生成中"); 376 | let info = await resizeAndSave(image, size, targetPath); 377 | // console.log( 378 | // `已生成: ${imageName} ${i}倍图,尺寸:宽:${info.width} 高${info.height}`, 379 | // ); 380 | } 381 | } 382 | -------------------------------------------------------------------------------- /builder/ios.js: -------------------------------------------------------------------------------- 1 | 2 | const sharp = require("sharp"); 3 | const { file, resolve, find, savefile, mkdir, exists } = require("../tools/file"); 4 | const { resizeAndSave } = require("../tools/image"); 5 | 6 | module.exports = { 7 | makeios, 8 | makeAndroid, 9 | } 10 | 11 | 12 | let iconConfig = [ 13 | iosLogo(1024, 1, '@1x'), 14 | iosLogo(83.5, 2), 15 | iosLogo(76, 2), 16 | iosLogo(76, 1, '@1x'), 17 | iosLogo(60, 3), 18 | iosLogo(60, 2), 19 | iosLogo(40, 3), 20 | iosLogo(40, 2), 21 | iosLogo(40, 1, '@1x'), 22 | iosLogo(29, 3), 23 | iosLogo(29, 2), 24 | iosLogo(29, 1, '@1x'), 25 | iosLogo(20, 3), 26 | iosLogo(20, 2), 27 | iosLogo(20, 1, '@1x'), 28 | ] 29 | 30 | function iosLogo(truesize, delta, sufix = "") { 31 | let fileName = `Icon-App-${truesize}x${truesize}` + ((delta > 1) ? `@${delta}x` : '') + sufix + ".png"; 32 | return { 33 | size: truesize * delta, 34 | fileName: fileName, 35 | } 36 | } 37 | 38 | function androidConfig() { 39 | return [ 40 | { 41 | size: 48, 42 | name: "mipmap-mdpi", 43 | }, 44 | { 45 | size: 72, 46 | name: "mipmap-hdpi", 47 | }, 48 | { 49 | size: 96, 50 | name: "mipmap-xhdpi", 51 | }, 52 | { 53 | size: 144, 54 | name: "mipmap-xxhdpi", 55 | }, 56 | { 57 | size: 192, 58 | name: "mipmap-xxxhdpi", 59 | }, 60 | ] 61 | } 62 | 63 | /** 64 | * 生成安卓图标 65 | * 66 | */ 67 | async function makeAndroid(filePath, androidProject = process.cwd()) { 68 | if (!filePath) { 69 | console.log("需要指定源文件"); 70 | return; 71 | } 72 | let isAndroid = await exists(`${androidProject}/build.gradle`); 73 | let androidAssetsPath; 74 | if (isAndroid) { 75 | console.log('当前目录是一个安卓项目') 76 | androidAssetsPath = `app/src/main/res` 77 | } else { 78 | console.log('当前目录似乎不是一个安卓项目目录,生成目录') 79 | androidAssetsPath = `android` 80 | await mkdir(`${androidProject}/${androidAssetsPath}/`); 81 | } 82 | let image = sharp(filePath); 83 | 84 | let square = await isSquare(image) 85 | if (!square) { 86 | console.error('\n错误:图标必须是正方形\n'); 87 | return; 88 | } 89 | 90 | for (const config of androidConfig()) { 91 | 92 | let fileName = `${androidProject}/${androidAssetsPath}/${config.name}/ic_launcher.png` 93 | console.log('生成Android图标', config); 94 | await mkdir(`${androidProject}/${androidAssetsPath}/${config.name}/`) 95 | await resizeAndSave(image, config.size, fileName); 96 | } 97 | return; 98 | } 99 | 100 | /** 101 | * 生成iOS图标,可以指定项目目录,默认在当前目录寻找iOS项目 102 | * @param filePath 103 | * @param iosProjectPath 104 | */ 105 | async function makeios(filePath, iosProjectPath = process.cwd()) { 106 | if (!filePath) { 107 | console.log("需要指定源文件"); 108 | return; 109 | } 110 | 111 | let iosProjectName = await findProjectName(iosProjectPath); 112 | 113 | let iosAssetsPath; 114 | if (iosProjectName) { 115 | iosAssetsPath = `${iosProjectName}/Assets.xcassets` 116 | } else { 117 | console.log('当前目录似乎不是一个iOS项目目录,生成目录Assets.xcassets') 118 | iosAssetsPath = `Assets.xcassets` 119 | } 120 | 121 | let image = sharp(filePath); 122 | 123 | let square = await isSquare(image) 124 | if (!square) { 125 | console.error('\n错误:iOS图标必须是正方形,且没有alpha通道!!!\n'); 126 | return; 127 | } 128 | 129 | await mkdir(`${iosProjectPath}/${iosAssetsPath}`) 130 | await mkdir(`${iosProjectPath}/${iosAssetsPath}/AppIcon.appiconset`) 131 | let contents = await file(resolve("../assets/Contents.json")) 132 | await savefile(`${iosProjectPath}/${iosAssetsPath}/AppIcon.appiconset/Contents.json`, contents); 133 | for (const config of iconConfig) { 134 | console.log('生成iOS图标', config); 135 | await resizeAndSave(image, config.size, `${iosProjectPath}/${iosAssetsPath}/AppIcon.appiconset/${config.fileName}`) 136 | } 137 | return; 138 | } 139 | 140 | async function isSquare(image) { 141 | return new Promise((r, e) => { 142 | image.metadata((err, metadata) => { 143 | if (err) { 144 | r(false) 145 | return 146 | } 147 | if (metadata.width === metadata.height) { 148 | // console.log(metadata); 149 | r(true); 150 | return 151 | } 152 | r(false); 153 | }) 154 | }) 155 | } 156 | 157 | 158 | async function findProjectName(path) { 159 | let pathList = await find(path); 160 | let iosProjectName = '' 161 | for (const file of pathList) { 162 | // console.log(file); 163 | let name = file.substring( 164 | file.lastIndexOf("/") + 1, 165 | file.length, 166 | ); 167 | if (name.indexOf('.xcodeproj') > 1) { 168 | iosProjectName = name.replace('.xcodeproj', ''); 169 | } 170 | } 171 | return iosProjectName; 172 | } 173 | 174 | 175 | -------------------------------------------------------------------------------- /builder/screenshot.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const sharp = require("sharp"); 3 | 4 | async function makeScreenshot() { 5 | const path = process.cwd(); 6 | var files = fs.readdirSync(path); 7 | for (const fileName of files) { 8 | if (!fileName.endsWith(".PNG")) continue; 9 | let image = sharp(`${path}/${fileName}`); 10 | const size = await sizeOf(image); 11 | const radio = size.height / size.width; 12 | if (radio > 2) { 13 | // iPhone x 1242*2688 14 | await resizeAndSave(image, [1242, 2688], `${path}/ipxs(1242*2688)/${fileName}`); 15 | } else { 16 | // iPhone 6s 1242*2208 17 | await resizeAndSave(image, [1242, 2208], `${path}/ip6s(1242*2208)/${fileName}`); 18 | } 19 | console.log(fileName, size, radio); 20 | } 21 | } 22 | 23 | async function sizeOf(image) { 24 | return new Promise((r, e) => { 25 | image.metadata((err, metadata) => { 26 | if (err) { 27 | r(undefined); 28 | return; 29 | } 30 | r({ 31 | width: metadata.width, 32 | height: metadata.height, 33 | }); 34 | }); 35 | }); 36 | } 37 | 38 | function resizeAndSave(image, size, fileName) { 39 | // console.log("resizeAndSave", fileName); 40 | var targetPath = fileName.split("/"); 41 | targetPath.pop(); 42 | targetPath = targetPath.join("/"); 43 | fs.mkdirSync(targetPath, { 44 | recursive: true, 45 | }); 46 | return new Promise((r, e) => { 47 | image.resize(size[0], size[1]).toFile(fileName, (err, info) => { 48 | err ? e(err) : r(info); 49 | }); 50 | }); 51 | } 52 | 53 | module.exports = { 54 | makeScreenshot, 55 | }; 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const exec = require("child_process").exec; 4 | const fs = require("fs"); 5 | const { program } = require("commander"); 6 | const { join } = require("path"); 7 | 8 | const { makeios, makeAndroid } = require("./builder/ios"); 9 | const { makeScreenshot } = require("./builder/screenshot"); 10 | const { 11 | initFlutter, 12 | makeFolder, 13 | makeflutter, 14 | makePreview, 15 | make, 16 | } = require("./builder/flutter"); 17 | 18 | /** 初始化项目 */ 19 | const init = program.command("init"); 20 | init 21 | .description( 22 | "在一个Flutter项目中初始化tmaker,为你创建文件夹,添加示例文件和添加.gitignore参数" 23 | ) 24 | .action(async (_, __) => { 25 | console.log("为你添加一些示例图片"); 26 | initFlutter(); 27 | }); 28 | 29 | /** 创建项目 */ 30 | program 31 | .command("build [parts]") 32 | .description( 33 | "创建资源,可指定创建指定部分,例: fmaker build ios,android,assets" 34 | ) 35 | .action(async (parts, __) => { 36 | console.log("创建flutter资源", parts); 37 | var partList = (parts || "").split(",").filter((e) => !!e); 38 | if (partList.length == 0) partList = ["ios", "android", "assets"]; 39 | await makeflutter(process.cwd(), { 40 | ios: !!~partList.indexOf("ios"), 41 | android: !!~partList.indexOf("android"), 42 | assets: !!~partList.indexOf("assets"), 43 | }); 44 | console.log("\nflutter资源全部创建完成\n"); 45 | }); 46 | 47 | /** 仅创建图片预览 */ 48 | program 49 | .command("preview") 50 | .description("仅创建资源的预览注释,也就是r.preview.dart文件") 51 | .action(async (_, __) => { 52 | console.log("创建注释中"); 53 | await makePreview(process.cwd()); 54 | console.log("\n注释全部创建完成\n"); 55 | }); 56 | 57 | /** 项目文件夹图标 */ 58 | const folder = program.command("folder"); 59 | folder 60 | .description("把app的图标渲染在本项目的文件夹上(仅mac)") 61 | .action(async (_, __) => { 62 | console.log("添加项目文件夹图标"); 63 | await makeFolder(); 64 | console.log("\n设置项目文件夹图标完成\n"); 65 | }); 66 | // program.addCommand(folder); 67 | /** 项目文件夹图标 */ 68 | const screenshot = program.command("screenshot"); 69 | screenshot 70 | .description("处理当前文件夹下的截图,处理成Apple的标准尺寸") 71 | .action(async (_, __) => { 72 | console.log("开始处理截图"); 73 | await makeScreenshot(); 74 | console.log("\n处理截图完成\n"); 75 | }); 76 | 77 | // 设置版本 78 | program.version("2.0.0"); 79 | // program.option("-i", "生成iOS图标"); 80 | // program.option("-a", "生成安卓图标"); 81 | // program.option("-p", "生成资源"); 82 | 83 | program.parse(process.argv); 84 | -------------------------------------------------------------------------------- /index_old.js: -------------------------------------------------------------------------------- 1 | // #!/usr/bin/env node 2 | // const sharp = require("sharp"); 3 | 4 | // const fs = require("fs"); 5 | // const join = require("path").join; 6 | // const path = require("path"); 7 | 8 | // // const { file, resolve, find, savefile, mkdir, exists } = require("../tools/file"); 9 | 10 | // const { makeios, makeAndroid } = require("./builder/ios"); 11 | // const { initFlutter, makeFolder, makeflutter, make } = require("./builder/flutter"); 12 | 13 | // /// 当前执行命令的路径 14 | // let execPath = process.cwd(); 15 | 16 | // main(process.argv); 17 | 18 | // async function main(args) { 19 | // console.log("args", args); 20 | // if (args[2] == "init") { 21 | // console.log("为你添加一些示例图片"); 22 | // initFlutter(); 23 | // return; 24 | // } else if (args[2] == "make") { 25 | // console.log("正在通过指定文件创建低倍图"); 26 | // make(args[3]); 27 | // return; 28 | // } else if (args[2] == "ios") { 29 | // console.log("单独创建iOS图标"); 30 | // makeios(args[3]); 31 | // return; 32 | // } else if (args[2] == "android") { 33 | // console.log("单独创建安卓图标"); 34 | // makeAndroid(args[3]); 35 | // return; 36 | // } else if (args[2] == "build") { 37 | // console.log("创建flutter资源"); 38 | // await makeflutter(); 39 | // console.log("\nflutter资源全部创建完成\n"); 40 | // return; 41 | // } else if (args[2] == "folder") { 42 | // console.log("添加项目文件夹图标"); 43 | // await makeFolder(); 44 | // console.log("\n设置项目文件夹图标完成\n"); 45 | // return; 46 | // } 47 | // console.log("没有对应指令,fmaker已安装"); 48 | // console.log(args[2]); 49 | // } 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-assets-maker", 3 | "version": "1.2.2", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "ansi-regex": { 8 | "version": "2.1.1", 9 | "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-2.1.1.tgz", 10 | "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==" 11 | }, 12 | "aproba": { 13 | "version": "1.2.0", 14 | "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", 15 | "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==" 16 | }, 17 | "are-we-there-yet": { 18 | "version": "1.1.7", 19 | "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.7.tgz", 20 | "integrity": "sha512-nxwy40TuMiUGqMyRHgCSWZ9FM4VAoRP4xUYSTv5ImRog+h9yISPbVH7H8fASCIzYn9wlEv4zvFL7uKDMCFQm3g==", 21 | "requires": { 22 | "delegates": "^1.0.0", 23 | "readable-stream": "^2.0.6" 24 | } 25 | }, 26 | "base64-js": { 27 | "version": "1.5.1", 28 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", 29 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" 30 | }, 31 | "bl": { 32 | "version": "4.1.0", 33 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", 34 | "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", 35 | "requires": { 36 | "buffer": "^5.5.0", 37 | "inherits": "^2.0.4", 38 | "readable-stream": "^3.4.0" 39 | }, 40 | "dependencies": { 41 | "readable-stream": { 42 | "version": "3.6.0", 43 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 44 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 45 | "requires": { 46 | "inherits": "^2.0.3", 47 | "string_decoder": "^1.1.1", 48 | "util-deprecate": "^1.0.1" 49 | } 50 | } 51 | } 52 | }, 53 | "buffer": { 54 | "version": "5.7.1", 55 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", 56 | "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", 57 | "requires": { 58 | "base64-js": "^1.3.1", 59 | "ieee754": "^1.1.13" 60 | } 61 | }, 62 | "chownr": { 63 | "version": "1.1.4", 64 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", 65 | "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" 66 | }, 67 | "code-point-at": { 68 | "version": "1.1.0", 69 | "resolved": "https://registry.npmmirror.com/code-point-at/-/code-point-at-1.1.0.tgz", 70 | "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==" 71 | }, 72 | "color": { 73 | "version": "4.2.3", 74 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 75 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 76 | "requires": { 77 | "color-convert": "^2.0.1", 78 | "color-string": "^1.9.0" 79 | } 80 | }, 81 | "color-convert": { 82 | "version": "2.0.1", 83 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 84 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 85 | "requires": { 86 | "color-name": "~1.1.4" 87 | } 88 | }, 89 | "color-name": { 90 | "version": "1.1.4", 91 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 92 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" 93 | }, 94 | "color-string": { 95 | "version": "1.9.1", 96 | "resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", 97 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 98 | "requires": { 99 | "color-name": "^1.0.0", 100 | "simple-swizzle": "^0.2.2" 101 | } 102 | }, 103 | "commander": { 104 | "version": "7.2.0", 105 | "resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz", 106 | "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" 107 | }, 108 | "console-control-strings": { 109 | "version": "1.1.0", 110 | "resolved": "https://registry.npmmirror.com/console-control-strings/-/console-control-strings-1.1.0.tgz", 111 | "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" 112 | }, 113 | "core-util-is": { 114 | "version": "1.0.3", 115 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 116 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 117 | }, 118 | "decompress-response": { 119 | "version": "6.0.0", 120 | "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", 121 | "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", 122 | "requires": { 123 | "mimic-response": "^3.1.0" 124 | } 125 | }, 126 | "deep-extend": { 127 | "version": "0.6.0", 128 | "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", 129 | "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==" 130 | }, 131 | "delegates": { 132 | "version": "1.0.0", 133 | "resolved": "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz", 134 | "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" 135 | }, 136 | "detect-libc": { 137 | "version": "2.0.1", 138 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", 139 | "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" 140 | }, 141 | "end-of-stream": { 142 | "version": "1.4.4", 143 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 144 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 145 | "requires": { 146 | "once": "^1.4.0" 147 | } 148 | }, 149 | "expand-template": { 150 | "version": "2.0.3", 151 | "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", 152 | "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==" 153 | }, 154 | "fs-constants": { 155 | "version": "1.0.0", 156 | "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", 157 | "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" 158 | }, 159 | "gauge": { 160 | "version": "2.7.4", 161 | "resolved": "https://registry.npmmirror.com/gauge/-/gauge-2.7.4.tgz", 162 | "integrity": "sha512-14x4kjc6lkD3ltw589k0NrPD6cCNTD6CWoVUNpB85+DrtONoZn+Rug6xZU5RvSC4+TZPxA5AnBibQYAvZn41Hg==", 163 | "requires": { 164 | "aproba": "^1.0.3", 165 | "console-control-strings": "^1.0.0", 166 | "has-unicode": "^2.0.0", 167 | "object-assign": "^4.1.0", 168 | "signal-exit": "^3.0.0", 169 | "string-width": "^1.0.1", 170 | "strip-ansi": "^3.0.1", 171 | "wide-align": "^1.1.0" 172 | } 173 | }, 174 | "github-from-package": { 175 | "version": "0.0.0", 176 | "resolved": "https://registry.npmmirror.com/github-from-package/-/github-from-package-0.0.0.tgz", 177 | "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" 178 | }, 179 | "has-unicode": { 180 | "version": "2.0.1", 181 | "resolved": "https://registry.npmmirror.com/has-unicode/-/has-unicode-2.0.1.tgz", 182 | "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" 183 | }, 184 | "ieee754": { 185 | "version": "1.2.1", 186 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", 187 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" 188 | }, 189 | "inherits": { 190 | "version": "2.0.4", 191 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 192 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 193 | }, 194 | "ini": { 195 | "version": "1.3.8", 196 | "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", 197 | "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" 198 | }, 199 | "is-arrayish": { 200 | "version": "0.3.2", 201 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 202 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" 203 | }, 204 | "is-fullwidth-code-point": { 205 | "version": "1.0.0", 206 | "resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 207 | "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", 208 | "requires": { 209 | "number-is-nan": "^1.0.0" 210 | } 211 | }, 212 | "isarray": { 213 | "version": "1.0.0", 214 | "resolved": "https://registry.npmmirror.com/isarray/-/isarray-1.0.0.tgz", 215 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 216 | }, 217 | "lru-cache": { 218 | "version": "6.0.0", 219 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", 220 | "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", 221 | "requires": { 222 | "yallist": "^4.0.0" 223 | } 224 | }, 225 | "mimic-response": { 226 | "version": "3.1.0", 227 | "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", 228 | "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" 229 | }, 230 | "minimist": { 231 | "version": "1.2.6", 232 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", 233 | "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" 234 | }, 235 | "mkdirp-classic": { 236 | "version": "0.5.3", 237 | "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", 238 | "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" 239 | }, 240 | "napi-build-utils": { 241 | "version": "1.0.2", 242 | "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", 243 | "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" 244 | }, 245 | "node-abi": { 246 | "version": "3.22.0", 247 | "resolved": "https://registry.npmmirror.com/node-abi/-/node-abi-3.22.0.tgz", 248 | "integrity": "sha512-u4uAs/4Zzmp/jjsD9cyFYDXeISfUWaAVWshPmDZOFOv4Xl4SbzTXm53I04C2uRueYJ+0t5PEtLH/owbn2Npf/w==", 249 | "requires": { 250 | "semver": "^7.3.5" 251 | } 252 | }, 253 | "node-addon-api": { 254 | "version": "5.0.0", 255 | "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-5.0.0.tgz", 256 | "integrity": "sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==" 257 | }, 258 | "npmlog": { 259 | "version": "4.1.2", 260 | "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", 261 | "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", 262 | "requires": { 263 | "are-we-there-yet": "~1.1.2", 264 | "console-control-strings": "~1.1.0", 265 | "gauge": "~2.7.3", 266 | "set-blocking": "~2.0.0" 267 | } 268 | }, 269 | "number-is-nan": { 270 | "version": "1.0.1", 271 | "resolved": "https://registry.npmmirror.com/number-is-nan/-/number-is-nan-1.0.1.tgz", 272 | "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==" 273 | }, 274 | "object-assign": { 275 | "version": "4.1.1", 276 | "resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", 277 | "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" 278 | }, 279 | "once": { 280 | "version": "1.4.0", 281 | "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", 282 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 283 | "requires": { 284 | "wrappy": "1" 285 | } 286 | }, 287 | "prebuild-install": { 288 | "version": "7.1.0", 289 | "resolved": "https://registry.npmmirror.com/prebuild-install/-/prebuild-install-7.1.0.tgz", 290 | "integrity": "sha512-CNcMgI1xBypOyGqjp3wOc8AAo1nMhZS3Cwd3iHIxOdAUbb+YxdNuM4Z5iIrZ8RLvOsf3F3bl7b7xGq6DjQoNYA==", 291 | "requires": { 292 | "detect-libc": "^2.0.0", 293 | "expand-template": "^2.0.3", 294 | "github-from-package": "0.0.0", 295 | "minimist": "^1.2.3", 296 | "mkdirp-classic": "^0.5.3", 297 | "napi-build-utils": "^1.0.1", 298 | "node-abi": "^3.3.0", 299 | "npmlog": "^4.0.1", 300 | "pump": "^3.0.0", 301 | "rc": "^1.2.7", 302 | "simple-get": "^4.0.0", 303 | "tar-fs": "^2.0.0", 304 | "tunnel-agent": "^0.6.0" 305 | } 306 | }, 307 | "process-nextick-args": { 308 | "version": "2.0.1", 309 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 310 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 311 | }, 312 | "pump": { 313 | "version": "3.0.0", 314 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 315 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 316 | "requires": { 317 | "end-of-stream": "^1.1.0", 318 | "once": "^1.3.1" 319 | } 320 | }, 321 | "rc": { 322 | "version": "1.2.8", 323 | "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", 324 | "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", 325 | "requires": { 326 | "deep-extend": "^0.6.0", 327 | "ini": "~1.3.0", 328 | "minimist": "^1.2.0", 329 | "strip-json-comments": "~2.0.1" 330 | } 331 | }, 332 | "readable-stream": { 333 | "version": "2.3.7", 334 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", 335 | "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", 336 | "requires": { 337 | "core-util-is": "~1.0.0", 338 | "inherits": "~2.0.3", 339 | "isarray": "~1.0.0", 340 | "process-nextick-args": "~2.0.0", 341 | "safe-buffer": "~5.1.1", 342 | "string_decoder": "~1.1.1", 343 | "util-deprecate": "~1.0.1" 344 | } 345 | }, 346 | "safe-buffer": { 347 | "version": "5.1.2", 348 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 349 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 350 | }, 351 | "semver": { 352 | "version": "7.3.7", 353 | "resolved": "https://registry.npmmirror.com/semver/-/semver-7.3.7.tgz", 354 | "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", 355 | "requires": { 356 | "lru-cache": "^6.0.0" 357 | } 358 | }, 359 | "set-blocking": { 360 | "version": "2.0.0", 361 | "resolved": "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", 362 | "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" 363 | }, 364 | "sharp": { 365 | "version": "0.30.5", 366 | "resolved": "https://registry.npmmirror.com/sharp/-/sharp-0.30.5.tgz", 367 | "integrity": "sha512-0T28KxqY4DzUMLSAp1/IhGVeHpPIQyp1xt7esmuXCAfyi/+6tYMUeRhQok+E/+E52Yk5yFjacXp90cQOkmkl4w==", 368 | "requires": { 369 | "color": "^4.2.3", 370 | "detect-libc": "^2.0.1", 371 | "node-addon-api": "^5.0.0", 372 | "prebuild-install": "^7.1.0", 373 | "semver": "^7.3.7", 374 | "simple-get": "^4.0.1", 375 | "tar-fs": "^2.1.1", 376 | "tunnel-agent": "^0.6.0" 377 | } 378 | }, 379 | "signal-exit": { 380 | "version": "3.0.7", 381 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", 382 | "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" 383 | }, 384 | "simple-concat": { 385 | "version": "1.0.1", 386 | "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", 387 | "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" 388 | }, 389 | "simple-get": { 390 | "version": "4.0.1", 391 | "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", 392 | "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", 393 | "requires": { 394 | "decompress-response": "^6.0.0", 395 | "once": "^1.3.1", 396 | "simple-concat": "^1.0.0" 397 | } 398 | }, 399 | "simple-swizzle": { 400 | "version": "0.2.2", 401 | "resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 402 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 403 | "requires": { 404 | "is-arrayish": "^0.3.1" 405 | } 406 | }, 407 | "string-width": { 408 | "version": "1.0.2", 409 | "resolved": "https://registry.npmmirror.com/string-width/-/string-width-1.0.2.tgz", 410 | "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", 411 | "requires": { 412 | "code-point-at": "^1.0.0", 413 | "is-fullwidth-code-point": "^1.0.0", 414 | "strip-ansi": "^3.0.0" 415 | } 416 | }, 417 | "string_decoder": { 418 | "version": "1.1.1", 419 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 420 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 421 | "requires": { 422 | "safe-buffer": "~5.1.0" 423 | } 424 | }, 425 | "strip-ansi": { 426 | "version": "3.0.1", 427 | "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-3.0.1.tgz", 428 | "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", 429 | "requires": { 430 | "ansi-regex": "^2.0.0" 431 | } 432 | }, 433 | "strip-json-comments": { 434 | "version": "2.0.1", 435 | "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 436 | "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==" 437 | }, 438 | "tar-fs": { 439 | "version": "2.1.1", 440 | "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", 441 | "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", 442 | "requires": { 443 | "chownr": "^1.1.1", 444 | "mkdirp-classic": "^0.5.2", 445 | "pump": "^3.0.0", 446 | "tar-stream": "^2.1.4" 447 | } 448 | }, 449 | "tar-stream": { 450 | "version": "2.2.0", 451 | "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", 452 | "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", 453 | "requires": { 454 | "bl": "^4.0.3", 455 | "end-of-stream": "^1.4.1", 456 | "fs-constants": "^1.0.0", 457 | "inherits": "^2.0.3", 458 | "readable-stream": "^3.1.1" 459 | }, 460 | "dependencies": { 461 | "readable-stream": { 462 | "version": "3.6.0", 463 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 464 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 465 | "requires": { 466 | "inherits": "^2.0.3", 467 | "string_decoder": "^1.1.1", 468 | "util-deprecate": "^1.0.1" 469 | } 470 | } 471 | } 472 | }, 473 | "tunnel-agent": { 474 | "version": "0.6.0", 475 | "resolved": "https://registry.npmmirror.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 476 | "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 477 | "requires": { 478 | "safe-buffer": "^5.0.1" 479 | } 480 | }, 481 | "util-deprecate": { 482 | "version": "1.0.2", 483 | "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", 484 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 485 | }, 486 | "wide-align": { 487 | "version": "1.1.5", 488 | "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", 489 | "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", 490 | "requires": { 491 | "string-width": "^1.0.2 || 2 || 3 || 4" 492 | } 493 | }, 494 | "wrappy": { 495 | "version": "1.0.2", 496 | "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", 497 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" 498 | }, 499 | "yallist": { 500 | "version": "4.0.0", 501 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", 502 | "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" 503 | } 504 | } 505 | } 506 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flutter-assets-maker", 3 | "version": "1.2.5", 4 | "description": "Auto creat flutter assets", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "bin": { 10 | "fmaker": "./index.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/mjl0602/flutter-assets-maker.git" 15 | }, 16 | "keywords": [ 17 | "flutter", 18 | "assets", 19 | "node", 20 | "images" 21 | ], 22 | "author": "mjl", 23 | "license": "ISC", 24 | "bugs": { 25 | "url": "https://github.com/mjl0602/flutter-assets-maker/issues" 26 | }, 27 | "homepage": "https://github.com/mjl0602/flutter-assets-maker#readme", 28 | "dependencies": { 29 | "commander": "^7.2.0", 30 | "sharp": "^0.30.5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tools/file.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const join = require("path").join; 3 | 4 | function file(path) { 5 | // console.log("read file:", path); 6 | return new Promise((r, e) => { 7 | fs.readFile(path, "utf8", async function (err, data) { 8 | if (!err) { 9 | r(data); 10 | } else { 11 | console.error("read file error", err); 12 | e(err); 13 | } 14 | }); 15 | }); 16 | } 17 | 18 | async function copyFile(p1, p2, force) { 19 | if (!force) 20 | if (await exists(p2)) { 21 | console.log(`[INFO]文件 ${p2} 已存在,跳过拷贝`); 22 | return; 23 | } 24 | return new Promise((r, e) => { 25 | fs.copyFile(p1, p2, (error) => { 26 | if (error) { 27 | e(error); 28 | } else r(); 29 | }); 30 | }); 31 | } 32 | 33 | function exists(path) { 34 | return new Promise((r, e) => { 35 | fs.exists(path, function (exists) { 36 | if (exists) { 37 | r(true); 38 | } else { 39 | r(false); 40 | } 41 | }); 42 | }); 43 | } 44 | 45 | // 查找目录下文件 46 | function find(startPath) { 47 | let result = []; 48 | fs.mkdirSync(startPath, { 49 | recursive: true, 50 | }); 51 | function finder(path) { 52 | let files = fs.readdirSync(path); 53 | files.forEach((val, index) => { 54 | let fPath = join(path, val); 55 | let stats = fs.statSync(fPath); 56 | if (stats.isDirectory()) result.push(fPath); 57 | if (stats.isFile()) result.push(fPath); 58 | }); 59 | } 60 | finder(startPath); 61 | return result; 62 | } 63 | 64 | // 递归查找所有文件 65 | function findAll(startPath) { 66 | let result = []; 67 | function finder(path) { 68 | let files = fs.readdirSync(path); 69 | files.forEach((val, index) => { 70 | let fPath = join(path, val); 71 | let stats = fs.statSync(fPath); 72 | if (stats.isDirectory()) finder(fPath); 73 | if (stats.isFile()) result.push(fPath); 74 | }); 75 | } 76 | finder(startPath); 77 | return result; 78 | } 79 | 80 | function savefile(path, content) { 81 | console.log("保存文件", path); 82 | var targetPath = path.split("/"); 83 | targetPath.pop(); 84 | targetPath = targetPath.join("/"); 85 | fs.mkdirSync(targetPath, { 86 | recursive: true, 87 | }); 88 | return new Promise((r, e) => { 89 | fs.writeFile(path, content, {}, async function (err) { 90 | if (!err) { 91 | r(); 92 | } else { 93 | console.error("save file error", err); 94 | e(err); 95 | } 96 | }); 97 | }); 98 | } 99 | 100 | function mkdir(path) { 101 | return new Promise((r, e) => { 102 | fs.mkdir(path, async function (err) { 103 | r(); 104 | }); 105 | }); 106 | } 107 | 108 | function resolve(dir) { 109 | return join(__dirname, dir); 110 | } 111 | 112 | module.exports = { 113 | find, 114 | file, 115 | savefile, 116 | mkdir, 117 | resolve, 118 | exists, 119 | copyFile, 120 | }; 121 | -------------------------------------------------------------------------------- /tools/image.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | let path = require("path"); 3 | module.exports = { 4 | resizeAndSave, 5 | deltaOf, 6 | }; 7 | 8 | function resizeAndSave(image, size, fileName) { 9 | // console.log("resizeAndSave", fileName); 10 | var targetPath = fileName.split("/"); 11 | targetPath.pop(); 12 | targetPath = targetPath.join("/"); 13 | fs.mkdirSync(targetPath, { 14 | recursive: true, 15 | }); 16 | return new Promise((r, e) => { 17 | image.resize(size).toFile(fileName, (err, info) => { 18 | err ? e(err) : r(info); 19 | }); 20 | }); 21 | } 22 | 23 | // 从文件名获取倍率 24 | function deltaOf(name) { 25 | let result = name.match(/@(\S*)[Xx]/) || []; 26 | if (result.length <= 1) { 27 | return 0; 28 | } 29 | result = parseInt(result[1]); 30 | return result || 0; 31 | } 32 | --------------------------------------------------------------------------------