├── .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):  \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 | `/// \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 |
--------------------------------------------------------------------------------