├── .appcast.xml ├── .gitignore ├── LICENSE ├── README.md ├── README_zh.md ├── android_res_export.png ├── assets ├── grid.png ├── grid_android_o.png ├── icon.png ├── manifest_en.json └── manifest_zh.json ├── package-lock.json ├── package.json ├── resources ├── export_assets.html ├── export_assets.js ├── export_vector_assets.html ├── export_vector_assets.js ├── icon.png ├── preferences.html ├── preferences.js ├── style.css ├── view_code.html ├── view_code.js ├── view_nine_patch.html ├── view_nine_patch.js ├── view_vector_drawable_code.html └── view_vector_drawable_code.js ├── src ├── export_app_icon.js ├── export_bitmap_assets.js ├── export_nine_patch_assets.js ├── export_vector_assets.js ├── fill_type_to_non_zero.js ├── help.js ├── language.js ├── languages.json ├── lib │ ├── android.js │ ├── fs.js │ ├── i18n.js │ └── sk.js ├── manifest.json ├── new_app_icon.js ├── new_asset.js ├── new_nine_patch_asset.js ├── preferences.js ├── view_color_code_from_color_variables.js ├── view_color_code_from_selected_layers.js ├── view_nine_patch.js ├── view_shape_code.js └── view_vector_drawable_code.js └── webpack.skpm.config.js /.appcast.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build artefacts 2 | vector-drawable.sketchplugin 3 | 4 | # npm 5 | node_modules 6 | .npm 7 | npm-debug.log 8 | 9 | # mac 10 | .DS_Store 11 | 12 | # WebStorm 13 | .idea 14 | 15 | # sketch 16 | # sketch-assets 17 | plugin 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Ashung Hung (ashung.hung@gmail.com) 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 | ![](android_res_export.png) 2 | 3 | # Android Res Export for Sketch 4 | 5 | **[中文说明](https://github.com/Ashung/Android_Res_Export/blob/master/README_zh.md)** 6 | 7 | Export Android resources in Sketch app. 8 | 9 | ## Features 10 | 11 | - Export PNG or WebP assets into multiple sizes. 12 | - Preview and export nine-patch assets into multiple sizes. 13 | - View and export vector drawable asset. 14 | - App icon template. 15 | - Export app launcher icon for Android 8 and older version. 16 | - View and export shape drawable XML code from selected layer. 17 | - View and export color resource XML code from selected layers or color variables. 18 | 19 | ## Installation 20 | 21 | - Search "Android Res Export" from [Sketch Runner](http://sketchrunner.com/). 22 | - [Download](https://github.com/Ashung/Android_Res_Export/releases/latest/download/android_res_export.sketchplugin.zip) zip file, unzip and double click on the ".sketchplugin" file. 23 | 24 | ## How it Works 25 | 26 | Your Sketch file must design at MDPI (1x) size. 27 | 28 | Selected 1 layer and run "New ... Asset" to create new asset, then run "Export ... Assets" to export them. You can export selected assets, or export all assets while deselect any layer. 29 | 30 | With out close the "... From Selected Layer" panel, you can select other layer to view the code realtime. 31 | 32 | In "Preferences" panel, you can choose which size when export bitmap assets, webp image quality. 33 | 34 | ## License 35 | 36 | MIT 37 | 38 | ## Donate 39 | 40 | [Buy me a coffee](https://www.buymeacoffee.com/ashung) or donate [$5.00](https://www.paypal.me/ashung/5) [$10.00](https://www.paypal.me/ashung/10) via PayPal. 41 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | ![](android_res_export.png) 2 | 3 | # Android Res Export for Sketch 4 | 5 | Android Res Export 是 Sketch 上的 Android 资源导出插件。 6 | 7 | ⚠️ 注意:4.x 版重写了所有功能,可能不支持 Sketch 72 以下的版本 。 8 | 9 | 新增功能如下:自适应 Sketch 界面语言;导出所有资源时将出现选择窗口;导出矢量资源功能不再需要安装任何依赖;图层的色彩、形状、矢量代码都可以实时查看。 10 | 11 | ## 功能 12 | 13 | - 导出多分辨率 PNG 或 WebP 格式位图资源; 14 | - 预览和导出多分辨率点九资源; 15 | - 查看和导出矢量资源(Vector Drawable); 16 | - App 图标模版; 17 | - 导出 App 图标,支持 Android 8.0 自适应图标、圆形图标及旧版图标; 18 | - 查看和导出选中形状图层的 XML 代码; 19 | - 查看和导出图层的选中图层或色彩变量的色彩资源代码; 20 | 21 | ## 安装 22 | 23 | - 推荐在 [Sketch Runner](http://sketchrunner.com/) 搜索 “Android Res Export”。 24 | - 或[下载](https://github.com/Ashung/Android_Res_Export/releases/latest/download/android_res_export.sketchplugin.zip),解压后,双击 ".sketchplugin" 文件,安装插件。 25 | 26 | ## 插件使用 27 | 28 | 设计稿必须是 MDPI (1x) 尺寸。 29 | 30 | 资源需要先选中一个图层后使用 “新建…” 菜单创建相应资源,然后使用 “导出…” 菜单导出;可以只导出选中的资源,未选中任何图层时会出现所有资源的选择窗口。默认使用合法的资源命名方式,新建资源前后可以修改图层的名称,切片图层的名称将作为资源的文件名。 31 | 32 | 在不关闭 “选中图层的…资源” 窗口时,可以选择不同图层查看相应的代码。 33 | 34 | 在 “参数设置” 窗口内,可以设置位图资源导出的尺寸,和 WebP 图像质量等等。 35 | 36 | ## 版权声明 37 | 38 | MIT 39 | 40 | ## 捐助 41 | 42 | 使用 [微信](http://ashung.github.io/donate.html) 或 [支付宝](http://ashung.github.io/donate.html) 捐助作者。 43 | 44 | ![](https://github.com/Ashung/ashung.github.io/blob/master/assets/img/donate_alipay_rmb_10.png?raw=true) 45 | 46 | ![](https://github.com/Ashung/ashung.github.io/blob/master/assets/img/donate_wechat_rmb_10.png?raw=true) -------------------------------------------------------------------------------- /android_res_export.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ashung/Android_Res_Export/f21788ca05b93228aecd8a7fbc6c1acf1245e5cb/android_res_export.png -------------------------------------------------------------------------------- /assets/grid.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ashung/Android_Res_Export/f21788ca05b93228aecd8a7fbc6c1acf1245e5cb/assets/grid.png -------------------------------------------------------------------------------- /assets/grid_android_o.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ashung/Android_Res_Export/f21788ca05b93228aecd8a7fbc6c1acf1245e5cb/assets/grid_android_o.png -------------------------------------------------------------------------------- /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ashung/Android_Res_Export/f21788ca05b93228aecd8a7fbc6c1acf1245e5cb/assets/icon.png -------------------------------------------------------------------------------- /assets/manifest_en.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": [ 3 | { 4 | "name": "New Bitmap Asset", 5 | "identifier": "new_bitmap_asset", 6 | "script": "new_asset.js" 7 | }, 8 | { 9 | "name": "New Vector Asset", 10 | "identifier": "new_vector_asset", 11 | "script": "new_asset.js" 12 | }, 13 | { 14 | "name": "New Nine-Patch Asset", 15 | "identifier": "new_nine_patch_asset", 16 | "script": "new_nine_patch_asset.js" 17 | }, 18 | { 19 | "name": "New App Icon", 20 | "identifier": "new_app_icon", 21 | "script": "new_app_icon.js" 22 | }, 23 | { 24 | "name": "Export Bitmap Assets (PNG)", 25 | "identifier": "export_bitmap_assets_png", 26 | "script": "export_bitmap_assets.js" 27 | }, 28 | { 29 | "name": "Export Bitmap Assets (WebP)", 30 | "identifier": "export_bitmap_assets_webp", 31 | "script": "export_bitmap_assets.js" 32 | }, 33 | { 34 | "name": "Export Nine-Patch Assets", 35 | "identifier": "export_nine_patch_assets", 36 | "script": "export_nine_patch_assets.js" 37 | }, 38 | { 39 | "name": "Export Vector Assets", 40 | "identifier": "export_vector_assets", 41 | "script": "export_vector_assets.js" 42 | }, 43 | { 44 | "name": "Export App Icon", 45 | "identifier": "export_app_icon", 46 | "script": "export_app_icon.js" 47 | }, 48 | { 49 | "name": "Color XML From Selected Layers", 50 | "identifier": "view_color_code_from_selected_layers", 51 | "script": "view_color_code_from_selected_layers.js", 52 | "handlers": { 53 | "run": "onRun", 54 | "actions": { 55 | "Shutdown": "onShutdown", 56 | "SelectionChanged.finish": "onSelectionChanged" 57 | } 58 | } 59 | }, 60 | { 61 | "name": "Color XML From Color Variables", 62 | "identifier": "view_color_code_from_color_variables", 63 | "script": "view_color_code_from_color_variables.js" 64 | }, 65 | { 66 | "name": "Shape Drawable From Selected Layer", 67 | "identifier": "view_shape_drawable_from_selected_layer", 68 | "script": "view_shape_code.js", 69 | "handlers": { 70 | "run": "onRun", 71 | "actions": { 72 | "Shutdown": "onShutdown", 73 | "SelectionChanged.finish": "onSelectionChanged" 74 | } 75 | } 76 | }, 77 | { 78 | "name": "Nine-Patch Preview", 79 | "identifier": "view_nine_patch", 80 | "script": "view_nine_patch.js" 81 | }, 82 | { 83 | "name": "Vector Drawable From Selected Layer", 84 | "identifier": "view_vector_drawable_code", 85 | "script": "view_vector_drawable_code.js", 86 | "handlers": { 87 | "run": "onRun", 88 | "actions": { 89 | "Shutdown": "onShutdown", 90 | "SelectionChanged.finish": "onSelectionChanged" 91 | } 92 | } 93 | }, 94 | { 95 | "name": "Change Fill Type to Non-Zero", 96 | "identifier": "fill_type_to_non_zero", 97 | "script": "fill_type_to_non_zero.js" 98 | }, 99 | { 100 | "name": "Preferences", 101 | "identifier": "preferences", 102 | "script": "preferences.js" 103 | }, 104 | { 105 | "name": "Web Site", 106 | "identifier": "web_site", 107 | "script": "help.js" 108 | }, 109 | { 110 | "name": "Report Issues", 111 | "identifier": "report_issues", 112 | "script": "help.js" 113 | }, 114 | { 115 | "name": "Donate", 116 | "identifier": "donate", 117 | "script": "help.js" 118 | }, 119 | { 120 | "name": "Buy Me a Coffee", 121 | "identifier": "buymeacoffee", 122 | "script": "help.js" 123 | }, 124 | { 125 | "handlers": { 126 | "actions": { 127 | "OpenDocument": "onOpenDocument" 128 | } 129 | }, 130 | "script": "languages.js" 131 | } 132 | ], 133 | "menu": { 134 | "title": "Android Res Export", 135 | "items": [ 136 | "new_bitmap_asset", 137 | "new_vector_asset", 138 | "new_nine_patch_asset", 139 | "new_app_icon", 140 | "-", 141 | "export_bitmap_assets_png", 142 | "export_bitmap_assets_webp", 143 | "export_vector_assets", 144 | "export_nine_patch_assets", 145 | "export_app_icon", 146 | "-", 147 | "view_nine_patch", 148 | "view_color_code_from_color_variables", 149 | "view_color_code_from_selected_layers", 150 | "view_shape_drawable_from_selected_layer", 151 | "view_vector_drawable_code", 152 | "-", 153 | "fill_type_to_non_zero", 154 | "-", 155 | "preferences", 156 | { 157 | "title": "Help", 158 | "items": [ 159 | "web_site", 160 | "report_issues", 161 | "-", 162 | "donate", 163 | "buymeacoffee" 164 | ] 165 | } 166 | ] 167 | }, 168 | "name": "Android Res Export", 169 | "description": "Export Android resources in Sketch.", 170 | "homepage": "https://github.com/Ashung/Android_Res_Export", 171 | "icon": "icon.png", 172 | "identifier": "com.ashung.hung.android_res_export", 173 | "compatibleVersion": 3, 174 | "bundleVersion": 1, 175 | "version": "4.1.0", 176 | "disableCocoaScriptPreprocessor": true, 177 | "appcast": "https://raw.githubusercontent.com//master/.appcast.xml", 178 | "author": "Ashung Hung", 179 | "authorEmail": "ashung.hung@foxmail.com" 180 | } -------------------------------------------------------------------------------- /assets/manifest_zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": [ 3 | { 4 | "name": "新建位图资源", 5 | "identifier": "new_bitmap_asset", 6 | "script": "new_asset.js" 7 | }, 8 | { 9 | "name": "新建矢量资源", 10 | "identifier": "new_vector_asset", 11 | "script": "new_asset.js" 12 | }, 13 | { 14 | "name": "新建点九资源", 15 | "identifier": "new_nine_patch_asset", 16 | "script": "new_nine_patch_asset.js" 17 | }, 18 | { 19 | "name": "新建应用图标", 20 | "identifier": "new_app_icon", 21 | "script": "new_app_icon.js" 22 | }, 23 | { 24 | "name": "导出位图资源 (PNG)", 25 | "identifier": "export_bitmap_assets_png", 26 | "script": "export_bitmap_assets.js" 27 | }, 28 | { 29 | "name": "导出位图资源 (WebP)", 30 | "identifier": "export_bitmap_assets_webp", 31 | "script": "export_bitmap_assets.js" 32 | }, 33 | { 34 | "name": "导出点九资源", 35 | "identifier": "export_nine_patch_assets", 36 | "script": "export_nine_patch_assets.js" 37 | }, 38 | { 39 | "name": "导出点九资源", 40 | "identifier": "export_vector_assets", 41 | "script": "export_vector_assets.js" 42 | }, 43 | { 44 | "name": "导出应用图标", 45 | "identifier": "export_app_icon", 46 | "script": "export_app_icon.js" 47 | }, 48 | { 49 | "name": "选中图层的色彩资源", 50 | "identifier": "view_color_code_from_selected_layers", 51 | "script": "view_color_code_from_selected_layers.js", 52 | "handlers": { 53 | "run": "onRun", 54 | "actions": { 55 | "Shutdown": "onShutdown", 56 | "SelectionChanged.finish": "onSelectionChanged" 57 | } 58 | } 59 | }, 60 | { 61 | "name": "文档的色彩资源", 62 | "identifier": "view_color_code_from_color_variables", 63 | "script": "view_color_code_from_color_variables.js" 64 | }, 65 | { 66 | "name": "选中图层的形状资源", 67 | "identifier": "view_shape_drawable_from_selected_layer", 68 | "script": "view_shape_code.js", 69 | "handlers": { 70 | "run": "onRun", 71 | "actions": { 72 | "Shutdown": "onShutdown", 73 | "SelectionChanged.finish": "onSelectionChanged" 74 | } 75 | } 76 | }, 77 | { 78 | "name": "点九资源预览", 79 | "identifier": "view_nine_patch", 80 | "script": "view_nine_patch.js" 81 | }, 82 | { 83 | "name": "选中图层的矢量资源", 84 | "identifier": "view_vector_drawable_code", 85 | "script": "view_vector_drawable_code.js", 86 | "handlers": { 87 | "run": "onRun", 88 | "actions": { 89 | "Shutdown": "onShutdown", 90 | "SelectionChanged.finish": "onSelectionChanged" 91 | } 92 | } 93 | }, 94 | { 95 | "name": "填充类型转为非零", 96 | "identifier": "fill_type_to_non_zero", 97 | "script": "fill_type_to_non_zero.js" 98 | }, 99 | { 100 | "name": "参数设置", 101 | "identifier": "preferences", 102 | "script": "preferences.js" 103 | }, 104 | { 105 | "name": "网站", 106 | "identifier": "web_site", 107 | "script": "help.js" 108 | }, 109 | { 110 | "name": "问题反馈", 111 | "identifier": "report_issues", 112 | "script": "help.js" 113 | }, 114 | { 115 | "name": "捐赠", 116 | "identifier": "donate", 117 | "script": "help.js" 118 | }, 119 | { 120 | "name": "微信打赏", 121 | "identifier": "donate_wechat", 122 | "script": "help.js" 123 | }, 124 | { 125 | "name": "支付宝打赏", 126 | "identifier": "donate_alipay", 127 | "script": "help.js" 128 | }, 129 | { 130 | "handlers": { 131 | "actions": { 132 | "OpenDocument": "onOpenDocument" 133 | } 134 | }, 135 | "script": "languages.js" 136 | } 137 | ], 138 | "menu": { 139 | "title": "Android 资源导出", 140 | "items": [ 141 | "new_bitmap_asset", 142 | "new_vector_asset", 143 | "new_nine_patch_asset", 144 | "new_app_icon", 145 | "-", 146 | "export_bitmap_assets_png", 147 | "export_bitmap_assets_webp", 148 | "export_vector_assets", 149 | "export_nine_patch_assets", 150 | "export_app_icon", 151 | "-", 152 | "view_nine_patch", 153 | "view_color_code_from_color_variables", 154 | "view_color_code_from_selected_layers", 155 | "view_shape_drawable_from_selected_layer", 156 | "view_vector_drawable_code", 157 | "-", 158 | "fill_type_to_non_zero", 159 | "-", 160 | "preferences", 161 | { 162 | "title": "帮助", 163 | "items": [ 164 | "web_site", 165 | "report_issues", 166 | "-", 167 | "donate", 168 | "donate_wechat", 169 | "donate_alipay" 170 | ] 171 | } 172 | ] 173 | }, 174 | "name": "Android Res Export", 175 | "description": "用于导出 Android 各种资源的 Sketch 插件", 176 | "homepage": "https://github.com/Ashung/Android_Res_Export", 177 | "icon": "icon.png", 178 | "identifier": "com.ashung.hung.android_res_export", 179 | "compatibleVersion": 3, 180 | "bundleVersion": 1, 181 | "version": "4.1.0", 182 | "disableCocoaScriptPreprocessor": true, 183 | "appcast": "https://raw.githubusercontent.com//master/.appcast.xml", 184 | "author": "Ashung Hung", 185 | "authorEmail": "ashung.hung@foxmail.com" 186 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "android_res_export", 3 | "productName": "Android Res Export", 4 | "version": "4.1.1", 5 | "engines": { 6 | "sketch": ">=3.0" 7 | }, 8 | "skpm": { 9 | "name": "android_res_export", 10 | "manifest": "src/manifest.json", 11 | "main": "./plugin/android_res_export.sketchplugin", 12 | "assets": [ 13 | "assets/**/*" 14 | ] 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/Ashung/Android_Res_Export.git" 19 | }, 20 | "scripts": { 21 | "build": "skpm-build", 22 | "watch": "skpm-build --watch", 23 | "start": "skpm-build --watch", 24 | "postinstall": "npm run build && skpm-link" 25 | }, 26 | "devDependencies": { 27 | "@skpm/builder": "^0.8.0", 28 | "@skpm/extract-loader": "^2.0.2", 29 | "css-loader": "^3.2.0", 30 | "html-loader": "^0.5.5" 31 | }, 32 | "resources": [ 33 | "resources/**/*.js" 34 | ], 35 | "dependencies": { 36 | "highlight.js": "^11.5.1", 37 | "sketch-module-web-view": "^3.5.1", 38 | "svg2vectordrawable": "^2.9.1" 39 | }, 40 | "author": "Ashung Hung " 41 | } 42 | -------------------------------------------------------------------------------- /resources/export_assets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Android Res Export 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/export_assets.js: -------------------------------------------------------------------------------- 1 | const main = document.getElementById('main'); 2 | const preview = document.getElementById('preview'); 3 | const selectButton = document.getElementById('select'); 4 | const exportButton = document.getElementById('export'); 5 | const cancelButton = document.getElementById('cancel'); 6 | let allCount; 7 | let selectedCount = 0; 8 | 9 | // disable the context menu (eg. the right click menu) to have a more native feel 10 | document.addEventListener('contextmenu', (e) => { 11 | e.preventDefault(); 12 | }); 13 | 14 | // call the plugin from the webview 15 | exportButton.addEventListener('click', () => { 16 | const assetIds = []; 17 | const checkboxs = document.querySelectorAll('input[type="checkbox"]'); 18 | checkboxs.forEach(checkbox => { 19 | if (checkbox.checked) { 20 | assetIds.push(checkbox.value); 21 | } 22 | }); 23 | window.postMessage('export', assetIds); 24 | }); 25 | 26 | cancelButton.addEventListener('click', () => { 27 | window.postMessage('cancel'); 28 | }); 29 | 30 | selectButton.addEventListener('click', () => { 31 | const checkboxs = document.querySelectorAll('input[type="checkbox"]'); 32 | if (selectedCount !== allCount) { 33 | checkboxs.forEach(checkbox => checkbox.checked = true); 34 | selectedCount = allCount; 35 | } else { 36 | checkboxs.forEach(checkbox => checkbox.checked = false); 37 | selectedCount = 0; 38 | } 39 | }); 40 | 41 | main.style.opacity = '0'; 42 | 43 | window.main = (assetsJson, langsJson) => { 44 | const assets = JSON.parse(assetsJson); 45 | allCount = assets.length; 46 | selectedCount = assets.length; 47 | assets.forEach(asset => { 48 | const item = document.createElement('label'); 49 | item.className = 'item'; 50 | const img = document.createElement('img'); 51 | img.srcset = `data:image/png;base64,${asset.data}, data:image/png;base64,${asset.data} 2x`; 52 | const name = document.createElement('div'); 53 | name.className = 'name'; 54 | const text = document.createElement('span'); 55 | text.textContent = asset.name; 56 | const checkbox = document.createElement('input'); 57 | checkbox.type = 'checkbox'; 58 | checkbox.value = asset.id; 59 | checkbox.checked = true; 60 | name.appendChild(text); 61 | item.appendChild(checkbox); 62 | item.appendChild(img); 63 | item.appendChild(name); 64 | preview.appendChild(item); 65 | checkbox.addEventListener('input', (event) => { 66 | if (event.target.checked === false) { 67 | selectedCount --; 68 | } else { 69 | selectedCount ++; 70 | } 71 | }) 72 | }); 73 | 74 | // i18n 75 | const langs = JSON.parse(langsJson); 76 | selectButton.textContent = langs.select_all; 77 | exportButton.textContent = langs.export; 78 | cancelButton.textContent = langs.cancel; 79 | 80 | main.style.opacity = '1'; 81 | } 82 | -------------------------------------------------------------------------------- /resources/export_vector_assets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Android Res Export 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /resources/export_vector_assets.js: -------------------------------------------------------------------------------- 1 | const svg2vectordrawable = require('svg2vectordrawable/src/main.browser'); 2 | 3 | const main = document.getElementById('main'); 4 | const preview = document.getElementById('preview'); 5 | const selectButton = document.getElementById('select'); 6 | const exportButton = document.getElementById('export'); 7 | const cancelButton = document.getElementById('cancel'); 8 | let allCount; 9 | let selectedCount = 0; 10 | 11 | // disable the context menu (eg. the right click menu) to have a more native feel 12 | document.addEventListener('contextmenu', (e) => { 13 | e.preventDefault(); 14 | }); 15 | 16 | // call the plugin from the webview 17 | exportButton.addEventListener('click', async () => { 18 | const assets = []; 19 | const checkboxs = document.querySelectorAll('input[type="checkbox"]'); 20 | for (let i = 0; i < checkboxs.length; i++) { 21 | if (checkboxs[i].checked) { 22 | const svg = checkboxs[i].parentNode.querySelector('input[type="hidden"]').value; 23 | const name = checkboxs[i].parentNode.querySelector('.name').textContent; 24 | const xml = await svg2vectordrawable(svg); 25 | assets.push({ name, xml }); 26 | } 27 | } 28 | window.postMessage('export', assets); 29 | }); 30 | 31 | cancelButton.addEventListener('click', () => { 32 | window.postMessage('cancel'); 33 | }); 34 | 35 | selectButton.addEventListener('click', () => { 36 | const checkboxs = document.querySelectorAll('input[type="checkbox"]'); 37 | if (selectedCount !== allCount) { 38 | checkboxs.forEach(checkbox => checkbox.checked = true); 39 | selectedCount = allCount; 40 | } else { 41 | checkboxs.forEach(checkbox => checkbox.checked = false); 42 | selectedCount = 0; 43 | } 44 | }); 45 | 46 | main.style.opacity = '0'; 47 | 48 | window.main = (assetsJson, langsJson) => { 49 | const assets = JSON.parse(assetsJson); 50 | allCount = assets.length; 51 | selectedCount = assets.length; 52 | assets.forEach(asset => { 53 | const item = document.createElement('label'); 54 | item.className = 'item'; 55 | const img = document.createElement('img'); 56 | img.srcset = `data:image/png;base64,${asset.data}, data:image/png;base64,${asset.data} 2x`; 57 | const name = document.createElement('div'); 58 | name.className = 'name'; 59 | const text = document.createElement('span'); 60 | text.textContent = asset.name; 61 | const checkbox = document.createElement('input'); 62 | checkbox.type = 'checkbox'; 63 | checkbox.checked = true; 64 | const hideSvg = document.createElement('input'); 65 | hideSvg.type = 'hidden'; 66 | hideSvg.value = decodeURIComponent(asset.svg); 67 | name.appendChild(text); 68 | item.appendChild(checkbox); 69 | item.appendChild(img); 70 | item.appendChild(name); 71 | item.appendChild(hideSvg); 72 | preview.appendChild(item); 73 | checkbox.addEventListener('input', (event) => { 74 | if (event.target.checked === false) { 75 | selectedCount --; 76 | } else { 77 | selectedCount ++; 78 | } 79 | }) 80 | }); 81 | 82 | // i18n 83 | const langs = JSON.parse(langsJson); 84 | selectButton.textContent = langs.select_all; 85 | exportButton.textContent = langs.export; 86 | cancelButton.textContent = langs.cancel; 87 | 88 | main.style.opacity = '1'; 89 | } 90 | 91 | window.exportSelection = async (assetsJson) => { 92 | const assets = JSON.parse(assetsJson); 93 | const result = []; 94 | for (let i = 0; i < assets.length; i++) { 95 | const name = assets[i].name; 96 | const svg = decodeURIComponent(assets[i].svg); 97 | const xml = await svg2vectordrawable(svg); 98 | result.push({ name, xml }); 99 | } 100 | window.postMessage('export', result); 101 | } -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Ashung/Android_Res_Export/f21788ca05b93228aecd8a7fbc6c1acf1245e5cb/resources/icon.png -------------------------------------------------------------------------------- /resources/preferences.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Android Res Export 6 | 7 | 8 | 9 |
10 |
11 | 15 |
16 |
17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 |
25 |
26 |
27 | 28 |
29 | 30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 |
38 |
39 | 40 |
41 | 42 | 43 |
44 |
45 |
46 | 47 |
48 | 49 |
50 |
51 |
52 |
53 |
54 | 55 | 56 | 57 | 58 |
59 |
60 | 61 | 62 | -------------------------------------------------------------------------------- /resources/preferences.js: -------------------------------------------------------------------------------- 1 | const main = document.getElementById('main'); 2 | const okButton = document.getElementById('ok'); 3 | const cancelButton = document.getElementById('cancel'); 4 | const nameTypeNode = document.getElementById('name-type'); 5 | const vectorDrawableFolderNode = document.getElementById('folder'); 6 | const webpQualityRangeNode = document.getElementById('webp-quality-range'); 7 | const webpQualityValueNode = document.getElementById('webp-quality-value'); 8 | const revealInFinderNode = document.getElementById('reveal-in-finder'); 9 | const exportDpiNodes = document.querySelectorAll('#export-dpis input'); 10 | 11 | // disable the context menu (eg. the right click menu) to have a more native feel 12 | document.addEventListener('contextmenu', (e) => { 13 | e.preventDefault(); 14 | }); 15 | 16 | // call the plugin from the webview 17 | okButton.addEventListener('click', () => { 18 | const preferences = { 19 | 'export_dpi': [], 20 | 'asset_name_type': nameTypeNode.selectedIndex, 21 | 'vector_drawable_folder': vectorDrawableFolderNode.selectedIndex, 22 | 'reveal_in_finder_after_export': revealInFinderNode.checked, 23 | 'webp_quality': webpQualityRangeNode.value / 100 24 | }; 25 | exportDpiNodes.forEach(input => { 26 | if (input.checked) { 27 | preferences.export_dpi.push(input.value); 28 | } 29 | }); 30 | window.postMessage('save', `${JSON.stringify(preferences)}`); 31 | }); 32 | 33 | cancelButton.addEventListener('click', () => { 34 | window.postMessage('cancel'); 35 | }); 36 | 37 | main.style.opacity = '0'; 38 | 39 | exportDpiNodes.forEach(input => { 40 | input.addEventListener('click', event => { 41 | let count = Array.from(exportDpiNodes).filter(node => node.checked).length; 42 | if (count === 0) { 43 | event.target.checked = true; 44 | } 45 | }); 46 | }); 47 | 48 | window.main = (json) => { 49 | const preferences = JSON.parse(json); 50 | 51 | // Init 52 | preferences.export_dpi.forEach(dpi => { 53 | let node = document.querySelector(`input[value="${dpi}"]`); 54 | node.checked = true; 55 | }); 56 | 57 | preferences.available_asset_name_type.forEach((type, idx) => { 58 | let option = document.createElement('option'); 59 | option.textContent = type; 60 | if (preferences.asset_name_type === idx) { 61 | option.selected = true; 62 | } 63 | nameTypeNode.appendChild(option); 64 | }); 65 | 66 | preferences.available_folders.forEach((folder, idx) => { 67 | let option = document.createElement('option'); 68 | option.textContent = folder; 69 | if (preferences.vector_drawable_folder === idx) { 70 | option.selected = true; 71 | } 72 | vectorDrawableFolderNode.appendChild(option); 73 | }); 74 | 75 | const quality = Math.round(preferences.webp_quality * 100); 76 | webpQualityRangeNode.setAttribute('value', quality); 77 | webpQualityValueNode.setAttribute('value', quality); 78 | webpQualityRangeNode.addEventListener('input', () => { 79 | webpQualityValueNode.setAttribute('value', webpQualityRangeNode.value); 80 | }); 81 | 82 | revealInFinderNode.checked = preferences.reveal_in_finder_after_export; 83 | 84 | // i18n 85 | document.getElementById('version').textContent = preferences.version; 86 | document.getElementById('label-export-dpi').textContent = preferences.i18n.export_dpis; 87 | document.getElementById('label-asset-name-type').textContent = preferences.i18n.asset_name_type; 88 | document.getElementById('label-vector-drawable-folder').textContent = preferences.i18n.vector_drawable_folder; 89 | document.getElementById('label-webp-quality').textContent = preferences.i18n.webp_quality; 90 | document.getElementById('label-others').textContent = preferences.i18n.others; 91 | document.getElementById('label-reveal-in-finder').textContent = preferences.i18n.reveal_in_finder_after_export; 92 | okButton.textContent = preferences.i18n.ok; 93 | cancelButton.textContent = preferences.i18n.cancel; 94 | 95 | main.style.opacity = '1'; 96 | } 97 | -------------------------------------------------------------------------------- /resources/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --accent: #3F51B5; 3 | --text-1: #000; 4 | --text-2: #666; 5 | --text-3: #999; 6 | --border: #DDD; 7 | --background: #F8F8F8; 8 | --preview-background: #FFF; 9 | --input-range-line: #B9B9B9; 10 | --input-background: #FFF; 11 | --input-border: rgba(0, 0, 0, .1); 12 | --input-background-disabled: rgba(0, 0, 0, .1); 13 | --checkbox-background: rgba(0, 0, 0, .15); 14 | --select-background: rgba(0, 0, 0, .15); 15 | --switch-background: #B9B9B9; 16 | --switch-thumb: #FFF; 17 | } 18 | 19 | @media (prefers-color-scheme: dark) { 20 | :root { 21 | --text-1: #FFF; 22 | --text-2: #999; 23 | --text-3: #666; 24 | --border: #333; 25 | --background: #212121; 26 | --preview-background: #121212; 27 | --input-range-line: #454545; 28 | --input-background: #121212; 29 | --input-border: rgba(255, 255, 255, .1); 30 | --input-background-disabled: rgba(255, 255, 255, .1); 31 | --checkbox-background: rgba(255, 255, 255, .15); 32 | --select-background: rgba(255, 255, 255, .15); 33 | --switch-background: #454545; 34 | --switch-thumb: #FFF; 35 | } 36 | } 37 | 38 | html { 39 | box-sizing: border-box; 40 | background: transparent; 41 | overflow: hidden; 42 | cursor: default; 43 | } 44 | *, *:before, *:after { 45 | box-sizing: inherit; 46 | margin: 0; 47 | padding: 0; 48 | position: relative; 49 | -webkit-user-select: none; 50 | user-select: none; 51 | } 52 | input, textarea, code, pre code * { 53 | -webkit-user-select: auto; 54 | user-select: auto; 55 | } 56 | 57 | html, body { 58 | height: 100%; 59 | color: var(--text-1); 60 | background: var(--background); 61 | } 62 | body { 63 | font: medium Roboto, Helvetica, Arial, sans-serif; 64 | } 65 | pre code { 66 | display: block; 67 | font: 12px/1.6 Monaco, Consolas, monospace; 68 | padding: 16px; 69 | } 70 | 71 | pre *::selection { 72 | background-color: rgba(60, 80, 180, .2); 73 | } 74 | 75 | button { 76 | display: inline-block; 77 | font: inherit; 78 | font-size: 14px; 79 | font-weight: 500; 80 | text-transform: uppercase; 81 | border: 0; 82 | border-radius: 4px; 83 | height: 36px; 84 | line-height: 36px; 85 | margin: 0; 86 | padding: 0 16px; 87 | vertical-align: middle; 88 | outline: none; 89 | cursor: pointer; 90 | color: var(--text-1); 91 | background: none; 92 | transition: background-color .2s cubic-bezier(.4,0,.2,1); 93 | } 94 | button:hover { 95 | background: rgba(158, 158, 158, 0.2); 96 | } 97 | button:active{ 98 | background: rgba(158, 158, 158, 0.4); 99 | } 100 | button.button-blue { 101 | background: var(--accent); 102 | color: #FFF; 103 | } 104 | button.button-blue:hover { 105 | background: #3949AB; 106 | } 107 | button.button-blue:active { 108 | background: #283593; 109 | } 110 | 111 | input[type="range"] { 112 | width: 160px; 113 | height: 2px; 114 | padding: 0; 115 | border: 0; 116 | -webkit-appearance: none; 117 | background-color: var(--input-range-line); 118 | background-image: linear-gradient(var(--accent), var(--accent)); 119 | background-repeat: no-repeat; 120 | background-size: 0 100%; 121 | } 122 | input[type=range]::-webkit-slider-thumb { 123 | -webkit-appearance: none; 124 | height: 12px; 125 | width: 12px; 126 | border-radius: 12px; 127 | background: var(--accent); 128 | transition: all .1s cubic-bezier(.4,0,.2,1); 129 | } 130 | input[type=range]:active::-webkit-slider-thumb { 131 | transform: scale(1.333); 132 | } 133 | input[type=range]:focus { 134 | outline: none; 135 | } 136 | 137 | input[type=checkbox] { 138 | -webkit-appearance: none; 139 | background: var(--checkbox-background); 140 | height: 16px; 141 | width: 16px; 142 | border-radius: 4px; 143 | vertical-align: -2px; 144 | margin-right: 4px; 145 | box-shadow: 0 0 1px #FFF; 146 | } 147 | input[type=checkbox]:checked { 148 | background-color: var(--accent); 149 | background-position: 50% 50%; 150 | background-repeat: no-repeat; 151 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16px' height='16px' viewBox='0 0 16 16'%3E%3Cpath stroke='%23FFF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none' d='M4 8 7 11 12 5'/%3E%3C/svg%3E"); 152 | } 153 | input[type=text], input[type="number"] { 154 | background-color: var(--input-background); 155 | outline: none; 156 | padding: 0 8px; 157 | height: 24px; 158 | border-radius: 4px; 159 | border: 1px solid var(--input-border); 160 | } 161 | input[type=text]:disabled, input[type="number"]:disabled { 162 | background: var(--input-background-disabled); 163 | } 164 | input[type=text]:focus, input[type="number"]:focus { 165 | border-color: var(--accent); 166 | } 167 | input[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-inner-spin-button { 168 | -webkit-appearance: none; 169 | margin: 0; 170 | } 171 | 172 | .text-input { 173 | position: relative; 174 | } 175 | .text-input-prefix input[type="text"], .text-input-prefix input[type="number"] { 176 | padding-left: 16px; 177 | } 178 | .text-input-prefix:before { 179 | content: "#"; 180 | position: absolute; 181 | top: 50%; 182 | left: 8px; 183 | z-index: 1; 184 | font-size: 12px; 185 | color: var(--text-3); 186 | transform: translateY(-50%); 187 | } 188 | .text-input-suffix input[type="text"], .text-input-suffix input[type="number"] { 189 | padding-right: 16px; 190 | } 191 | .text-input-suffix:after { 192 | content: "%"; 193 | position: absolute; 194 | top: 50%; 195 | right: 8px; 196 | z-index: 1; 197 | font-size: 12px; 198 | color: var(--text-3); 199 | transform: translateY(-50%); 200 | } 201 | 202 | input[type="text"]:invalid { 203 | border-color: #DD1144; 204 | } 205 | 206 | select { 207 | -webkit-appearance: none; 208 | font-size: 14px; 209 | padding: 0 24px 0 8px; 210 | height: 24px; 211 | outline: none; 212 | border: 0; 213 | border-radius: 5px; 214 | color: inherit; 215 | background-color: var(--select-background); 216 | background-position: calc(100% - 3px) 50%; 217 | background-repeat: no-repeat; 218 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='18px' height='18px' viewBox='0 0 18 18'%3E%3Crect width='18' height='18' rx='3' fill='%233F51B5'/%3E%3Cpath stroke='%23FFF' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none' d='M6 6.5 9 4 12 6.5 M6 11.5 9 14 12 11.5'/%3E%3C/svg%3E"); 219 | } 220 | 221 | .switch { 222 | position: relative; 223 | display: inline-block; 224 | width: 28px; 225 | height: 16px; 226 | vertical-align: -4px; 227 | } 228 | .switch input { 229 | display: none; 230 | } 231 | .switch .slider { 232 | position: absolute; 233 | cursor: pointer; 234 | top: 0; 235 | left: 0; 236 | width: 28px; 237 | height: 16px; 238 | border-radius: 8px; 239 | background-color: var(--switch-background); 240 | transition: .4s; 241 | } 242 | .switch .slider:before { 243 | position: absolute; 244 | content: ""; 245 | height: 12px; 246 | width: 12px; 247 | top: 2px; 248 | left: 2px; 249 | border-radius: 10px; 250 | background-color: var(--switch-thumb); 251 | transition: .4s; 252 | } 253 | .switch input:checked + .slider { 254 | background-color: #9DA6D8; 255 | } 256 | .switch input:checked + .slider:before { 257 | background-color: var(--accent); 258 | transform: translateX(12px); 259 | } 260 | 261 | #main { 262 | height: 100%; 263 | } 264 | #main, #preview, #preview pre { 265 | display: flex; 266 | flex-direction: column; 267 | } 268 | #preview, #preview pre { 269 | flex: auto; 270 | background: var(--preview-background); 271 | white-space: break-spaces; 272 | word-break: break-all; 273 | } 274 | #foot-actions { 275 | padding: 8px; 276 | align-items: center; 277 | display: flex; 278 | border-top: 1px solid var(--border); 279 | flex-shrink: 0; 280 | } 281 | 282 | .gap { 283 | display: block; 284 | width: 8px; 285 | } 286 | .gap-flex { 287 | flex: auto; 288 | } 289 | 290 | #preview { 291 | position: relative; 292 | overflow: auto; 293 | transition: background .2s; 294 | } 295 | #background_toggle { 296 | position: fixed; 297 | top: 16px; 298 | right: 16px; 299 | z-index: 100; 300 | } 301 | #background_toggle li { 302 | float: left; 303 | margin-left: 12px; 304 | list-style: none; 305 | } 306 | #background_toggle a { 307 | display: block; 308 | width: 20px; 309 | height: 20px; 310 | border-radius: 12px; 311 | border: 2px solid #FFF; 312 | overflow: hidden; 313 | background: var(--background); 314 | box-shadow: 0 2px 3px rgba(0,0,0,.3), inset 0 2px 3px rgba(0,0,0,.3); 315 | } 316 | #background_toggle #bg_toggle_light, #preview.bg_light { 317 | background: #FFF url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTBweCIgaGVpZ2h0PSIxMHB4IiB2aWV3Qm94PSIwIDAgMTAgMTAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDQ2LjEgKDQ0NDYzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5Hcm91cDwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGZpbGw9IiNFM0UzRTMiPgogICAgICAgICAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNSIgaGVpZ2h0PSI1Ij48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IHg9IjUiIHk9IjUiIHdpZHRoPSI1IiBoZWlnaHQ9IjUiPjwvcmVjdD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat; 318 | } 319 | #background_toggle #bg_toggle_dark, #preview.bg_dark { 320 | background: #2B2B2B url("data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iMTBweCIgaGVpZ2h0PSIxMHB4IiB2aWV3Qm94PSIwIDAgMTAgMTAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+CiAgICA8IS0tIEdlbmVyYXRvcjogU2tldGNoIDQ2LjEgKDQ0NDYzKSAtIGh0dHA6Ly93d3cuYm9oZW1pYW5jb2RpbmcuY29tL3NrZXRjaCAtLT4KICAgIDx0aXRsZT5Hcm91cDwvdGl0bGU+CiAgICA8ZGVzYz5DcmVhdGVkIHdpdGggU2tldGNoLjwvZGVzYz4KICAgIDxkZWZzPjwvZGVmcz4KICAgIDxnIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxnIGZpbGw9IiMwMDAwMDAiPgogICAgICAgICAgICA8cmVjdCB4PSIwIiB5PSIwIiB3aWR0aD0iNSIgaGVpZ2h0PSI1Ij48L3JlY3Q+CiAgICAgICAgICAgIDxyZWN0IHg9IjUiIHk9IjUiIHdpZHRoPSI1IiBoZWlnaHQ9IjUiPjwvcmVjdD4KICAgICAgICA8L2c+CiAgICA8L2c+Cjwvc3ZnPg==") repeat; 321 | } 322 | 323 | #nine_patch_preview { 324 | position: absolute; 325 | width: 100px; 326 | height: 100px; 327 | top: 50%; 328 | left: 50%; 329 | box-sizing: border-box; 330 | } 331 | #controller { 332 | font-size: 14px; 333 | box-sizing: border-box; 334 | padding: 8px 16px; 335 | align-items: center; 336 | display: flex; 337 | border-top: 1px solid var(--border); 338 | } 339 | #controller label { 340 | margin-right: 8px; 341 | } 342 | #controller input[type="range"] { 343 | margin-right: 16px; 344 | } 345 | #controller + #foot-actions { 346 | border: 0; 347 | } 348 | 349 | #preferences { 350 | flex-grow: 1; 351 | padding: 16px; 352 | font-size: 14px; 353 | overflow: auto; 354 | } 355 | .from-row { 356 | margin-bottom: 24px; 357 | } 358 | .from-row .from-label { 359 | display: block; 360 | color: var(--text-2); 361 | margin-bottom: 8px; 362 | font-size: 12px; 363 | text-transform: uppercase; 364 | } 365 | .grid-list { 366 | display: grid; 367 | grid-gap: 8px; 368 | grid-template-columns: 1fr 1fr 1fr; 369 | } 370 | .flex-from { 371 | display: flex; 372 | gap: 8px; 373 | align-items: center; 374 | } 375 | .logo { 376 | display: flex; 377 | flex-direction: column; 378 | justify-content: center; 379 | align-items: center; 380 | padding: 16px 0 32px 0; 381 | color: var(--text-3); 382 | } 383 | .logo img { 384 | display: block; 385 | margin-bottom: 8px; 386 | } 387 | 388 | .asset-thumbs { 389 | padding: 16px; 390 | flex-wrap: wrap; 391 | flex-direction: row !important; 392 | align-items: flex-start; 393 | gap: 8px; 394 | } 395 | .asset-thumbs .item { 396 | position: relative; 397 | width: 88px; 398 | } 399 | .asset-thumbs img { 400 | display: block; 401 | width: 88px; 402 | height: 88px; 403 | margin-bottom: 6px; 404 | border-radius: 4px; 405 | background: var(--background); 406 | object-fit: scale-down; 407 | } 408 | .asset-thumbs .name { 409 | font-size: 12px; 410 | } 411 | .asset-thumbs .name span { 412 | display: inline-block; 413 | padding: 2px 4px; 414 | max-width: 100%; 415 | overflow: hidden; 416 | white-space: nowrap; 417 | text-overflow: ellipsis; 418 | border-radius: 4px; 419 | } 420 | .asset-thumbs input[type="checkbox"] { 421 | position: absolute; 422 | z-index: 1; 423 | top: 4px; 424 | left: 4px; 425 | } 426 | .asset-thumbs input:checked + img { 427 | background: rgba(63, 81, 181, 0.1); 428 | } 429 | .asset-thumbs input:checked + img + .name span { 430 | background: var(--accent); 431 | color: #FFFFFF; 432 | } 433 | 434 | .code-tab { 435 | position: absolute; 436 | z-index: 1; 437 | top: 8px; 438 | right: 8px; 439 | font-size: 12px; 440 | display: flex; 441 | border-radius: 8px; 442 | border: 1px solid var(--border); 443 | background: var(--background); 444 | } 445 | .code-tab label { 446 | padding: 4px 8px; 447 | } 448 | .code-tab label + label { 449 | border-left: 1px solid var(--border); 450 | } 451 | .code-tab input { 452 | display: none; 453 | } 454 | .code-tab input:checked + span { 455 | color: var(--accent); 456 | } 457 | 458 | /* github.com style (c) Vasily Polovnyov */ 459 | .hljs{display:block;overflow-x:auto;color:#333;} 460 | .hljs-comment,.hljs-quote{color:#999;font-style:italic} 461 | .hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#333;font-weight:700} 462 | .hljs-number,.hljs-literal,.hljs-variable,.hljs-template-variable,.hljs-tag .hljs-attr{color:#094} 463 | .hljs-string,.hljs-doctag{color:#d14} 464 | .hljs-title,.hljs-section,.hljs-selector-id{color:#900;font-weight:500} 465 | .hljs-type,.hljs-class .hljs-title{color:#458;font-weight:500} 466 | .hljs-tag,.hljs-name,.hljs-attribute{color:#03F;} 467 | .hljs-regexp,.hljs-link{color:#093} 468 | .hljs-symbol,.hljs-bullet{color:#907} 469 | .hljs-built_in,.hljs-builtin-name{color:#08A} 470 | .hljs-meta{color:#999;font-weight:500} 471 | .hljs-deletion{background:#fdd} 472 | .hljs-addition{background:#dfd} 473 | .hljs-emphasis{font-style:italic} 474 | .hljs-strong{font-weight:500} 475 | 476 | @media (prefers-color-scheme: dark) { 477 | .hljs{color:#EEE} 478 | .hljs-comment,.hljs-quote{color:#666} 479 | .hljs-keyword,.hljs-selector-tag,.hljs-subst{color:#EEE} 480 | .hljs-number,.hljs-literal,.hljs-variable,.hljs-template-variable,.hljs-tag .hljs-attr{color:#0CC} 481 | .hljs-string,.hljs-doctag{color:#E57} 482 | .hljs-title,.hljs-section,.hljs-selector-id{color:#E44} 483 | .hljs-type,.hljs-class .hljs-title{color:#57E} 484 | .hljs-tag,.hljs-name,.hljs-attribute{color:#44F} 485 | .hljs-regexp,.hljs-link{color:#3E6} 486 | .hljs-symbol,.hljs-bullet{color:#F3B} 487 | .hljs-built_in,.hljs-builtin-name{color:#3CF} 488 | .hljs-meta{color:#666} 489 | .hljs-deletion{background:#766} 490 | .hljs-addition{background:#8B8} 491 | } -------------------------------------------------------------------------------- /resources/view_code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Android Res Export 6 | 7 | 8 | 9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /resources/view_code.js: -------------------------------------------------------------------------------- 1 | const highlight = require('highlight.js/lib/core'); 2 | const xml = require('highlight.js/lib/languages/xml'); 3 | highlight.registerLanguage('xml', xml); 4 | 5 | const main = document.getElementById('main'); 6 | const tempXMLElement = document.getElementById('tempXML'); 7 | const codeElement = document.getElementById("code"); 8 | const copyButton = document.getElementById('copy'); 9 | const saveButton = document.getElementById('save'); 10 | const cancelButton = document.getElementById('cancel'); 11 | 12 | // disable the context menu (eg. the right click menu) to have a more native feel 13 | document.addEventListener('contextmenu', (e) => { 14 | e.preventDefault(); 15 | }); 16 | 17 | // call the plugin from the webview 18 | copyButton.addEventListener('click', () => { 19 | window.postMessage('copy', tempXMLElement.value); 20 | }); 21 | 22 | saveButton.addEventListener('click', () => { 23 | window.postMessage('save', tempXMLElement.value); 24 | }); 25 | 26 | cancelButton.addEventListener('click', () => { 27 | window.postMessage('cancel'); 28 | }); 29 | 30 | main.style.opacity = '0'; 31 | 32 | window.main = (code, json) => { 33 | codeElement.innerHTML = highlight.highlight(code, {language: 'xml'}).value; 34 | tempXMLElement.value = code; 35 | 36 | // i18n 37 | if (json) { 38 | const langs = JSON.parse(json); 39 | saveButton.textContent = langs.save; 40 | cancelButton.textContent = langs.cancel; 41 | copyButton.textContent = langs.copy; 42 | } 43 | 44 | main.style.opacity = '1'; 45 | } 46 | -------------------------------------------------------------------------------- /resources/view_nine_patch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Android Res Export 6 | 7 | 8 | 9 |
10 |
    11 |
  • 12 |
  • 13 |
  • 14 |
15 |
16 |
17 |
18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 | 29 |
30 |
31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /resources/view_nine_patch.js: -------------------------------------------------------------------------------- 1 | const main = document.getElementById('main'); 2 | const ninePatchPreview = document.getElementById('nine_patch_preview'); 3 | const previewBackground = document.getElementById('preview'); 4 | const stretchWidth = document.getElementById('stretch_width'); 5 | const stretchHeight = document.getElementById('stretch_height'); 6 | const showContent = document.getElementById('show_content'); 7 | const bgToggleLight = document.getElementById('bg_toggle_light'); 8 | const bgToggleDark = document.getElementById('bg_toggle_dark'); 9 | const bgToggleWhite = document.getElementById('bg_toggle_white'); 10 | const labelStretchWidth = document.getElementById('label_stretch_width'); 11 | const labelStretchHeight = document.getElementById('label_stretch_height'); 12 | const labelStretchContext = document.getElementById('label_stretch_content'); 13 | const cancelButton = document.getElementById('cancel'); 14 | const exportButton = document.getElementById('export'); 15 | let originalWidth = ninePatchPreview.offsetWidth; 16 | let originalHeight = ninePatchPreview.offsetHeight; 17 | 18 | bgToggleLight.onclick = function() { 19 | previewBackground.setAttribute("class", "bg_light"); 20 | } 21 | 22 | bgToggleDark.onclick = function() { 23 | previewBackground.setAttribute("class", "bg_dark"); 24 | } 25 | 26 | bgToggleWhite.onclick = function() { 27 | previewBackground.setAttribute("class", "bg_white"); 28 | } 29 | 30 | function redrawNinePatch(base64) { 31 | var ninePatchWidth = originalWidth + originalWidth * stretchWidth.value/50, 32 | ninePatchHeight = originalHeight + originalHeight * stretchHeight.value/50; 33 | ninePatchPreview.style.transition = ".2s"; //width .2s, height 34 | stretchHeight.style.backgroundSize = stretchHeight.value + "% 100%"; 35 | stretchWidth.style.backgroundSize = stretchWidth.value + "% 100%"; 36 | drawNinePatch(base64, ninePatchWidth, ninePatchHeight, showContent.checked); 37 | } 38 | 39 | function drawNinePatch(base64, width, height, showContent) { 40 | 41 | var scale = 2; 42 | var _canvas = document.createElement("canvas"); 43 | var _ctx = _canvas.getContext("2d"); 44 | var img = new Image(); 45 | img.onload = function() { 46 | _canvas.width = img.width; 47 | _canvas.height = img.height; 48 | _ctx.drawImage(img, 0, 0); 49 | 50 | // Patch data 51 | var horizontalData = _ctx.getImageData(0, 0, img.width - 1 * scale, 1).data; 52 | var verticalData = _ctx.getImageData(0, 0, 1, img.height - 1 * scale).data; 53 | var patchTop = [[scale,0,0]]; 54 | var patchLeft = [[scale,0,0]]; 55 | for (var i = 1; i < horizontalData.length/4; i ++) { 56 | var a = horizontalData[i*4+3], 57 | _a = horizontalData[(i-1)*4+3]; 58 | if (a != _a) { 59 | patchTop[patchTop.length - 1][1] = i - patchTop[patchTop.length - 1][0]; 60 | if (a == 255) { 61 | patchTop[patchTop.length - 1][2] = 0; 62 | } else { 63 | patchTop[patchTop.length - 1][2] = 1; 64 | } 65 | patchTop.push([i, horizontalData.length/4-i, 0]) 66 | } 67 | if (i == horizontalData.length/4 - 1 && a == 255) { 68 | patchTop[patchTop.length - 1][2] = 1; 69 | } 70 | } 71 | for (var i = 1; i < verticalData.length/4; i ++) { 72 | var a = verticalData[i*4+3], 73 | _a = verticalData[(i-1)*4+3]; 74 | if (a != _a) { 75 | patchLeft[patchLeft.length - 1][1] = i - patchLeft[patchLeft.length - 1][0]; 76 | if (a == 255) { 77 | patchLeft[patchLeft.length - 1][2] = 0; 78 | } else { 79 | patchLeft[patchLeft.length - 1][2] = 1; 80 | } 81 | patchLeft.push([i, verticalData.length/4-i, 0]) 82 | } 83 | if (i == verticalData.length/4 - 1 && a == 255) { 84 | patchLeft[patchLeft.length - 1][2] = 1; 85 | } 86 | } 87 | 88 | // Get padding 89 | var paddingTop = 0, 90 | paddingBottom = 0, 91 | paddingLeft = 0, 92 | paddingRight = 0; 93 | var paddingTopBottomData = _ctx.getImageData(img.width-1, 1*scale, 1, img.height-2*scale).data; 94 | var paddingLeftRightData = _ctx.getImageData(1*scale, img.height-1, img.width-2*scale, 1).data; 95 | for (var i = 0; i < paddingTopBottomData.length/4; i ++) { 96 | if ( 97 | paddingTopBottomData[i*4] == 0 && 98 | paddingTopBottomData[i*4+1] == 0 && 99 | paddingTopBottomData[i*4+2] == 0 && 100 | paddingTopBottomData[i*4+3] == 255 101 | ) { 102 | paddingTop = i; 103 | break; 104 | } 105 | } 106 | for (var i = paddingTopBottomData.length/4; i > -1; i --) { 107 | if ( 108 | paddingTopBottomData[i*4] == 0 && 109 | paddingTopBottomData[i*4+1] == 0 && 110 | paddingTopBottomData[i*4+2] == 0 && 111 | paddingTopBottomData[i*4+3] == 255 112 | ) { 113 | paddingBottom = paddingTopBottomData.length/4-i-1; 114 | break; 115 | } 116 | } 117 | for (var i = 0; i < paddingLeftRightData.length/4; i ++) { 118 | if ( 119 | paddingLeftRightData[i*4] == 0 && 120 | paddingLeftRightData[i*4+1] == 0 && 121 | paddingLeftRightData[i*4+2] == 0 && 122 | paddingLeftRightData[i*4+3] == 255 123 | ) { 124 | paddingLeft = i; 125 | break; 126 | } 127 | } 128 | for (var i = paddingLeftRightData.length/4; i > -1; i --) { 129 | if ( 130 | paddingLeftRightData[i*4] == 0 && 131 | paddingLeftRightData[i*4+1] == 0 && 132 | paddingLeftRightData[i*4+2] == 0 && 133 | paddingLeftRightData[i*4+3] == 255 134 | ) { 135 | paddingRight = paddingLeftRightData.length/4-i-1; 136 | break; 137 | } 138 | } 139 | 140 | var canvas = document.createElement("canvas"); 141 | var ctx = canvas.getContext("2d"); 142 | canvas.width = width; 143 | canvas.height = height; 144 | canvas.style.width = width + "px"; 145 | canvas.style.height = height + "px"; 146 | 147 | ctx.imageSmoothingEnabled = false; 148 | 149 | // Draw start 150 | var dx = 0, 151 | dy = 0, 152 | dw = 0, 153 | dh = 0; 154 | for (var i = 0; i < patchLeft.length; i++) { 155 | dy += dh; 156 | if (patchLeft[i][2] == 0) { 157 | dh = patchLeft[i][1]; 158 | } else { 159 | dh = getLength(patchLeft, height); 160 | } 161 | for (var j = 0; j < patchTop.length; j++) { 162 | var sx = patchTop[j][0], 163 | sy = patchLeft[i][0], 164 | sw = patchTop[j][1], 165 | sh = patchLeft[i][1]; 166 | dx += dw; 167 | if (patchTop[j][2] == 0) { 168 | dw = patchTop[j][1]; 169 | } else { 170 | dw = getLength(patchTop, width); 171 | } 172 | ctx.drawImage(img, sx, sy, sw, sh, dx, dy, dw, dh); 173 | } 174 | dx = 0; 175 | dw = 0; 176 | } 177 | 178 | // Draw padding 179 | if (showContent) { 180 | ctx.fillStyle = "rgba(233, 32, 99, 0.3)"; 181 | ctx.fillRect( 182 | paddingLeft, 183 | paddingTop, 184 | width-paddingLeft-paddingRight, 185 | height-paddingTop-paddingBottom 186 | ); 187 | } 188 | 189 | ninePatchPreview.style.width = (width / scale) + "px"; 190 | ninePatchPreview.style.height = (height / scale) + "px"; 191 | ninePatchPreview.style.marginLeft = (width / scale) * -0.5 + "px"; 192 | ninePatchPreview.style.marginTop = (height / scale) * -0.5 + "px"; 193 | ninePatchPreview.style.background = "url(" + canvas.toDataURL("image/png") + ") no-repeat"; 194 | ninePatchPreview.style.backgroundSize = "cover"; 195 | 196 | } 197 | 198 | img.src = "data:image/png;base64," + base64; 199 | } 200 | 201 | function getLength(patchData, maxLength) { 202 | var fixWidth = 0, 203 | count = 0; 204 | for (var i = 0; i < patchData.length; i++) { 205 | if (patchData[i][2] == 0) { 206 | fixWidth += patchData[i][1]; 207 | } else { 208 | count ++; 209 | } 210 | } 211 | return Math.round((maxLength - fixWidth) / count); 212 | } 213 | 214 | // disable the context menu (eg. the right click menu) to have a more native feel 215 | document.addEventListener('contextmenu', (e) => { 216 | e.preventDefault(); 217 | }); 218 | 219 | // call the plugin from the webview 220 | exportButton.addEventListener('click', () => { 221 | window.postMessage('export'); 222 | }); 223 | 224 | cancelButton.addEventListener('click', () => { 225 | window.postMessage('cancel'); 226 | }); 227 | 228 | main.style.opacity = '0'; 229 | 230 | window.main = (base64, ninePatchWidth, ninePatchHeight, json) => { 231 | 232 | originalWidth = ninePatchWidth; 233 | originalHeight = ninePatchHeight; 234 | 235 | stretchWidth.oninput = function() { 236 | redrawNinePatch(base64); 237 | } 238 | 239 | stretchHeight.oninput = function() { 240 | redrawNinePatch(base64); 241 | } 242 | 243 | showContent.onchange = function() { 244 | redrawNinePatch(base64); 245 | } 246 | 247 | drawNinePatch(base64, ninePatchWidth, ninePatchHeight, false); 248 | 249 | // i18n 250 | const langs = JSON.parse(json); 251 | bgToggleLight.setAttribute('title', langs.tip_bg_light); 252 | bgToggleDark.setAttribute('title', langs.tip_bg_dark); 253 | bgToggleWhite.setAttribute('title', langs.tip_bg_white); 254 | labelStretchWidth.textContent = langs.width; 255 | labelStretchHeight.textContent = langs.height; 256 | labelStretchContext.textContent = langs.content; 257 | exportButton.textContent = langs.export; 258 | cancelButton.textContent = langs.cancel; 259 | 260 | main.style.opacity = '1'; 261 | } 262 | -------------------------------------------------------------------------------- /resources/view_vector_drawable_code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Android Res Export 6 | 7 | 8 | 9 |
10 |
11 | 12 | 13 |
14 |
15 |
16 |
17 |
18 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 33 |
34 |
35 | 36 | 37 | 38 | 39 | 40 |
41 |
42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /resources/view_vector_drawable_code.js: -------------------------------------------------------------------------------- 1 | const svg2vectordrawable = require('svg2vectordrawable/src/main.browser'); 2 | 3 | const highlight = require('highlight.js/lib/core'); 4 | const xml = require('highlight.js/lib/languages/xml'); 5 | highlight.registerLanguage('xml', xml); 6 | 7 | const main = document.getElementById('main'); 8 | const labelTint = document.getElementById('label_tint'); 9 | const labelXml = document.getElementById('label_xml_declaration'); 10 | const codeViewAvd = document.getElementById('code-view-avd'); 11 | const codeViewSvg = document.getElementById('code-view-svg'); 12 | const checkboxAddXml = document.getElementById('xml_declaration'); 13 | const tintColorHex = document.getElementById('tint_color'); 14 | const tintColorAlpha = document.getElementById('tint_color_alpha'); 15 | const tintColorSwitch = document.getElementById('tint_color_switch'); 16 | const tempSVGElement = document.getElementById('tempSVG'); 17 | const tempXMLElement = document.getElementById('tempXML'); 18 | const codeElement = document.getElementById("code"); 19 | const copyButton = document.getElementById('copy'); 20 | const saveButton = document.getElementById('save'); 21 | const cancelButton = document.getElementById('cancel'); 22 | 23 | let warning = ''; 24 | 25 | // disable the context menu (eg. the right click menu) to have a more native feel 26 | document.addEventListener('contextmenu', (e) => { 27 | e.preventDefault(); 28 | }); 29 | 30 | // call the plugin from the webview 31 | copyButton.addEventListener('click', () => { 32 | if (codeViewAvd.checked) { 33 | window.postMessage('copyCode', tempXMLElement.value); 34 | } 35 | if (codeViewSvg.checked) { 36 | window.postMessage('copyCode', tempSVGElement.value); 37 | } 38 | }); 39 | 40 | saveButton.addEventListener('click', () => { 41 | window.postMessage('saveCode', tempXMLElement.value); 42 | }); 43 | 44 | cancelButton.addEventListener('click', () => { 45 | window.postMessage('cancel'); 46 | }); 47 | 48 | checkboxAddXml.addEventListener('click', async (event) => { 49 | window.postMessage('add_xml_declaration', event.target.checked); 50 | await convert(tempSVGElement.value); 51 | }); 52 | 53 | tintColorHex.addEventListener('change', async (event) => { 54 | window.postMessage('tint_color', event.target.value.trim()); 55 | if (tintColorSwitch.checked) { 56 | await convert(tempSVGElement.value); 57 | } 58 | }); 59 | 60 | tintColorAlpha.addEventListener('change', async (event) => { 61 | window.postMessage('tint_color_alpha', event.target.value); 62 | if (tintColorSwitch.checked) { 63 | await convert(tempSVGElement.value); 64 | } 65 | }); 66 | 67 | tintColorSwitch.addEventListener('click', async (event) => { 68 | window.postMessage('tint', event.target.checked); 69 | await convert(tempSVGElement.value); 70 | }); 71 | 72 | document.querySelectorAll('input[name="view"]').forEach(node => { 73 | node.onclick = (event) => { 74 | let code = ''; 75 | if (event.target.value === 'avd') { 76 | code = tempXMLElement.value; 77 | codeElement.innerHTML = highlight.highlight(code, {language: 'xml'}).value; 78 | tooLongPathWarning(code, warning); 79 | } else { 80 | code = tempSVGElement.value; 81 | codeElement.innerHTML = highlight.highlight(code, {language: 'xml'}).value; 82 | } 83 | }; 84 | }); 85 | 86 | main.style.opacity = '0'; 87 | 88 | window.main = async (svg, json, addXml, tint, tintColor, tintAlpha) => { 89 | 90 | // i18n 91 | if (json) { 92 | const langs = JSON.parse(json); 93 | labelXml.textContent = langs.add_xml_declaration; 94 | labelTint.textContent = langs.tint_color; 95 | saveButton.textContent = langs.save; 96 | cancelButton.textContent = langs.cancel; 97 | copyButton.textContent = langs.copy; 98 | warning = langs.very_long_vector_path; 99 | } 100 | 101 | tempSVGElement.value = svg; 102 | checkboxAddXml.checked = addXml || false; 103 | tintColorHex.value = tintColor || '000000'; 104 | tintColorAlpha.value = tintAlpha || 100; 105 | tintColorSwitch.checked = tint || false; 106 | tintColorHex.blur(); 107 | tintColorAlpha.blur(); 108 | 109 | await convert(svg, warning); 110 | 111 | main.style.opacity = '1'; 112 | } 113 | 114 | async function convert(svg, warning) { 115 | const option = {} 116 | if (checkboxAddXml.checked) option.xmlTag = true; 117 | if (tintColorSwitch.checked) option.tint = toAndroidColor(tintColorHex.value, tintColorAlpha.value); 118 | const avd = await svg2vectordrawable(svg, option); 119 | if (codeViewAvd.checked) { 120 | tempXMLElement.value = avd; 121 | codeElement.innerHTML = highlight.highlight(avd, {language: 'xml'}).value; 122 | tooLongPathWarning(avd, warning); 123 | } 124 | if (codeViewSvg.checked) { 125 | tempSVGElement.value = svg; 126 | codeElement.innerHTML = highlight.highlight(svg, {language: 'xml'}).value; 127 | } 128 | } 129 | 130 | function toAndroidColor(hex, alpha) { 131 | if (!/^[a-f0-9]{6}$/i.test(hex)) { 132 | hex = '000000'; 133 | } 134 | if (parseInt(alpha) === 100) { 135 | return '#' + hex; 136 | } else { 137 | return '#' + Math.round(parseInt(alpha) * 255 / 100).toString(16).padStart(2, '0') + hex; 138 | } 139 | } 140 | 141 | function tooLongPathWarning(avd, warning) { 142 | const searchResult = avd.match(/android:pathData="(.*)"/g); 143 | if (searchResult) { 144 | const paths = searchResult.map(path => { 145 | return path.substring(18, path.length - 1); 146 | }); 147 | const nodes = document.querySelectorAll('.hljs-string'); 148 | nodes.forEach(node => { 149 | const text = node.textContent.replace(/"/g, ''); 150 | if (paths.includes(text) && text.length >= 800) { 151 | node.style.backgroundColor = 'rgba(255, 255, 102, 0.6)'; 152 | node.setAttribute('title', warning.replace(/\%/, text.length)); 153 | } 154 | }); 155 | } 156 | } -------------------------------------------------------------------------------- /src/export_app_icon.js: -------------------------------------------------------------------------------- 1 | const sketch = require('sketch/dom'); 2 | const ui = require('sketch/ui'); 3 | const settings = require('sketch/settings'); 4 | 5 | const i18n = require('./lib/i18n'); 6 | const sk = require('./lib/sk'); 7 | const { chooseFolder, directoryIsWriteable, writeContentToFile, revealInFinder } = require('./lib/fs'); 8 | 9 | export default function() { 10 | 11 | const document = sketch.getSelectedDocument(); 12 | const page = document.selectedPage; 13 | 14 | if (!settings.layerSettingForKey(page, 'is_android_app_icon_template')) { 15 | ui.message(i18n('current_page_is_no_template')); 16 | return; 17 | } 18 | 19 | const background = sk.getLayerByNameFromParent('ic_background', page); 20 | const foreground = sk.getLayerByNameFromParent('ic_foreground', page); 21 | const iconNormal = sk.getLayerByNameFromParent('ic_launcher', page); 22 | const iconRound = sk.getLayerByNameFromParent('ic_launcher_round', page); 23 | 24 | if (!background || !foreground || !iconNormal || !iconRound) { 25 | ui.message(i18n('app_icon_not_find')); 26 | return; 27 | } 28 | 29 | sk.resizeLayer(background, 108); 30 | sk.resizeLayer(foreground, 108); 31 | sk.resizeLayer(iconNormal, 192); 32 | sk.resizeLayer(iconRound, 192); 33 | 34 | // Export. 35 | let exportFolder = chooseFolder(); 36 | if (exportFolder) { 37 | // ExportFolder is writeable 38 | if (!directoryIsWriteable(exportFolder)) { 39 | ui.message(i18n('cannot_export_to_folder')); 40 | return; 41 | } 42 | 43 | // Hide grid symbol instances 44 | const gridStatus = {}; 45 | const gridForAdaptiveIcon = sk.getLayerByNameFromParent('icon_grid_android_o', page); 46 | const gridForLegacyIcon = sk.getLayerByNameFromParent('icon_grid', page); 47 | gridForAdaptiveIcon.getAllInstances().forEach(instance => { 48 | gridStatus[String(instance.id)] = instance.hidden; 49 | instance.hidden = true; 50 | }); 51 | gridForLegacyIcon.getAllInstances().forEach(instance => { 52 | gridStatus[String(instance.id)] = instance.hidden; 53 | instance.hidden = true; 54 | }); 55 | 56 | // Export XML 57 | const xmlContent = '\n' + 58 | ' \n' + 59 | ' \n' + 60 | ''; 61 | writeContentToFile(`${exportFolder}/mipmap-anydpi-v26/ic_launcher.xml`, xmlContent); 62 | 63 | // Export png 64 | [ 65 | { layer: background, name: 'ic_launcher_background.png' }, 66 | { layer: foreground, name: 'ic_launcher_foreground.png' } 67 | ].forEach(layer => { 68 | [ 69 | { scale: 1, suffix: "mdpi" }, 70 | { scale: 1.5, suffix: "hdpi" }, 71 | { scale: 2, suffix: "xhdpi" }, 72 | { scale: 3, suffix: "xxhdpi" }, 73 | { scale: 4, suffix: "xxxhdpi" } 74 | ].forEach(option => { 75 | sk.export(layer.layer, { 76 | scale: option.scale, 77 | format: 'png', 78 | output: `${exportFolder}/mipmap-${option.suffix}/${layer.name}` 79 | }); 80 | }); 81 | }); 82 | 83 | [ 84 | { layer: iconNormal, name: "ic_launcher.png" }, 85 | { layer: iconRound, name: "ic_launcher_round.png" } 86 | ].forEach(layer => { 87 | [ 88 | { scale: 0.25, suffix: "mdpi" }, 89 | { scale: 0.375, suffix: "hdpi" }, 90 | { scale: 0.5, suffix: "xhdpi" }, 91 | { scale: 0.75, suffix: "xxhdpi" }, 92 | { scale: 1, suffix: "xxxhdpi" } 93 | ].forEach(option => { 94 | sk.export(layer.layer, { 95 | scale: option.scale, 96 | format: 'png', 97 | output: `${exportFolder}/mipmap-${option.suffix}/${layer.name}` 98 | }); 99 | }); 100 | }); 101 | 102 | const googlePlayIcon = sk.getLayerByNameFromParent('google_play_icon', page); 103 | if (googlePlayIcon) { 104 | sk.resizeLayer(googlePlayIcon, 512); 105 | sk.export(googlePlayIcon, { 106 | scale: 1, 107 | format: 'png', 108 | output: `${exportFolder}/google_play_icon.png` 109 | }); 110 | } 111 | 112 | // Restore grid layer status 113 | for (let layerID in gridStatus) { 114 | sk.layerWidthID(layerID).hidden = gridStatus[layerID]; 115 | } 116 | 117 | // Reveal in Finder 118 | if (settings.settingForKey('reveal_in_finder_after_export')) { 119 | revealInFinder(exportFolder); 120 | } 121 | 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/export_bitmap_assets.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const sketch = require('sketch/dom'); 3 | const ui = require('sketch/ui'); 4 | const settings = require('sketch/settings'); 5 | const util = require('util'); 6 | 7 | const i18n = require('./lib/i18n'); 8 | const android = require('./lib/android'); 9 | const sk = require('./lib/sk'); 10 | const { chooseFolder, directoryIsWriteable, revealInFinder } = require('./lib/fs'); 11 | 12 | const document = sketch.getSelectedDocument(); 13 | const assetNameType = settings.settingForKey('asset_name_type') || 0; 14 | const exportDpis = settings.settingForKey('export_dpi') || Object.keys(android.DPIS); 15 | const appVersion = sketch.version.sketch; 16 | 17 | export default function() { 18 | 19 | const identifier = String(__command.identifier()); 20 | const showUI = document.selectedLayers.length === 0 ? true : false; 21 | const exportAssets = getBitmapAsset(); 22 | 23 | if (exportAssets.length === 0) { 24 | ui.message(i18n('no_bitmap_asset')); 25 | return; 26 | } 27 | 28 | const format = identifier === 'export_bitmap_assets_png' ? 'png' : 'webp'; 29 | let exportFolder; 30 | 31 | if (!showUI) { 32 | exportFolder = chooseFolder(); 33 | if (exportFolder) { 34 | // ExportFolder is writeable 35 | if (!directoryIsWriteable(exportFolder)) { 36 | ui.message(i18n('cannot_export_to_folder')); 37 | return; 38 | } 39 | exportAssets.forEach(layer => { 40 | exportDpis.forEach(dpi => { 41 | sk.export(layer, { 42 | output: `${exportFolder}/drawable-${dpi}/${android.assetName(layer.name, assetNameType)}.${format}`, 43 | formats: format, 44 | scale: android.dpiToScale(dpi) 45 | }); 46 | }); 47 | }); 48 | if (settings.settingForKey('reveal_in_finder_after_export')) { 49 | revealInFinder(exportFolder); 50 | } 51 | } 52 | } else { 53 | const options = { 54 | identifier: 'export_assets.webview', 55 | width: 600, 56 | height: 400, 57 | show: false, 58 | title: identifier === 'export_bitmap_assets_png' ? i18n('export_bitmap_assets_png') : i18n('export_bitmap_assets_webp'), 59 | resizable: false, 60 | minimizable: false, 61 | remembersWindowFrame: true, 62 | acceptsFirstMouse: true, 63 | alwaysOnTop: true 64 | }; 65 | 66 | const browserWindow = new BrowserWindow(options); 67 | 68 | browserWindow.once('ready-to-show', () => { 69 | browserWindow.show(); 70 | }); 71 | 72 | const webContents = browserWindow.webContents; 73 | 74 | // Main 75 | webContents.on('did-finish-load', () => { 76 | const assets = exportAssets.map(layer => { 77 | return { 78 | name: android.assetName(layer.name, assetNameType), 79 | id: layer.id, 80 | data: sk.getBase64FromLayer(layer) 81 | } 82 | }); 83 | const langs = {}; 84 | ['select_all', 'export', 'cancel'].forEach(key => langs[key] = i18n(key)); 85 | webContents.executeJavaScript(`main('${JSON.stringify(assets)}', '${JSON.stringify(langs)}')`); 86 | }); 87 | 88 | // Export 89 | webContents.on('export', assetIds => { 90 | if (assetIds.length === 0) { 91 | ui.message(i18n('select_asset_to_export')); 92 | return; 93 | } 94 | exportFolder = chooseFolder(); 95 | if (exportFolder) { 96 | // ExportFolder is writeable 97 | if (!directoryIsWriteable(exportFolder)) { 98 | ui.message(i18n('cannot_export_to_folder')); 99 | return; 100 | } 101 | exportAssets.forEach(layer => { 102 | if (assetIds.includes(layer.id)) { 103 | exportDpis.forEach(dpi => { 104 | sk.export(layer, { 105 | output: `${exportFolder}/drawable-${dpi}/${android.assetName(layer.name, assetNameType)}.${format}`, 106 | formats: format, 107 | scale: android.dpiToScale(dpi) 108 | }); 109 | }); 110 | } 111 | }); 112 | if (settings.settingForKey('reveal_in_finder_after_export')) { 113 | revealInFinder(exportFolder); 114 | } 115 | browserWindow.close(); 116 | } 117 | }); 118 | 119 | // Close 120 | webContents.on('cancel', () => { 121 | browserWindow.close(); 122 | }); 123 | 124 | browserWindow.loadURL(require('../resources/export_assets.html')); 125 | 126 | } 127 | } 128 | 129 | function getBitmapAsset() { 130 | let assets = []; 131 | let predicate = NSPredicate.predicateWithFormat( 132 | 'className == "MSSliceLayer" && name != "#9patch" && (exportOptions.firstFormat == "png" || exportOptions.firstFormat == "webp")' 133 | ); 134 | let nativeSelectedLayers; 135 | if (appVersion >= 84) { 136 | nativeSelectedLayers = document.sketchObject.documentData().selectedLayers(); 137 | } else { 138 | nativeSelectedLayers = document.sketchObject.documentData().selectedLayers().layers(); 139 | } 140 | let selectedLayers = util.toArray(nativeSelectedLayers); 141 | if (selectedLayers.length > 0) { 142 | selectedLayers.forEach(layer => { 143 | let slices = util.toArray(layer.children().filteredArrayUsingPredicate(predicate)).map(sketch.fromNative); 144 | assets = assets.concat(slices); 145 | }); 146 | return assets; 147 | } else { 148 | return util.toArray(document.sketchObject.allExportableLayers().filteredArrayUsingPredicate(predicate)).map(sketch.fromNative); 149 | } 150 | } -------------------------------------------------------------------------------- /src/export_nine_patch_assets.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const sketch = require('sketch/dom'); 3 | const ui = require('sketch/ui'); 4 | const settings = require('sketch/settings'); 5 | const util = require('util'); 6 | 7 | const i18n = require('./lib/i18n'); 8 | const android = require('./lib/android'); 9 | const { chooseFolder, directoryIsWriteable, revealInFinder, mkdir } = require('./lib/fs'); 10 | 11 | const document = sketch.getSelectedDocument(); 12 | const assetNameType = settings.settingForKey('asset_name_type') || 0; 13 | const exportDpis = settings.settingForKey('export_dpi') || Object.keys(android.DPIS); 14 | const appVersion = sketch.version.sketch; 15 | 16 | export default function() { 17 | 18 | const showUI = document.selectedLayers.length === 0 ? true : false; 19 | const exportAssets = getNinePatchAsset(); 20 | 21 | if (exportAssets.length === 0) { 22 | ui.message(i18n('no_nine_patch_asset')); 23 | return; 24 | } 25 | 26 | let exportFolder; 27 | 28 | if (!showUI) { 29 | exportFolder = chooseFolder(); 30 | if (exportFolder) { 31 | // ExportFolder is writeable 32 | if (!directoryIsWriteable(exportFolder)) { 33 | ui.message(i18n('cannot_export_to_folder')); 34 | return; 35 | } 36 | exportAssets.forEach(ninePatch => { 37 | exportNinePatch(ninePatch, exportFolder); 38 | }); 39 | if (settings.settingForKey('reveal_in_finder_after_export')) { 40 | revealInFinder(exportFolder); 41 | } 42 | } 43 | } else { 44 | const options = { 45 | identifier: 'export_nine_patch_assets.webview', 46 | width: 600, 47 | height: 400, 48 | show: false, 49 | title: i18n('export_nine_patch_assets'), 50 | resizable: false, 51 | minimizable: false, 52 | remembersWindowFrame: true, 53 | acceptsFirstMouse: true, 54 | alwaysOnTop: true 55 | }; 56 | 57 | const browserWindow = new BrowserWindow(options); 58 | 59 | browserWindow.once('ready-to-show', () => { 60 | browserWindow.show(); 61 | }); 62 | 63 | const webContents = browserWindow.webContents; 64 | 65 | // Main 66 | webContents.on('did-finish-load', () => { 67 | const assets = exportAssets.map(ninePatch => { 68 | const buffer = sketch.export(sketch.fromNative(ninePatch.group), { 69 | output: false, 70 | scales: '2', 71 | formats: 'png' 72 | }); 73 | return { 74 | name: android.assetName(String(ninePatch.group.name()), assetNameType), 75 | id: ninePatch.id, 76 | data: buffer.toString('base64') 77 | } 78 | }); 79 | const langs = {}; 80 | ['select_all', 'export', 'cancel'].forEach(key => langs[key] = i18n(key)); 81 | webContents.executeJavaScript(`main('${JSON.stringify(assets)}', '${JSON.stringify(langs)}')`); 82 | }); 83 | 84 | // Export 85 | webContents.on('export', assetIds => { 86 | if (assetIds.length === 0) { 87 | ui.message(i18n('select_asset_to_export')); 88 | return; 89 | } 90 | exportFolder = chooseFolder(); 91 | if (exportFolder) { 92 | // ExportFolder is writeable 93 | if (!directoryIsWriteable(exportFolder)) { 94 | ui.message(i18n('cannot_export_to_folder')); 95 | return; 96 | } 97 | exportAssets.forEach(ninePatch => { 98 | if (assetIds.includes(ninePatch.id)) { 99 | exportNinePatch(ninePatch, exportFolder); 100 | } 101 | }); 102 | if (settings.settingForKey('reveal_in_finder_after_export')) { 103 | revealInFinder(exportFolder); 104 | } 105 | browserWindow.close(); 106 | } 107 | }); 108 | 109 | // Close 110 | webContents.on('cancel', () => { 111 | browserWindow.close(); 112 | }); 113 | 114 | browserWindow.loadURL(require('../resources/export_assets.html')); 115 | 116 | } 117 | } 118 | 119 | function exportNinePatch(ninePatch, exportFolder) { 120 | var ninePatchGroup = ninePatch.group; 121 | var ninePatchContent = ninePatch.content; 122 | var ninePatchPatch = ninePatch.patch; 123 | var ninePatchName = android.assetName(String(ninePatchGroup.name()), assetNameType); 124 | 125 | // Patch lines round to pixel 126 | util.toArray(ninePatchPatch.layers()).forEach(line => { 127 | if (line.className() == 'MSRectangleShape') { 128 | line.frame().setX(Math.round(line.frame().x())); 129 | line.frame().setY(Math.round(line.frame().y())); 130 | line.frame().setWidth(Math.max(1, Math.ceil(line.frame().width()))); 131 | line.frame().setHeight(Math.max(1, Math.ceil(line.frame().height()))); 132 | } 133 | }); 134 | 135 | // Save nine-patch patch NSImage at mdpi 136 | var nsImageOfPatchTop = imageOfLayer_rect_scale( 137 | ninePatchPatch, 138 | CGRectMake(ninePatchPatch.absoluteRect().x() + 1, ninePatchPatch.absoluteRect().y(), ninePatchPatch.absoluteRect().width() - 2, 1), 139 | 1 140 | ); 141 | var nsImageOfPatchRight = imageOfLayer_rect_scale( 142 | ninePatchPatch, 143 | CGRectMake(ninePatchPatch.absoluteRect().x() + ninePatchPatch.absoluteRect().width() - 1, ninePatchPatch.absoluteRect().y() + 1, 1, ninePatchPatch.absoluteRect().height() - 2), 144 | 1 145 | ); 146 | var nsImageOfPatchBottom = imageOfLayer_rect_scale( 147 | ninePatchPatch, 148 | CGRectMake(ninePatchPatch.absoluteRect().x() + 1, ninePatchPatch.absoluteRect().y() + ninePatchPatch.absoluteRect().height() - 1, ninePatchPatch.absoluteRect().width() - 2, 1), 149 | 1 150 | ); 151 | var nsImageOfPatchLeft = imageOfLayer_rect_scale( 152 | ninePatchPatch, 153 | CGRectMake(ninePatchPatch.absoluteRect().x(), ninePatchPatch.absoluteRect().y() + 1, 1, ninePatchPatch.absoluteRect().height() - 2), 154 | 1 155 | ); 156 | 157 | // Export 158 | exportDpis.forEach(dpi => { 159 | const scale = android.dpiToScale(dpi); 160 | if (scale === 1) { 161 | const exportRequestOfPatchGroup = exportRequestOfLayer_inRect_scale(ninePatchGroup, ninePatchGroup.absoluteRect().rect(), 1); 162 | document.sketchObject.saveExportRequest_toFile(exportRequestOfPatchGroup, `${exportFolder}/drawable-mdpi/${ninePatchName}.9.png`); 163 | } else { 164 | const nsImageOfPatchContent = imageOfLayer_rect_scale(ninePatchContent, ninePatchContent.absoluteRect().rect(), scale); 165 | const bitmapRepOfPatchContent = bitmapRepFromNSImage_scale(nsImageOfPatchContent, 1); 166 | const bitmapRepOfPatchTop = bitmapRepFromNSImage_scale(nsImageOfPatchTop, scale); 167 | const bitmapRepOfPatchRight = bitmapRepFromNSImage_scale(nsImageOfPatchRight, scale); 168 | const bitmapRepOfPatchBottom= bitmapRepFromNSImage_scale(nsImageOfPatchBottom, scale); 169 | const bitmapRepOfPatchLeft = bitmapRepFromNSImage_scale(nsImageOfPatchLeft, scale); 170 | 171 | const contentImageSize = nsImageOfPatchContent.size(); 172 | const scaleFactor = NSScreen.mainScreen().backingScaleFactor(); 173 | const resultImageSize = NSMakeSize(Math.round((contentImageSize.width + 2)), Math.round((contentImageSize.height + 2))); 174 | const resultImage = NSImage.alloc().initWithSize(resultImageSize); 175 | resultImage.lockFocus(); 176 | NSGraphicsContext.currentContext().setImageInterpolation(NSImageInterpolationNone); 177 | let transform = NSAffineTransform.transform(); 178 | transform.scaleXBy_yBy(1 / scaleFactor, 1 / scaleFactor); 179 | transform.concat(); 180 | nsImageOfPatchContent.drawRepresentation_inRect(bitmapRepOfPatchContent, NSMakeRect(1, 1, contentImageSize.width, contentImageSize.height)); 181 | nsImageOfPatchTop.drawRepresentation_inRect(bitmapRepOfPatchTop, NSMakeRect(1, resultImageSize.height - 1, contentImageSize.width, 1)); 182 | nsImageOfPatchRight.drawRepresentation_inRect(bitmapRepOfPatchRight, NSMakeRect(resultImageSize.width - 1, 1, 1, contentImageSize.height)); 183 | nsImageOfPatchBottom.drawRepresentation_inRect(bitmapRepOfPatchBottom, NSMakeRect(1, 1 - bitmapRepOfPatchBottom.pixelsHigh(), contentImageSize.width, 1)); 184 | nsImageOfPatchLeft.drawRepresentation_inRect(bitmapRepOfPatchLeft, NSMakeRect(1 - bitmapRepOfPatchLeft.pixelsWide(), 1, 1, contentImageSize.height)); 185 | const resultImageBitmapRep = NSBitmapImageRep.alloc().initWithFocusedViewRect(NSMakeRect(0, 0, resultImageSize.width, resultImageSize.height)); 186 | resultImage.unlockFocus(); 187 | 188 | const imageData = resultImageBitmapRep.representationUsingType_properties(NSPNGFileType, nil); 189 | const folder = `${exportFolder}/drawable-${dpi}`; 190 | mkdir(folder); 191 | imageData.writeToFile_atomically(`${folder}/${ninePatchName}.9.png`, 'NO'); 192 | } 193 | }); 194 | } 195 | 196 | function imageOfLayer_rect_scale(layer, rect, scale) { 197 | const exportRequest = exportRequestOfLayer_inRect_scale(layer, rect, scale); 198 | const exporter = MSExporter.exporterForRequest_colorSpace(exportRequest, document.sketchObject.colorSpace()); 199 | const image = exporter.image(); 200 | const imageSize = NSMakeSize(Math.round(rect.size.width * scale), Math.round(rect.size.height * scale)); 201 | image.setSize(imageSize); 202 | return image; 203 | } 204 | 205 | function exportRequestOfLayer_inRect_scale(layer, rect, scale) { 206 | const exportRequest = MSExportRequest.exportRequestsFromLayerAncestry_inRect(layer.ancestry(), rect).firstObject(); 207 | exportRequest.setFormat('png'); 208 | exportRequest.setScale(scale); 209 | return exportRequest; 210 | } 211 | 212 | function bitmapRepFromNSImage_scale(nsImage, scale) { 213 | const scaledSize = NSMakeSize(Math.round(nsImage.size().width * scale), Math.round(nsImage.size().height * scale)); 214 | const imageData = nsImage.TIFFRepresentation(); 215 | const imageRep = NSBitmapImageRep.imageRepWithData(imageData); 216 | imageRep.setSize(scaledSize); 217 | imageRep.setPixelsWide(scaledSize.width); 218 | imageRep.setPixelsHigh(scaledSize.height); 219 | 220 | const scaleFactor = NSScreen.mainScreen().backingScaleFactor(); 221 | const imageScale = NSImage.alloc().initWithSize( 222 | NSMakeSize(Math.round(nsImage.size().width * scale / scaleFactor), Math.round(nsImage.size().height * scale / scaleFactor)) 223 | ); 224 | imageScale.lockFocus(); 225 | NSGraphicsContext.currentContext().setImageInterpolation(NSImageInterpolationNone); 226 | let transform = NSAffineTransform.transform(); 227 | transform.scaleXBy_yBy(1 / scaleFactor, 1 / scaleFactor); 228 | transform.concat(); 229 | nsImage.drawRepresentation_inRect(imageRep, NSMakeRect(0, 0, imageScale.size().width, imageScale.size().height)); 230 | const bitmapRep = NSBitmapImageRep.alloc().initWithFocusedViewRect(NSMakeRect(0, 0, scaledSize.width, scaledSize.height)); 231 | imageScale.unlockFocus(); 232 | 233 | return bitmapRep; 234 | } 235 | 236 | function getNinePatchAsset() { 237 | let assets = []; 238 | let predicate = NSPredicate.predicateWithFormat( 239 | 'className == "MSSliceLayer" && name == "#9patch" && exportOptions.firstFormat == "png"' 240 | ); 241 | let nativeSelectedLayers; 242 | if (appVersion >= 84) { 243 | nativeSelectedLayers = document.sketchObject.documentData().selectedLayers(); 244 | } else { 245 | nativeSelectedLayers = document.sketchObject.documentData().selectedLayers().layers(); 246 | } 247 | let selectedLayers = util.toArray(nativeSelectedLayers); 248 | if (selectedLayers.length > 0) { 249 | selectedLayers.forEach(layer => { 250 | let slices = util.toArray(layer.children().filteredArrayUsingPredicate(predicate)); 251 | slicesToAssets(slices, assets); 252 | }); 253 | } else { 254 | let slices = util.toArray(document.sketchObject.allExportableLayers().filteredArrayUsingPredicate(predicate)); 255 | slicesToAssets(slices, assets); 256 | } 257 | return assets; 258 | } 259 | 260 | function slicesToAssets(slices, assets) { 261 | slices.forEach(slice => { 262 | if (isNinePatchLayerGroup(slice)) { 263 | let group = slice.parentGroup().parentGroup(); 264 | assets.push({ 265 | id: String(group.objectID()), 266 | group, 267 | content: slice.parentGroup(), 268 | patch: util.toArray(group.layers()).find(layer => layer.name() == 'patch') 269 | }); 270 | } 271 | }); 272 | } 273 | 274 | function isNinePatchLayerGroup(msSlice) { 275 | const root = msSlice.parentGroup().parentGroup(); 276 | if ( 277 | root && 278 | root.class() == 'MSLayerGroup' && 279 | msSlice.parentGroup().name() == 'content' && 280 | root.layers().count() == 2 && 281 | util.toArray(root.layers()).map(layer => String(layer.name())).every(name => ['content', 'patch'].includes(name)) && 282 | util.toArray(root.layers()).find(layer => layer.name() == 'patch').layers().count() >= 4 283 | ) { 284 | return true; 285 | } 286 | return false; 287 | } -------------------------------------------------------------------------------- /src/export_vector_assets.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const sketch = require('sketch/dom'); 3 | const ui = require('sketch/ui'); 4 | const settings = require('sketch/settings'); 5 | const util = require('util'); 6 | 7 | const i18n = require('./lib/i18n'); 8 | const android = require('./lib/android'); 9 | const sk = require('./lib/sk'); 10 | const { chooseFolder, directoryIsWriteable, revealInFinder, writeContentToFile } = require('./lib/fs'); 11 | 12 | const document = sketch.getSelectedDocument(); 13 | const assetNameType = settings.settingForKey('asset_name_type') || 0; 14 | const vectorFolder = android.VECTORDRAWABLE_FOLDERS[settings.settingForKey('vector_drawable_folder') || 2]; 15 | const showUI = document.selectedLayers.length === 0 ? true : false; 16 | const appVersion = sketch.version.sketch; 17 | 18 | const langs = {}; 19 | ['select_all', 'export', 'cancel'].forEach(key => langs[key] = i18n(key)); 20 | 21 | export default function() { 22 | 23 | const exportAssets = getVectorDrawableAsset(); 24 | 25 | if (exportAssets.length === 0) { 26 | ui.message(i18n('no_vector_drawable_asset')); 27 | return; 28 | } 29 | 30 | const options = { 31 | identifier: 'export_vector_drawable.webview', 32 | width: 600, 33 | height: 400, 34 | show: false, 35 | title: i18n('export_vector_assets'), 36 | resizable: false, 37 | minimizable: false, 38 | remembersWindowFrame: true, 39 | acceptsFirstMouse: true, 40 | alwaysOnTop: true 41 | }; 42 | 43 | const browserWindow = new BrowserWindow(options); 44 | 45 | if (showUI) { 46 | browserWindow.once('ready-to-show', () => { 47 | browserWindow.show(); 48 | }); 49 | } 50 | 51 | const webContents = browserWindow.webContents; 52 | 53 | // Main 54 | webContents.on('did-finish-load', () => { 55 | let supportedExportAssets = exportAssets.filter(layer => { 56 | return isSupported(layer); 57 | }); 58 | if (supportedExportAssets.length !== exportAssets.length) { 59 | ui.message(i18n('ignore_not_support_layer')); 60 | } 61 | if (showUI) { 62 | const assets = supportedExportAssets.map(layer => { 63 | return { 64 | name: android.assetName(layer.name, assetNameType), 65 | data: sk.getBase64FromLayer(layer), 66 | svg: encodeURIComponent(sk.getOriginalSVGFromLayer(layer)) 67 | } 68 | }); 69 | webContents.executeJavaScript(`main('${JSON.stringify(assets)}', '${JSON.stringify(langs)}')`); 70 | } else { 71 | const assets = supportedExportAssets.map(layer => { 72 | return { 73 | name: android.assetName(layer.name, assetNameType), 74 | svg: encodeURIComponent(sk.getOriginalSVGFromLayer(layer)) 75 | } 76 | }); 77 | webContents.executeJavaScript(`exportSelection('${JSON.stringify(assets)}')`); 78 | } 79 | }); 80 | 81 | // Export 82 | webContents.on('export', assets => { 83 | if (assets.length === 0) { 84 | ui.message(i18n('select_asset_to_export')); 85 | return; 86 | } 87 | let exportFolder = chooseFolder(); 88 | if (exportFolder) { 89 | // ExportFolder is writeable 90 | if (!directoryIsWriteable(exportFolder)) { 91 | ui.message(i18n('cannot_export_to_folder')); 92 | return; 93 | } 94 | 95 | assets.forEach(({name, xml}) => { 96 | writeContentToFile(`${exportFolder}/${vectorFolder}/${name}.xml`, xml); 97 | }); 98 | 99 | if (settings.settingForKey('reveal_in_finder_after_export')) { 100 | revealInFinder(exportFolder); 101 | } 102 | 103 | browserWindow.close(); 104 | } 105 | }); 106 | 107 | // Close 108 | webContents.on('cancel', () => { 109 | browserWindow.close(); 110 | }); 111 | 112 | browserWindow.loadURL(require('../resources/export_vector_assets.html')); 113 | 114 | } 115 | 116 | function getVectorDrawableAsset() { 117 | let assets = []; 118 | let predicate = NSPredicate.predicateWithFormat( 119 | 'className == "MSSliceLayer" && exportOptions.firstFormat == "svg"' 120 | ); 121 | let nativeSelectedLayers; 122 | if (appVersion >= 84) { 123 | nativeSelectedLayers = document.sketchObject.documentData().selectedLayers(); 124 | } else { 125 | nativeSelectedLayers = document.sketchObject.documentData().selectedLayers().layers(); 126 | } 127 | let selectedLayers = util.toArray(nativeSelectedLayers); 128 | if (selectedLayers.length > 0) { 129 | selectedLayers.forEach(layer => { 130 | let slices = util.toArray(layer.children().filteredArrayUsingPredicate(predicate)).map(sketch.fromNative); 131 | assets = assets.concat(slices); 132 | }); 133 | return assets; 134 | } else { 135 | return util.toArray(document.sketchObject.allExportableLayers().filteredArrayUsingPredicate(predicate)).map(sketch.fromNative); 136 | } 137 | } 138 | 139 | function isSupported(layer) { 140 | if (layer.hidden) { 141 | return false; 142 | } 143 | if (layer.frame.width > 200 && layer.frame.height > 200) { 144 | return false; 145 | } 146 | if (sk.countChildOfLayer(layer) > 20) { 147 | return false; 148 | } 149 | for (let child of sk.recursivelyChildOfLayer(layer)) { 150 | if (sk.isImage(child) && !layer.hidden) { 151 | return false; 152 | } 153 | if ((sk.hasShadow(child) || sk.hasInnerShadow(child)) && !layer.hidden) { 154 | return false; 155 | } 156 | if (sk.hasBlur(child) && !layer.hidden) { 157 | return false; 158 | } 159 | } 160 | return true; 161 | } -------------------------------------------------------------------------------- /src/fill_type_to_non_zero.js: -------------------------------------------------------------------------------- 1 | const sketch = require('sketch/dom'); 2 | const ui = require('sketch/ui'); 3 | 4 | const i18n = require('./lib/i18n'); 5 | 6 | export default function() { 7 | const document = sketch.getSelectedDocument(); 8 | const selection = document.selectedLayers; 9 | 10 | if (selection.isEmpty) { 11 | ui.message(i18n('no_selection')); 12 | return; 13 | } 14 | 15 | selection.layers.forEach(layer => { 16 | traverse(layer.sketchObject); 17 | }); 18 | 19 | } 20 | 21 | function traverse(layer) { 22 | layer.children().forEach(child => { 23 | const shapeTypes = ['MSShapeGroup', 'MSRectangleShape', 'MSOvalShape', 'MSShapePathLayer', 'MSTriangleShape', 'MSStarShape', 'MSPolygonShape']; 24 | if (shapeTypes.includes(String(child.className())) && child.parentGroup().className() != 'MSShapeGroup') { 25 | child.style().setWindingRule(0); 26 | } 27 | if (child.className() == 'MSSymbolInstance') { 28 | if (child.symbolMaster().isForeign()) { 29 | ui.message(i18n('library_symbol_can_not_change_fill_type')); 30 | } else { 31 | traverse(child.symbolMaster()); 32 | } 33 | } 34 | }); 35 | } -------------------------------------------------------------------------------- /src/help.js: -------------------------------------------------------------------------------- 1 | const { openURL } = require('./lib/fs'); 2 | 3 | export default function() { 4 | const identifier = String(__command.identifier()); 5 | 6 | if (identifier === 'web_site') { 7 | openURL('https://github.com/Ashung/Android_Res_Export') 8 | } 9 | 10 | if (identifier === 'report_issues') { 11 | openURL('https://github.com/Ashung/Android_Res_Export/issues') 12 | } 13 | 14 | if (identifier === 'donate') { 15 | openURL('http://ashung.github.io/donate.html') 16 | } 17 | 18 | if (identifier === 'buymeacoffee') { 19 | openURL('https://www.buymeacoffee.com/ashung') 20 | } 21 | 22 | if (identifier === 'donate_wechat') { 23 | openURL('https://github.com/Ashung/ashung.github.io/blob/master/assets/img/donate_wechat_rmb_10.png') 24 | } 25 | 26 | if (identifier === 'donate_alipay') { 27 | openURL('https://github.com/Ashung/ashung.github.io/blob/master/assets/img/donate_alipay_rmb_10.png') 28 | } 29 | } -------------------------------------------------------------------------------- /src/language.js: -------------------------------------------------------------------------------- 1 | const { fileExists, copyFileTo } = require('./lib/fs'); 2 | 3 | export function onOpenDocument() { 4 | 5 | const sketchLanguage = String(NSUserDefaults.standardUserDefaults().objectForKey('AppleLanguages').firstObject()).replace(/-\w*/g, ''); 6 | const languageFile = __command.pluginBundle().urlForResourceNamed(`manifest_${sketchLanguage}.json`).path(); 7 | const manifestFilePath = __command.pluginBundle().url().path() + '/Contents/Sketch/manifest.json'; 8 | 9 | if (fileExists(languageFile)) { 10 | NSFileManager.defaultManager().removeItemAtPath_error(manifestFilePath, nil); 11 | NSFileManager.defaultManager().copyItemAtPath_toPath_error(languageFile, manifestFilePath, nil); 12 | AppController.sharedInstance().pluginManager().reloadPlugins(); 13 | } 14 | } -------------------------------------------------------------------------------- /src/languages.json: -------------------------------------------------------------------------------- 1 | { 2 | "no_selection": { 3 | "en": "Please select at least 1 layer.", 4 | "zh": "请至少选中一个图层。" 5 | }, 6 | "select_one_layer": { 7 | "en": "Please select 1 layer.", 8 | "zh": "请选中一个图层。" 9 | }, 10 | "can_not_create_asset_from_hot_spot": { 11 | "en": "Can't create asset from hotspot.", 12 | "zh": "不能从热区创建资源。" 13 | }, 14 | "can_not_create_asset_from_artboard": { 15 | "en": "Can't create asset from artboard.", 16 | "zh": "不能从画板创建资源。" 17 | }, 18 | "can_not_create_asset_from_symbol_master": { 19 | "en": "Can't create asset from symbol master.", 20 | "zh": "不能从组件创建资源。" 21 | }, 22 | "can_not_create_asset_from_slice": { 23 | "en": "Can't create asset from slice.", 24 | "zh": "不能从切片创建资源。" 25 | }, 26 | "slice_must_in_group": { 27 | "en": "Slice layer must in side a group.", 28 | "zh": "切片图层必须在组内。" 29 | }, 30 | "color_xml_from_layers": { 31 | "en": "Color XML from Selected Layers", 32 | "zh": "选中图层的色彩 XML 资源" 33 | }, 34 | "color_xml_from_color_variables": { 35 | "en": "Color XML from Color Variables", 36 | "zh": "色彩变量的 XML 资源" 37 | }, 38 | "no_color_variables": { 39 | "en": "Document have not color variables.", 40 | "zh": "文档中没有色彩变量。" 41 | }, 42 | "ok": { 43 | "en": "OK", 44 | "zh": "确定" 45 | }, 46 | "cancel": { 47 | "en": "Cancel", 48 | "zh": "取消" 49 | }, 50 | "export": { 51 | "en": "Export", 52 | "zh": "导出" 53 | }, 54 | "save": { 55 | "en": "Save", 56 | "zh": "保存" 57 | }, 58 | "copy": { 59 | "en": "Copy", 60 | "zh": "拷贝" 61 | }, 62 | "copied": { 63 | "en": "Code copied.", 64 | "zh": "代码已复制。" 65 | }, 66 | "save_done": { 67 | "en": "File saved.", 68 | "zh": "保存完成。" 69 | }, 70 | "view_shape_drawable_from_selected_layer": { 71 | "en": "Shape Drawable from Selected Layer", 72 | "zh": "选中图层的形状代码" 73 | }, 74 | "not_support_layer_style": { 75 | "en": "Layer style like shadow and blur is not support by Android shape drawable.", 76 | "zh": "Android 形状资源不支持投影、模糊等图层样式。" 77 | }, 78 | "too_many_color_stop": { 79 | "en": "The gradient in Android shape drawable not support greater than 3 colors.", 80 | "zh": "Android 形状资源不支持大于 3 种颜色的渐变。" 81 | }, 82 | "not_support_fill_type": { 83 | "en": "The fill type is not support for Android shape drawable.", 84 | "zh": "Android 形状资源不支持图像等填充类型。" 85 | }, 86 | "not_support_stroke_type": { 87 | "en": "The stroke type is not support for Android shape drawable.", 88 | "zh": "Android 形状资源不支持渐变、图像等描边类型。" 89 | }, 90 | "not_support_shape": { 91 | "en": "Only support shape layer like rectangle or oval.", 92 | "zh": "Android 形状资源只支持矩形、圆等形状。" 93 | }, 94 | "no_shape_layer": { 95 | "en": "Please choose 1 shape layer like rectangle or oval.", 96 | "zh": "请选择一个矩形、圆等形状图层。" 97 | }, 98 | "no_colors_in_selection": { 99 | "en": "Please select at least 1 shape layer with solid or gradient fill.", 100 | "zh": "请选中至少一个带有单色或渐变填充的图层。" 101 | }, 102 | "view_nine_patch": { 103 | "en": "Nine-Patch Preview", 104 | "zh": "点九图片预览" 105 | }, 106 | "select_one_nine_patch_asset": { 107 | "en": "Please select a nine-patch asset.", 108 | "zh": "请选择一个点九资源。" 109 | }, 110 | "nine_patch_layer_structure_is_wrong": { 111 | "en": "The layer structure of nine-patch asset is wrong.", 112 | "zh": "点九资源的图层结构有误。" 113 | }, 114 | "can_not_create_nine_patch_from_artboard_or_symbol_master": { 115 | "en": "Can't create Nine-Patch from Artboard or Symbol Master.", 116 | "zh": "不可从画板或组件模版创建点九资源。" 117 | }, 118 | "preferences": { 119 | "en": "Preferences", 120 | "zh": "参数设置" 121 | }, 122 | "asset_name_type_0": { 123 | "en": "Valid last part of layer name. (a / b / c -> c)", 124 | "zh": "合法的最后段图层名. (a / b / c -> c)" 125 | }, 126 | "asset_name_type_1": { 127 | "en": "Valid full layer name. (a / b / c -> a_b_c)", 128 | "zh": "合法的完整图层名. (a / b / c -> a_b_c)" 129 | }, 130 | "asset_name_type_2": { 131 | "en": "Last part of layer name. (a / b / c -> c)", 132 | "zh": "最后段图层名. (a / b / c -> c)" 133 | }, 134 | "asset_name_type_3": { 135 | "en": "Full layer name. (a / b / c -> a_b_c)", 136 | "zh": "完整图层名. (a / b / c -> a_b_c)" 137 | }, 138 | "no_nine_patch_asset": { 139 | "en": "No any nine-patch asset.", 140 | "zh": "无任何点九资源。" 141 | }, 142 | "no_vector_drawable_asset": { 143 | "en": "No any vector drawable asset.", 144 | "zh": "无任何矢量资源。" 145 | }, 146 | "export_dpis": { 147 | "en": "Export DPIs", 148 | "zh": "导出分辨率" 149 | }, 150 | "asset_name_type": { 151 | "en": "Asset Name Type", 152 | "zh": "资源命名方式" 153 | }, 154 | "vector_drawable_folder": { 155 | "en": "Vector Drawable Folder", 156 | "zh": "矢量资源文件夹" 157 | }, 158 | "others": { 159 | "en": "Others", 160 | "zh": "其他" 161 | }, 162 | "reveal_in_finder_after_export": { 163 | "en": "Reveal in Finder after export.", 164 | "zh": "导出资源后在访达中显示。" 165 | }, 166 | "webp_quality": { 167 | "en": "WebP Quality", 168 | "zh": "WebP 图片质量" 169 | }, 170 | "vector_drawable_limit": { 171 | "en": "Recommend that limit a vector drawable to maximum fo 200 x 200dp.", 172 | "zh": "建议矢量资源不超过 200 x 200dp。" 173 | }, 174 | "tip_bg_light": { 175 | "en": "Use light transparent background", 176 | "zh": "使用亮色透明背景" 177 | }, 178 | "tip_bg_dark": { 179 | "en": "Use dark transparent background", 180 | "zh": "使用暗色透明色背景" 181 | }, 182 | "tip_bg_white": { 183 | "en": "Use white background", 184 | "zh": "使用白色背景" 185 | }, 186 | "width": { 187 | "en": "Width", 188 | "zh": "宽" 189 | }, 190 | "height": { 191 | "en": "Height", 192 | "zh": "高" 193 | }, 194 | "content": { 195 | "en": "Content", 196 | "zh": "内容区" 197 | }, 198 | "export_done": { 199 | "en": "Export done!", 200 | "zh": "导出完成!" 201 | }, 202 | "no_bitmap_asset": { 203 | "en": "No any bitmap asset.", 204 | "zh": "无任何位图资源。" 205 | }, 206 | "cannot_export_to_folder": { 207 | "en": "You can't export assets to this folder.", 208 | "zh": "无法导入文件到此文件夹。" 209 | }, 210 | "export_bitmap_assets_png": { 211 | "en": "Export Bitmap Assets (PNG)", 212 | "zh": "导出位图资源 (PNG)" 213 | }, 214 | "export_bitmap_assets_webp": { 215 | "en": "Export Bitmap Assets (WebP)", 216 | "zh": "导出位图资源 (WebP)" 217 | }, 218 | "export_nine_patch_assets": { 219 | "en": "Export Nine-Patch Assets", 220 | "zh": "导出点九资源" 221 | }, 222 | "select_all": { 223 | "en": "Select / Deselect All", 224 | "zh": "选择/取消选择全部" 225 | }, 226 | "no_permission": { 227 | "en": "No permission to save file.", 228 | "zh": "无写入权限" 229 | }, 230 | "select_asset_to_export": { 231 | "en": "Please select 1 asset to export.", 232 | "zh": "请选择一个需要导出的资源。" 233 | }, 234 | "current_page_is_no_template": { 235 | "en": "Current page is no a Android app icon template.", 236 | "zh": "当前页不是一个 Android 应用图标模版。" 237 | }, 238 | "app_icon_not_find": { 239 | "en": "Can't find Android app icon inside current page.", 240 | "zh": "在当前页中找不到 Android 应用图标。" 241 | }, 242 | "view_vector_drawable_from_selected_layer": { 243 | "en": "Vector Drawable from Selected Layer", 244 | "zh": "选中图层的矢量资源代码" 245 | }, 246 | "add_xml_declaration": { 247 | "en": "Add XML Declaration", 248 | "zh": "添加 XML 声明" 249 | }, 250 | "tint_color": { 251 | "en": "Tint Color", 252 | "zh": "着色" 253 | }, 254 | "vector_drawable_not_support_bitmap_layer": { 255 | "en": "Vector drawable not support image layer.", 256 | "zh": "矢量资源不支持位图图层。" 257 | }, 258 | "vector_drawable_not_support_shadow": { 259 | "en": "Vector drawable not support shadow and inner shadow style.", 260 | "zh": "矢量资源不支持投影和内投影样式。" 261 | }, 262 | "vector_drawable_not_support_blur": { 263 | "en": "Vector drawable not support blur style.", 264 | "zh": "矢量资源不支持模糊样式。" 265 | }, 266 | "hidden_layer": { 267 | "en": "Don't select a hidden layer.", 268 | "zh": "请勿选择隐藏图层。" 269 | }, 270 | "export_vector_assets": { 271 | "en": "Export Vector Assets", 272 | "zh": "导出矢量资源" 273 | }, 274 | "vector_drawable_too_many_layer": { 275 | "en": "Not support too many layer.", 276 | "zh": "不支持过多图层。" 277 | }, 278 | "ignore_not_support_layer": { 279 | "en": "Ignore some layer that not support by vector drawable.", 280 | "zh": "已忽略部分矢量资源不支持的图层。" 281 | }, 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | "new_nine_patch_asset": { 292 | "en": "New Nine-Patch Asset", 293 | "zh": "新建点九资源" 294 | }, 295 | "new_app_icon": { 296 | "en": "New App Icon", 297 | "zh": "新建应用图标" 298 | }, 299 | "new_vector_asset": { 300 | "en": "New Vector Asset", 301 | "zh": "新建矢量资源" 302 | }, 303 | "library_symbol_can_not_change_fill_type": { 304 | "en": "Library symbol can't change fill type to Non-Zero.", 305 | "zh": "库组件无法改变填充规则为非零" 306 | }, 307 | "very_long_vector_path": { 308 | "en": "Very long vector path (% characters), which is bad for performance. Considering reducing precision, removing minor details or rasterizing vector.", 309 | "zh": "非常长的矢量路径 (%个字符),这对性能不利。考虑降低精度、删除次要细节或光栅化矢量。" 310 | } 311 | } -------------------------------------------------------------------------------- /src/lib/android.js: -------------------------------------------------------------------------------- 1 | const VECTORDRAWABLE_FOLDERS = [ 2 | 'drawable', 3 | 'drawable-anydpi', 4 | 'drawable-anydpi-v21', 5 | 'drawable-anydpi-v24' 6 | ]; 7 | 8 | const DPIS = { 9 | 'mdpi': 1, 10 | 'hdpi': 1.5, 11 | 'xhdpi': 2, 12 | 'xxhdpi': 3, 13 | 'xxxhdpi': 4 14 | }; 15 | 16 | function dpiToScale(dpi) { 17 | if (DPIS[dpi]) { 18 | return DPIS[dpi]; 19 | } 20 | return 1; 21 | } 22 | 23 | function assetName(layerName, type, defaultName) { 24 | let nameArray = layerName.split(/\s*\/\s*/); 25 | let name; 26 | // Valid last part of layer name. 27 | if (type == 0 || type == null) { 28 | name = cleanName(nameArray[nameArray.length - 1]).replace(/^\d+_*/, ''); 29 | } 30 | // Valid full layer name 31 | else if (type == 1) { 32 | let nameParts = []; 33 | nameArray.forEach(part => { 34 | nameParts.push(cleanName(part)); 35 | }); 36 | name = nameParts.join('_').replace(/^\d+_*/, ''); 37 | } 38 | // Last part of layer name. 39 | else if (type == 2) { 40 | name = nameArray[nameArray.length - 1]; 41 | name = name.replace(/[`~!@#$%^&*+=:;,<>?|(){}\[\]\\]/g, ''); 42 | name = name.trim(); 43 | } 44 | // Full layer name 45 | else if (type == 3) { 46 | name = layerName.replace(/\s*\/\s*/g, '_'); 47 | name = name.replace(/[`~!@#$%^&*+=:;,<>?|(){}\[\]\\]/g, ''); 48 | name = name.trim(); 49 | } 50 | if ( 51 | name == '' || 52 | (name.match(/_/g) != null && name.match(/_/g).length == name.length) 53 | ) { 54 | return defaultName || 'untitled_asset'; 55 | } 56 | return name.toLowerCase(); 57 | } 58 | 59 | function cleanName(name) { 60 | // Latin to ascii 61 | const latinToAsciiMapping = { 62 | 'ae': 'ä|æ|ǽ', 63 | 'oe': 'ö|œ', 64 | 'ue': 'ü', 65 | 'Ae': 'Ä', 66 | 'Ue': 'Ü', 67 | 'Oe': 'Ö', 68 | 'A': 'À|Á|Â|Ã|Ä|Å|Ǻ|Ā|Ă|Ą|Ǎ', 69 | 'a': 'à|á|â|ã|å|ǻ|ā|ă|ą|ǎ|ª', 70 | 'C': 'Ç|Ć|Ĉ|Ċ|Č', 71 | 'c': 'ç|ć|ĉ|ċ|č', 72 | 'D': 'Ð|Ď|Đ', 73 | 'd': 'ð|ď|đ', 74 | 'E': 'È|É|Ê|Ë|Ē|Ĕ|Ė|Ę|Ě', 75 | 'e': 'è|é|ê|ë|ē|ĕ|ė|ę|ě', 76 | 'G': 'Ĝ|Ğ|Ġ|Ģ', 77 | 'g': 'ĝ|ğ|ġ|ģ', 78 | 'H': 'Ĥ|Ħ', 79 | 'h': 'ĥ|ħ', 80 | 'I': 'Ì|Í|Î|Ï|Ĩ|Ī|Ĭ|Ǐ|Į|İ', 81 | 'i': 'ì|í|î|ï|ĩ|ī|ĭ|ǐ|į|ı', 82 | 'J': 'Ĵ', 83 | 'j': 'ĵ', 84 | 'K': 'Ķ', 85 | 'k': 'ķ', 86 | 'L': 'Ĺ|Ļ|Ľ|Ŀ|Ł', 87 | 'l': 'ĺ|ļ|ľ|ŀ|ł', 88 | 'N': 'Ñ|Ń|Ņ|Ň', 89 | 'n': 'ñ|ń|ņ|ň|ʼn', 90 | 'O': 'Ò|Ó|Ô|Õ|Ō|Ŏ|Ǒ|Ő|Ơ|Ø|Ǿ', 91 | 'o': 'ò|ó|ô|õ|ō|ŏ|ǒ|ő|ơ|ø|ǿ|º', 92 | 'R': 'Ŕ|Ŗ|Ř', 93 | 'r': 'ŕ|ŗ|ř', 94 | 'S': 'Ś|Ŝ|Ş|Š', 95 | 's': 'ś|ŝ|ş|š|ſ', 96 | 'T': 'Ţ|Ť|Ŧ', 97 | 't': 'ţ|ť|ŧ', 98 | 'U': 'Ù|Ú|Û|Ũ|Ū|Ŭ|Ů|Ű|Ų|Ư|Ǔ|Ǖ|Ǘ|Ǚ|Ǜ', 99 | 'u': 'ù|ú|û|ũ|ū|ŭ|ů|ű|ų|ư|ǔ|ǖ|ǘ|ǚ|ǜ', 100 | 'Y': 'Ý|Ÿ|Ŷ', 101 | 'y': 'ý|ÿ|ŷ', 102 | 'W': 'Ŵ', 103 | 'w': 'ŵ', 104 | 'Z': 'Ź|Ż|Ž', 105 | 'z': 'ź|ż|ž', 106 | 'AE': 'Æ|Ǽ', 107 | 'ss': 'ß', 108 | 'IJ': 'IJ', 109 | 'ij': 'ij', 110 | 'OE': 'Œ', 111 | 'f': 'ƒ', 112 | }; 113 | for (let i in latinToAsciiMapping) { 114 | let regexp = new RegExp(latinToAsciiMapping[i], 'g'); 115 | name = name.replace(regexp, i); 116 | } 117 | // Remove no ascii character 118 | name = name.replace(/[^\u0020-\u007E]/g, ''); 119 | // Remove unsupport character 120 | name = name.replace(/[\u0021-\u002B\u003A-\u0040\u005B-\u005E\u0060\u007B-\u007E]/g, ''); 121 | // Unix hidden file 122 | name = name.replace(/^\./, ''); 123 | // , - . _ to space 124 | name = name.replace(/[\u002C-\u002E\u005F]/g, '_'); 125 | // Replace space to _ 126 | name = name.trim(); 127 | name = name.replace(/\s+/g, '_'); 128 | name = name.toLowerCase(); 129 | return name; 130 | } 131 | 132 | function mscolorToAndroid(mscolor) { 133 | let color = mscolor.immutableModelObject().hexValue(); 134 | let alpha = mscolor.alpha(); 135 | if (alpha < 1) { 136 | color = Math.round(alpha * 255).toString(16).padStart(2, '0') + color; 137 | } 138 | return ('#' + color).toUpperCase(); 139 | } 140 | 141 | function colorToAndroid(color) { 142 | // RRGGBBAA to AARRGGBB 143 | // 8 digit hex code 144 | if (/^#[0-9a-f]{8}$/i.test(color)) { 145 | if (color.substr(7, 2).toLowerCase() === 'ff') { 146 | color = '#' + color.substr(1, 6); 147 | } else { 148 | color = '#' + color.substr(7, 2) + color.substr(1, 6); 149 | } 150 | } 151 | // RGBA to AARRGGBB 152 | // 4 digit hex code 153 | else if (/^#[0-9a-f]{4}$/i.test(color)) { 154 | if (color[4].toLowerCase() === 'f') { 155 | color = '#' + color[1].repeat(2) + color[2].repeat(2) + color[3].repeat(2); 156 | } else { 157 | color = '#' + color[4].repeat(2) + color[1].repeat(2) + color[2].repeat(2) + color[3].repeat(2); 158 | } 159 | } 160 | else if (/^#[0-9a-f]{6}$/i.test(color)) { 161 | color = '#' + color.substr(1, 6); 162 | } 163 | else if (/^#[0-9a-f]{3}$/i.test(color)) { 164 | color = '#' + color[1].repeat(2) + color[2].repeat(2) + color[3].repeat(2); 165 | } 166 | else { 167 | color = '#000000'; 168 | } 169 | return color.toUpperCase(); 170 | } 171 | 172 | module.exports.VECTORDRAWABLE_FOLDERS = VECTORDRAWABLE_FOLDERS; 173 | module.exports.DPIS = DPIS; 174 | module.exports.dpiToScale = dpiToScale; 175 | module.exports.assetName = assetName; 176 | module.exports.cleanName = cleanName; 177 | module.exports.mscolorToAndroid = mscolorToAndroid; 178 | module.exports.colorToAndroid = colorToAndroid; 179 | -------------------------------------------------------------------------------- /src/lib/fs.js: -------------------------------------------------------------------------------- 1 | function fileExists(path) { 2 | return NSFileManager.defaultManager().fileExistsAtPath_(path); 3 | } 4 | 5 | function writeContentToFile(filePath, content) { 6 | const parentDir = NSString.stringWithString(filePath).stringByDeletingLastPathComponent(); 7 | mkdir(parentDir); 8 | const data = NSString.stringWithString(content); 9 | data.writeToFile_atomically_encoding_error( 10 | filePath, true, NSUTF8StringEncoding, null 11 | ); 12 | return parentDir; 13 | } 14 | 15 | function mkdir(path) { 16 | if (!fileExists(path)) { 17 | return NSFileManager.defaultManager().createDirectoryAtPath_withIntermediateDirectories_attributes_error_( 18 | path, true, nil, nil 19 | ); 20 | } 21 | } 22 | 23 | function directoryIsWriteable(path) { 24 | return NSFileManager.defaultManager().isWritableFileAtPath(path); 25 | } 26 | 27 | function chooseFolder() { 28 | var panel = NSOpenPanel.openPanel(); 29 | panel.setCanChooseDirectories(true); 30 | panel.setCanChooseFiles(false); 31 | panel.setCanCreateDirectories(true); 32 | if (panel.runModal() == NSOKButton) { 33 | return panel.URL().path(); 34 | } 35 | } 36 | 37 | function saveToFolder(fileName) { 38 | var panel = NSSavePanel.savePanel(); 39 | panel.setNameFieldStringValue(fileName); 40 | panel.setCanCreateDirectories(true); 41 | if (panel.runModal() == NSOKButton) { 42 | return panel.URL().path(); 43 | } 44 | } 45 | 46 | function revealInFinder(path) { 47 | return NSWorkspace.sharedWorkspace().openFile_withApplication(path, "Finder"); 48 | } 49 | 50 | function openInFinder(path) { 51 | var fileManager = NSFileManager.defaultManager(); 52 | var workspace = NSWorkspace.sharedWorkspace(); 53 | var attributesOfFile = fileManager.attributesOfItemAtPath_error(path, nil); 54 | if (attributesOfFile) { 55 | var fileType = attributesOfFile.objectForKey("NSFileType"); 56 | if (fileType == "NSFileTypeSymbolicLink") { 57 | var symbolicLinkPath = fileManager.destinationOfSymbolicLinkAtPath_error(path, nil); 58 | var url = NSURL.alloc().initWithString(path); 59 | var absolutePath = NSURL.fileURLWithPath_relativeToURL(symbolicLinkPath, url).path(); 60 | if (fileManager.fileExistsAtPath(absolutePath)) { 61 | var fileTypeOfSymbolicLink = fileManager.attributesOfItemAtPath_error(absolutePath, nil).objectForKey("NSFileType"); 62 | if (fileTypeOfSymbolicLink == "NSFileTypeRegular") { 63 | return workspace.selectFile_inFileViewerRootedAtPath(path, nil); 64 | } 65 | if (fileTypeOfSymbolicLink == "NSFileTypeDirectory") { 66 | return workspace.openFile(path); 67 | } 68 | } else { 69 | return workspace.selectFile_inFileViewerRootedAtPath(path, nil); 70 | } 71 | } else if (fileType == "NSFileTypeRegular") { 72 | return workspace.selectFile_inFileViewerRootedAtPath(path, nil); 73 | } else if (fileType == "NSFileTypeDirectory") { 74 | return workspace.openFile(path); 75 | } 76 | } 77 | } 78 | 79 | function pasteboardCopy(text) { 80 | var pasteboard = NSPasteboard.generalPasteboard(); 81 | pasteboard.clearContents(); 82 | pasteboard.setString_forType(text, NSStringPboardType); 83 | } 84 | 85 | function openURL(url) { 86 | NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(url)); 87 | } 88 | 89 | module.exports.fileExists = fileExists; 90 | module.exports.writeContentToFile = writeContentToFile; 91 | module.exports.mkdir = mkdir; 92 | module.exports.directoryIsWriteable = directoryIsWriteable; 93 | module.exports.chooseFolder = chooseFolder; 94 | module.exports.saveToFolder = saveToFolder; 95 | module.exports.revealInFinder = revealInFinder; 96 | module.exports.openInFinder = openInFinder; 97 | module.exports.pasteboardCopy = pasteboardCopy; 98 | module.exports.openURL = openURL; -------------------------------------------------------------------------------- /src/lib/i18n.js: -------------------------------------------------------------------------------- 1 | module.exports = function(langKey) { 2 | const languages = require('../languages.json'); 3 | // Sketch language 4 | const sketchLanguage = String( 5 | NSUserDefaults.standardUserDefaults().objectForKey('AppleLanguages').firstObject() 6 | ).replace(/-\w*/g, ''); 7 | 8 | if (languages[langKey]) { 9 | let langString = languages[langKey][sketchLanguage]; 10 | for (let i = 1; i < arguments.length; i++) { 11 | let regExp = new RegExp('\%' + i, 'g'); 12 | langString = langString.replace(regExp, arguments[i]); 13 | } 14 | return langString; 15 | } 16 | return ''; 17 | } 18 | -------------------------------------------------------------------------------- /src/lib/sk.js: -------------------------------------------------------------------------------- 1 | const util = require('util'); 2 | const sketch = require('sketch/dom'); 3 | const { Document, Slice, Rectangle, ShapePath, Artboard } = require('sketch/dom'); 4 | const appVersion = sketch.version.sketch; 5 | 6 | module.exports.isGroup = function(layer) { 7 | const types = ['Group', 'Artboard', 'SymbolMaster']; 8 | return types.includes(layer.type); 9 | } 10 | 11 | module.exports.isShape = function(layer) { 12 | const types = ['Shape', 'ShapePath']; 13 | return types.includes(layer.type); 14 | } 15 | 16 | module.exports.isRectangleShape = function(layer) { 17 | return layer.sketchObject.className() == 'MSRectangleShape'; 18 | } 19 | 20 | module.exports.isLayerGroup = function(layer) { 21 | return layer.type === 'Group'; 22 | } 23 | 24 | module.exports.isArtboard = function(layer) { 25 | return layer.type === 'Artboard'; 26 | } 27 | 28 | module.exports.isSymbolMaster = function(layer) { 29 | return layer.type === 'SymbolMaster'; 30 | } 31 | 32 | module.exports.isSlice = function(layer) { 33 | return layer.type === 'Slice'; 34 | } 35 | 36 | module.exports.isHotspot = function(layer) { 37 | return layer.type === 'HotSpot'; 38 | } 39 | 40 | module.exports.isPage = function(layer) { 41 | return layer.type === 'Page'; 42 | } 43 | 44 | module.exports.isImage = function(layer) { 45 | if (layer.type === 'Image') { 46 | return true; 47 | } 48 | if (layer.style && layer.style.fills.some(fill => fill.fillType === 'Pattern' && fill.enabled)) { 49 | return true; 50 | } 51 | return false; 52 | } 53 | 54 | module.exports.roundToPixel = function(layer) { 55 | layer.frame = new Rectangle( 56 | Math.round(layer.frame.x), 57 | Math.round(layer.frame.y), 58 | Math.ceil(layer.frame.width), 59 | Math.ceil(layer.frame.height) 60 | ); 61 | } 62 | 63 | module.exports.addSliceIntoGroup = function(group, sliceName, exportFormats) { 64 | let slice = new Slice({ 65 | name: sliceName, 66 | parent: group, 67 | frame: new Rectangle(0, 0, Math.ceil(group.frame.width), Math.ceil(group.frame.height)), 68 | exportFormats: exportFormats 69 | }); 70 | this.exportGroupContentOnly(slice); 71 | this.sendToBack(slice); 72 | return slice; 73 | } 74 | 75 | module.exports.addSliceBeforeLayer = function(layer, sliceName, exportFormats) { 76 | let slice = new Slice({ 77 | name: sliceName, 78 | parent: layer.parent, 79 | frame: new Rectangle( 80 | Math.round(layer.frame.x), 81 | Math.round(layer.frame.y), 82 | Math.ceil(layer.frame.width), 83 | Math.ceil(layer.frame.height) 84 | ), 85 | exportFormats: exportFormats 86 | }); 87 | let msLayer = slice.sketchObject; 88 | msLayer.moveToLayer_beforeLayer(msLayer.parentGroup(), layer.sketchObject); 89 | return slice; 90 | } 91 | 92 | module.exports.removeSliceInGroup = function(group) { 93 | group.sketchObject.children().forEach(child => { 94 | if (child.class() == 'MSSliceLayer') { 95 | child.removeFromParent(); 96 | } 97 | }); 98 | } 99 | 100 | module.exports.group = function(layers) { 101 | let group; 102 | if (appVersion >= 84) { 103 | group = MSLayerGroup.groupWithLayers(layers.map(layer => layer.sketchObject)); 104 | } else { 105 | let layerArray = MSLayerArray.arrayWithLayers(layers.map(layer => layer.sketchObject)); 106 | if (appVersion >= 83) { 107 | group = MSLayerGroup.groupWithLayers(layerArray.layers()); 108 | } else if (appVersion >= 52) { 109 | group = MSLayerGroup.groupWithLayers(layerArray); 110 | } else { 111 | group = MSLayerGroup.groupFromLayers(layerArray); 112 | } 113 | } 114 | return sketch.fromNative(group); 115 | } 116 | 117 | module.exports.exportGroupContentOnly = function(slice) { 118 | let msLayer = slice.sketchObject; 119 | msLayer.exportOptions().setLayerOptions(2); 120 | } 121 | 122 | module.exports.sendToBack = function(layer) { 123 | let msLayer = layer.sketchObject; 124 | msLayer.moveToLayer_beforeLayer(msLayer.parentGroup(), msLayer.parentGroup().firstLayer()); 125 | } 126 | 127 | module.exports.getLayerByNameFromParent = function(name, parent) { 128 | return parent.layers.find(layer => layer.name === name); 129 | } 130 | 131 | module.exports.addRectShape = function(parent, frame, color, name) { 132 | let shape = new ShapePath({ 133 | name, 134 | parent, 135 | frame: new Rectangle(frame.x, frame.y, frame.width, frame.height), 136 | style: { 137 | fills: [color] 138 | } 139 | }); 140 | return shape; 141 | } 142 | 143 | module.exports.fitGroup = function(group) { 144 | group.sketchObject.fixGeometryWithOptions(1); 145 | } 146 | 147 | module.exports.selectLayer = function(layer) { 148 | const document = Document.getSelectedDocument(); 149 | const selection = document.selectedLayers; 150 | selection.clear(); 151 | layer.selected = true; 152 | } 153 | 154 | module.exports.moveLayerIntoGroup = function(layer, group) { 155 | layer.sketchObject.moveToLayer_beforeLayer(group.sketchObject, group.sketchObject.firstLayer()); 156 | } 157 | 158 | module.exports.getSVGFromLayer = function(layer) { 159 | let svg = this.getOriginalSVGFromLayer(layer); 160 | return svg.replace(/\n/g, '\\n'); 161 | } 162 | 163 | module.exports.getOriginalSVGFromLayer = function(layer) { 164 | const nativeLayer = layer.sketchObject; 165 | const document = Document.getSelectedDocument(); 166 | const page = MSPage.alloc().init(); 167 | document._getMSDocumentData().addPage(page); 168 | const artboard = MSArtboardGroup.alloc().init(); 169 | artboard.setFrame(MSRect.rectWithRect(CGRectMake(0, 0, nativeLayer.frame().width(), nativeLayer.frame().height()))); 170 | page.addLayer(artboard); 171 | if (nativeLayer.className() == 'MSArtboardGroup' || nativeLayer.className() == 'MSSymbolMaster' || nativeLayer.className() == 'MSLayerGroup') { 172 | const children = []; 173 | nativeLayer.layers().forEach(function(layer) { 174 | children.push(layer.copy()); 175 | }); 176 | artboard.setLayers(children); 177 | } else { 178 | const clonedLayer = nativeLayer.copy(); 179 | clonedLayer.frame().setX(0); 180 | clonedLayer.frame().setY(0); 181 | artboard.addLayer(clonedLayer); 182 | } 183 | const symbolInstances = []; 184 | for (let i = 1; i < artboard.children().count(); i++) { 185 | const child = artboard.children().objectAtIndex(i); 186 | if (child.className() == 'MSSymbolInstance') { 187 | symbolInstances.push(child); 188 | } 189 | } 190 | for (let i = 0; i < symbolInstances.length; i++) { 191 | const detach = symbolInstances[i].detachStylesAndReplaceWithGroupRecursively(); 192 | if (detach.isKindOfClass(NSMapTable)) { 193 | const group = detach.objectForKey(symbolInstances[i].immutableModelObject()); 194 | group.ungroup(); 195 | } 196 | } 197 | const options = { formats: 'svg', output: false }; 198 | const buffer = sketch.export(Artboard.fromNative(artboard), options); 199 | const svg = buffer.toString(); 200 | document._getMSDocumentData().removePage(page); 201 | return svg; 202 | } 203 | 204 | module.exports.getBase64FromLayer = function(layer) { 205 | const buffer = sketch.export(layer, { 206 | output: false, 207 | scales: '2', 208 | formats: 'png' 209 | }); 210 | return buffer.toString('base64'); 211 | } 212 | 213 | module.exports.export = function(layer, option) { 214 | const ancestry = layer.sketchObject.ancestry(); 215 | const exportRequest = MSExportRequest.exportRequestsFromLayerAncestry(ancestry).firstObject(); 216 | exportRequest.setFormat(option.format || 'png'); 217 | exportRequest.setScale(option.scale || 1); 218 | Document.getSelectedDocument().sketchObject.saveExportRequest_toFile(exportRequest, option.output); 219 | } 220 | 221 | module.exports.collapse = function(layer) { 222 | layer.sketchObject.setLayerListExpandedType(1); 223 | } 224 | 225 | module.exports.resizeLayer = function(layer, size) { 226 | let { width, height } = size 227 | layer.frame.width = width || size; 228 | layer.frame.height = height || size; 229 | } 230 | 231 | module.exports.layerWidthID = function(id) { 232 | let layer = Document.getSelectedDocument().sketchObject.documentData().layerWithID(id); 233 | return sketch.fromNative(layer); 234 | } 235 | 236 | module.exports.childOfLayer = function(layer) { 237 | return util.toArray(layer.sketchObject.children()).map(sketch.fromNative); 238 | } 239 | 240 | module.exports.recursivelyChildOfLayer = function(layer) { 241 | function traversing(layer) { 242 | return util.toArray(layer.sketchObject.children()).map(_child => { 243 | let child = sketch.fromNative(_child); 244 | if (child.type === "SymbolInstance") { 245 | return traversing(child.master); 246 | } else { 247 | return child; 248 | } 249 | }); 250 | } 251 | return traversing(layer).flat(Infinity); 252 | } 253 | 254 | module.exports.countChildOfLayer = function(layer) { 255 | let count = 0; 256 | function traversing(layer) { 257 | if (layer.layers && layer.type !== 'Shape') { 258 | layer.layers.forEach(child => { 259 | traversing(child); 260 | }); 261 | } else if (layer.type === "SymbolInstance") { 262 | traversing(layer.master); 263 | } else if (!['Slice', 'HotSpot', 'Group', 'SymbolMaster', 'Artboard'].includes(layer.type)) { 264 | count ++; 265 | } 266 | } 267 | traversing(layer); 268 | return count; 269 | } 270 | 271 | module.exports.hasShadow = function(layer) { 272 | return layer.style && layer.style.shadows.some(shadow => shadow.enabled); 273 | } 274 | 275 | module.exports.hasInnerShadow = function(layer) { 276 | return layer.style && layer.style.innerShadows.some(shadow => shadow.enabled); 277 | } 278 | 279 | module.exports.hasBlur = function(layer) { 280 | return layer.style && layer.style.blur.enabled; 281 | } 282 | 283 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": [ 3 | { 4 | "name": "New Bitmap Asset", 5 | "identifier": "new_bitmap_asset", 6 | "script": "new_asset.js" 7 | }, 8 | { 9 | "name": "New Vector Asset", 10 | "identifier": "new_vector_asset", 11 | "script": "new_asset.js" 12 | }, 13 | { 14 | "name": "New Nine-Patch Asset", 15 | "identifier": "new_nine_patch_asset", 16 | "script": "new_nine_patch_asset.js" 17 | }, 18 | { 19 | "name": "New App Icon", 20 | "identifier": "new_app_icon", 21 | "script": "new_app_icon.js" 22 | }, 23 | { 24 | "name": "Export Bitmap Assets (PNG)", 25 | "identifier": "export_bitmap_assets_png", 26 | "script": "export_bitmap_assets.js" 27 | }, 28 | { 29 | "name": "Export Bitmap Assets (WebP)", 30 | "identifier": "export_bitmap_assets_webp", 31 | "script": "export_bitmap_assets.js" 32 | }, 33 | { 34 | "name": "Export Nine-Patch Assets", 35 | "identifier": "export_nine_patch_assets", 36 | "script": "export_nine_patch_assets.js" 37 | }, 38 | { 39 | "name": "Export Vector Assets", 40 | "identifier": "export_vector_assets", 41 | "script": "export_vector_assets.js" 42 | }, 43 | { 44 | "name": "Export App Icon", 45 | "identifier": "export_app_icon", 46 | "script": "export_app_icon.js" 47 | }, 48 | { 49 | "name": "Color XML From Selected Layers", 50 | "identifier": "view_color_code_from_selected_layers", 51 | "script": "view_color_code_from_selected_layers.js", 52 | "handlers": { 53 | "run": "onRun", 54 | "actions": { 55 | "Shutdown": "onShutdown", 56 | "SelectionChanged.finish": "onSelectionChanged" 57 | } 58 | } 59 | }, 60 | { 61 | "name": "Color XML From Color Variables", 62 | "identifier": "view_color_code_from_color_variables", 63 | "script": "view_color_code_from_color_variables.js" 64 | }, 65 | { 66 | "name": "Shape Drawable From Selected Layer", 67 | "identifier": "view_shape_drawable_from_selected_layer", 68 | "script": "view_shape_code.js", 69 | "handlers": { 70 | "run": "onRun", 71 | "actions": { 72 | "Shutdown": "onShutdown", 73 | "SelectionChanged.finish": "onSelectionChanged" 74 | } 75 | } 76 | }, 77 | { 78 | "name": "Nine-Patch Preview", 79 | "identifier": "view_nine_patch", 80 | "script": "view_nine_patch.js" 81 | }, 82 | { 83 | "name": "Vector Drawable From Selected Layer", 84 | "identifier": "view_vector_drawable_code", 85 | "script": "view_vector_drawable_code.js", 86 | "handlers": { 87 | "run": "onRun", 88 | "actions": { 89 | "Shutdown": "onShutdown", 90 | "SelectionChanged.finish": "onSelectionChanged" 91 | } 92 | } 93 | }, 94 | { 95 | "name": "Change Fill Type to Non-Zero", 96 | "identifier": "fill_type_to_non_zero", 97 | "script": "fill_type_to_non_zero.js" 98 | }, 99 | { 100 | "name": "Preferences", 101 | "identifier": "preferences", 102 | "script": "preferences.js" 103 | }, 104 | { 105 | "name": "Web Site", 106 | "identifier": "web_site", 107 | "script": "help.js" 108 | }, 109 | { 110 | "name": "Report Issues", 111 | "identifier": "report_issues", 112 | "script": "help.js" 113 | }, 114 | { 115 | "name": "Donate", 116 | "identifier": "donate", 117 | "script": "help.js" 118 | }, 119 | { 120 | "name": "Buy Me a Coffee", 121 | "identifier": "buymeacoffee", 122 | "script": "help.js" 123 | }, 124 | { 125 | "handlers": { 126 | "actions": { 127 | "OpenDocument": "onOpenDocument" 128 | } 129 | }, 130 | "script": "language.js" 131 | } 132 | ], 133 | "menu": { 134 | "title": "Android Res Export", 135 | "items": [ 136 | "new_bitmap_asset", 137 | "new_vector_asset", 138 | "new_nine_patch_asset", 139 | "new_app_icon", 140 | "-", 141 | "export_bitmap_assets_png", 142 | "export_bitmap_assets_webp", 143 | "export_vector_assets", 144 | "export_nine_patch_assets", 145 | "export_app_icon", 146 | "-", 147 | "view_nine_patch", 148 | "view_color_code_from_color_variables", 149 | "view_color_code_from_selected_layers", 150 | "view_shape_drawable_from_selected_layer", 151 | "view_vector_drawable_code", 152 | "-", 153 | "fill_type_to_non_zero", 154 | "-", 155 | "preferences", 156 | { 157 | "title": "Help", 158 | "items": [ 159 | "web_site", 160 | "report_issues", 161 | "-", 162 | "donate", 163 | "buymeacoffee" 164 | ] 165 | } 166 | ] 167 | }, 168 | "name": "Android Res Export", 169 | "description": "Export Android resources in Sketch.", 170 | "homepage": "https://github.com/Ashung/Android_Res_Export", 171 | "icon": "icon.png", 172 | "identifier": "com.ashung.hung.android_res_export", 173 | "compatibleVersion": 3, 174 | "bundleVersion": 1 175 | } -------------------------------------------------------------------------------- /src/new_app_icon.js: -------------------------------------------------------------------------------- 1 | const sketch = require('sketch/dom'); 2 | const settings = require('sketch/settings'); 3 | const { 4 | Artboard, 5 | Group, 6 | Image, 7 | Page, 8 | Rectangle, 9 | ShapePath, 10 | Style, 11 | SymbolMaster, 12 | } = require('sketch/dom'); 13 | 14 | const { collapse } = require('./lib/sk'); 15 | 16 | export default function() { 17 | 18 | const document = sketch.getSelectedDocument(); 19 | 20 | const logoSVG = ` 21 | 22 | `; 23 | 24 | // grid 25 | const iconGrid = new SymbolMaster({ 26 | name: 'icon_grid', 27 | frame: new Rectangle(484, 608, 96, 96), 28 | layers: [ 29 | new Image({ 30 | name: 'grid', 31 | image: String(__command.pluginBundle().urlForResourceNamed('grid.png').path()), 32 | frame: new Rectangle(0, 0, 96, 96) 33 | }) 34 | ] 35 | }); 36 | 37 | // grid_android_o 38 | const iconGridAndroidO = new SymbolMaster({ 39 | name: 'icon_grid_android_o', 40 | frame: new Rectangle(316, 400, 108, 108), 41 | layers: [ 42 | new Image({ 43 | name: 'grid', 44 | image: String(__command.pluginBundle().urlForResourceNamed('grid_android_o.png').path()), 45 | frame: new Rectangle(0, 0, 108, 108) 46 | }) 47 | ] 48 | }); 49 | 50 | // ic_launcher 51 | const launcherIconLegacy = new SymbolMaster({ 52 | name: 'ic_launcher', 53 | frame: new Rectangle(0, 608, 192, 192), 54 | layers: (() => { 55 | let bg = new ShapePath({ 56 | name: 'bg', 57 | frame: new Rectangle(20, 20, 152, 152), 58 | shapeType: ShapePath.ShapeType.Rectangle, 59 | style: { 60 | fills: [ 61 | { color: '#39AA57' } 62 | ], 63 | shadows: [ 64 | { color: '#00000033', x: 0, y: 4, blur: 4 }, 65 | { color: '#0000001a', x: 0, y: 0, blur: 4 } 66 | ], 67 | innerShadows: [ 68 | { color: '#1b5e2033', x: 0, y: -1, blur: 1 }, 69 | ] 70 | } 71 | }); 72 | bg.points.forEach(point => { 73 | point.cornerRadius = 12; 74 | }); 75 | bg.sketchObject.setHasClippingMask(true); 76 | 77 | let shadow = ShapePath.fromSVGPath('M52 116 129.05 103 129.05 86.39 172 129.5 172 172 108 172z'); 78 | shadow.name = 'shadow'; 79 | shadow.style.fills = [ 80 | { 81 | fillType: Style.FillType.Gradient, 82 | gradient: { 83 | gradientType: Style.GradientType.Linear, 84 | from: { x: 0.248, y: 0.114 }, 85 | to: { x: 1, y: 1 }, 86 | stops: [ 87 | { position: 0, color: '#00000026' }, 88 | { position: 0.142, color: '#00000026' }, 89 | { position: 0.643, color: '#00000005' }, 90 | { position: 1, color: '#00000000' } 91 | ] 92 | } 93 | } 94 | ]; 95 | 96 | let logoGroup = sketch.createLayerFromData(logoSVG, 'svg'); 97 | let logo = logoGroup.layers[0]; 98 | logo.name = 'logo'; 99 | logo.frame = logoGroup.frame; 100 | 101 | let grid = iconGrid.createNewInstance(); 102 | grid.name = 'grid'; 103 | grid.frame = new Rectangle(0, 0, 192, 192); 104 | grid.sketchObject.setShouldBreakMaskChain(true); 105 | 106 | return [bg, shadow, logo, grid]; 107 | })() 108 | }); 109 | 110 | // ic_launcher_round 111 | const launcherRoundIconLegacy = new SymbolMaster({ 112 | name: 'ic_launcher_round', 113 | frame: new Rectangle(242, 608, 192, 192), 114 | layers: (() => { 115 | let bg = new ShapePath({ 116 | name: 'bg', 117 | frame: new Rectangle(8, 8, 176, 176), 118 | shapeType: ShapePath.ShapeType.Oval, 119 | style: { 120 | fills: [ 121 | { color: '#FFFFFF' } 122 | ], 123 | shadows: [ 124 | { color: '#00000033', x: 0, y: 4, blur: 4 }, 125 | { color: '#0000001a', x: 0, y: 0, blur: 4 } 126 | ] 127 | } 128 | }); 129 | bg.sketchObject.setHasClippingMask(true); 130 | 131 | let logoGroup = sketch.createLayerFromData(logoSVG, 'svg'); 132 | let logo = logoGroup.layers[0]; 133 | logo.name = 'logo'; 134 | logo.style.fills = [{ color: '#39AA57' }]; 135 | logo.frame = logoGroup.frame; 136 | 137 | let grid = iconGrid.createNewInstance(); 138 | grid.name = 'grid'; 139 | grid.frame = new Rectangle(0, 0, 192, 192); 140 | grid.sketchObject.setShouldBreakMaskChain(true); 141 | 142 | return [bg, logo, grid]; 143 | })() 144 | }); 145 | 146 | // ic_background 147 | const iconBackground = new SymbolMaster({ 148 | name: 'ic_background', 149 | frame: new Rectangle(0, 400, 108, 108), 150 | background: { 151 | color: '#FFFFFF', 152 | includedInExport: true 153 | }, 154 | layers: (() => { 155 | let bg = new ShapePath({ 156 | name: 'bg', 157 | frame: new Rectangle(0, 0, 108, 108), 158 | shapeType: ShapePath.ShapeType.Rectangle, 159 | style: { 160 | fills: [ 161 | { color: '#2196F3' } 162 | ] 163 | } 164 | }); 165 | 166 | let grid = iconGridAndroidO.createNewInstance(); 167 | grid.name = 'grid'; 168 | grid.frame = new Rectangle(0, 0, 108, 108); 169 | 170 | return [bg, grid]; 171 | })() 172 | }); 173 | 174 | // ic_foreground 175 | const iconForeground = new SymbolMaster({ 176 | name: 'ic_foreground', 177 | frame: new Rectangle(158, 400, 108, 108), 178 | background: { 179 | color: '#999999', 180 | enabled: true, 181 | includedInExport: false, 182 | includedInInstance: false 183 | }, 184 | layers: (() => { 185 | let shadow = ShapePath.fromSVGPath('M32 64 70.52 57.5 70.52 49.19 108 87.31 108 108 75.97 108z'); 186 | shadow.name = 'shadow'; 187 | shadow.style.fills = [ 188 | { 189 | fillType: Style.FillType.Gradient, 190 | gradient: { 191 | gradientType: Style.GradientType.Linear, 192 | from: { x: 0.238, y: 0.115 }, 193 | to: { x: 1, y: 1 }, 194 | stops: [ 195 | { position: 0, color: '#00000026' }, 196 | { position: 0.142, color: '#00000026' }, 197 | { position: 0.643, color: '#00000005' }, 198 | { position: 1, color: '#00000000' } 199 | ] 200 | } 201 | } 202 | ]; 203 | 204 | let logoGroup = sketch.createLayerFromData(logoSVG, 'svg'); 205 | let logo = logoGroup.layers[0]; 206 | logo.name = 'logo'; 207 | logo.frame = new Rectangle(32, 38, 44, 26); 208 | 209 | let grid = iconGridAndroidO.createNewInstance(); 210 | grid.name = 'grid'; 211 | grid.frame = new Rectangle(0, 0, 108, 108); 212 | 213 | return [shadow, logo, grid]; 214 | })() 215 | }); 216 | 217 | // google_play_icon 218 | const googlePlayIcon = new SymbolMaster({ 219 | name: 'google_play_icon', 220 | frame: new Rectangle(0, 900, 512, 512), 221 | background: { 222 | color: '#FFFFFF', 223 | includedInExport: true 224 | }, 225 | layers: (() => { 226 | let mask = new ShapePath({ 227 | name: 'bg', 228 | frame: new Rectangle(0, 0, 512, 512), 229 | shapeType: ShapePath.ShapeType.Rectangle 230 | }); 231 | mask.sketchObject.setHasClippingMask(true); 232 | 233 | let background = iconBackground.createNewInstance(); 234 | background.frame = new Rectangle(-128, -128, 768, 768); 235 | 236 | let foreground = iconForeground.createNewInstance(); 237 | foreground.frame = new Rectangle(-128, -128, 768, 768); 238 | 239 | return [mask, background, foreground]; 240 | })() 241 | }); 242 | 243 | // preview 244 | const preview = new Artboard({ 245 | name: 'Preview', 246 | frame: new Rectangle(0, 0, 400, 300), 247 | layers: (() => { 248 | let legacyIconSymbol = launcherIconLegacy.createNewInstance(); 249 | legacyIconSymbol.name = 'ic_launcher'; 250 | legacyIconSymbol.frame = new Rectangle(0, 0, 108, 108); 251 | legacyIconSymbol.sketchObject.setScale(108/192); 252 | let legacyIcon = new Group({ 253 | name: 'Legacy App Icon', 254 | frame: new Rectangle(18, 40, 108, 108), 255 | layers: [legacyIconSymbol] 256 | }); 257 | 258 | let legacyIconRoundSymbol = launcherRoundIconLegacy.createNewInstance(); 259 | legacyIconRoundSymbol.name = 'ic_launcher_round'; 260 | legacyIconRoundSymbol.frame = new Rectangle(0, 0, 108, 108); 261 | legacyIconRoundSymbol.sketchObject.setScale(108/192); 262 | let legacyIconRound = new Group({ 263 | name: 'Legacy App Icon Round', 264 | frame: new Rectangle(146, 40, 108, 108), 265 | layers: [legacyIconRoundSymbol] 266 | }); 267 | 268 | let fullBleedBackground = iconBackground.createNewInstance(); 269 | fullBleedBackground.frame = new Rectangle(0, 0, 108, 108); 270 | let fullBleedForeground = iconForeground.createNewInstance(); 271 | fullBleedForeground.frame = new Rectangle(0, 0, 108, 108); 272 | let fullBleed = new Group({ 273 | name: 'Full Bleed Layers', 274 | frame: new Rectangle(274, 40, 108, 108), 275 | layers: [fullBleedBackground, fullBleedForeground] 276 | }); 277 | 278 | let circleMask = new ShapePath({ 279 | name: 'mask', 280 | frame: new Rectangle(0, 0, 72, 72), 281 | shapeType: ShapePath.ShapeType.Oval 282 | }); 283 | circleMask.sketchObject.setHasClippingMask(true); 284 | let circleBackground = iconBackground.createNewInstance(); 285 | circleBackground.frame = new Rectangle(-18, -18, 108, 108); 286 | let circleForeground = iconForeground.createNewInstance(); 287 | circleForeground.frame = new Rectangle(-18, -18, 108, 108); 288 | let circle = new Group({ 289 | name: 'Circle', 290 | frame: new Rectangle(26, 188, 72, 72), 291 | layers: [circleMask, circleBackground, circleForeground] 292 | }); 293 | 294 | let squircleMask = ShapePath.fromSVGPath('M36,0 C7.2,0 0,7.2 0,36 C0,64.8 7.2,72 36,72 C64.8,72 72,64.8 72,36 C72,7.2 64.8,0 36,0 Z'); 295 | squircleMask.name = 'mask'; 296 | squircleMask.frame = new Rectangle(0, 0, 72, 72); 297 | squircleMask.sketchObject.setHasClippingMask(true); 298 | let squircleBackground = iconBackground.createNewInstance(); 299 | squircleBackground.frame = new Rectangle(-18, -18, 108, 108); 300 | let squircleForeground = iconForeground.createNewInstance(); 301 | squircleForeground.frame = new Rectangle(-18, -18, 108, 108); 302 | let squircle = new Group({ 303 | name: 'Squircle', 304 | frame: new Rectangle(118, 188, 72, 72), 305 | layers: [squircleMask, squircleBackground, squircleForeground] 306 | }); 307 | 308 | let roundedSquareMask = new ShapePath({ 309 | name: 'mask', 310 | frame: new Rectangle(0, 0, 72, 72), 311 | shapeType: ShapePath.ShapeType.Rectangle 312 | }); 313 | roundedSquareMask.points.forEach(point => { 314 | point.cornerRadius = 22; 315 | }); 316 | roundedSquareMask.sketchObject.setHasClippingMask(true); 317 | let roundedSquareBackground = iconBackground.createNewInstance(); 318 | roundedSquareBackground.frame = new Rectangle(-18, -18, 108, 108); 319 | let roundedSquareForeground = iconForeground.createNewInstance(); 320 | roundedSquareForeground.frame = new Rectangle(-18, -18, 108, 108); 321 | let roundedSquare = new Group({ 322 | name: 'Rounded Square', 323 | frame: new Rectangle(210, 188, 72, 72), 324 | layers: [roundedSquareMask, roundedSquareBackground, roundedSquareForeground] 325 | }); 326 | 327 | let squareMask = new ShapePath({ 328 | name: 'mask', 329 | frame: new Rectangle(0, 0, 72, 72), 330 | shapeType: ShapePath.ShapeType.Rectangle 331 | }); 332 | squareMask.points.forEach(point => { 333 | point.cornerRadius = 8; 334 | }); 335 | squareMask.sketchObject.setHasClippingMask(true); 336 | let squareBackground = iconBackground.createNewInstance(); 337 | squareBackground.frame = new Rectangle(-18, -18, 108, 108); 338 | let squareForeground = iconForeground.createNewInstance(); 339 | squareForeground.frame = new Rectangle(-18, -18, 108, 108); 340 | let square = new Group({ 341 | name: 'Square', 342 | frame: new Rectangle(302, 188, 72, 72), 343 | layers: [squareMask, squareBackground, squareForeground] 344 | }); 345 | 346 | return [legacyIcon, legacyIconRound, fullBleed, circle, squircle, roundedSquare, square] 347 | })() 348 | }); 349 | 350 | const page = new Page({ 351 | name: 'App Icon', 352 | parent: document, 353 | selected: true, 354 | layers: [iconGrid, iconGridAndroidO, launcherIconLegacy, launcherRoundIconLegacy, iconBackground, iconForeground, googlePlayIcon, preview] 355 | }); 356 | settings.setLayerSettingForKey(page, 'is_android_app_icon_template', true); 357 | 358 | // Set override to none 359 | googlePlayIcon.layers[1].overrides[0].value = ''; 360 | googlePlayIcon.layers[2].overrides[0].value = ''; 361 | preview.layers[0].layers[0].overrides[0].value = ''; 362 | preview.layers[1].layers[0].overrides[0].value = ''; 363 | preview.layers[2].layers[0].overrides[0].value = ''; 364 | preview.layers[2].layers[1].overrides[0].value = ''; 365 | preview.layers[3].layers[1].overrides[0].value = ''; 366 | preview.layers[3].layers[2].overrides[0].value = ''; 367 | preview.layers[4].layers[1].overrides[0].value = ''; 368 | preview.layers[4].layers[2].overrides[0].value = ''; 369 | preview.layers[5].layers[1].overrides[0].value = ''; 370 | preview.layers[5].layers[2].overrides[0].value = ''; 371 | preview.layers[6].layers[1].overrides[0].value = ''; 372 | preview.layers[6].layers[2].overrides[0].value = ''; 373 | 374 | [ 375 | iconGrid, iconGridAndroidO, launcherIconLegacy, launcherRoundIconLegacy, 376 | iconBackground, iconForeground, googlePlayIcon, preview 377 | ].forEach(layer => { 378 | collapse(layer); 379 | }); 380 | 381 | } -------------------------------------------------------------------------------- /src/new_asset.js: -------------------------------------------------------------------------------- 1 | const sketch = require('sketch/dom'); 2 | const ui = require('sketch/ui'); 3 | const settings = require('sketch/settings'); 4 | 5 | const i18n = require('./lib/i18n'); 6 | const android = require('./lib/android'); 7 | const sk = require('./lib/sk'); 8 | 9 | export default function() { 10 | 11 | const document = sketch.getSelectedDocument(); 12 | const selection = document.selectedLayers; 13 | const identifier = String(__command.identifier()); 14 | 15 | if (selection.isEmpty) { 16 | ui.message(i18n('no_selection')); 17 | return; 18 | } 19 | 20 | let assetNameType = settings.settingForKey('asset_name_type') || 0; 21 | selection.layers.forEach(layer => { 22 | let format = 'png'; 23 | if (identifier === 'new_bitmap_asset') { 24 | format = 'png'; 25 | } 26 | if (identifier === 'new_vector_asset') { 27 | format = 'svg'; 28 | } 29 | let name = android.assetName(layer.name, assetNameType); 30 | newAsset(layer, name, format); 31 | }); 32 | 33 | } 34 | 35 | function newAsset(layer, name, format) { 36 | 37 | let exportFormats = [{ 38 | size: '1x', 39 | fileFormat: format 40 | }]; 41 | 42 | if (sk.isGroup(layer)) { 43 | // Group round to pixel 44 | sk.roundToPixel(layer); 45 | // Add slice into group 46 | sk.removeSliceInGroup(layer); 47 | sk.addSliceIntoGroup(layer, name, exportFormats); 48 | } else { 49 | // HotSpot layer 50 | if (sk.isHotspot(layer)) { 51 | ui.message(i18n('can_not_create_asset_from_hot_spot')); 52 | } 53 | 54 | // Slice layer 55 | else if (sk.isSlice(layer)) { 56 | if (sk.isGroup(layer.parent)) { 57 | sk.roundToPixel(layer); 58 | layer.name = name; 59 | layer.exportFormats = exportFormats; 60 | sk.exportGroupContentOnly(layer); 61 | } else { 62 | ui.message(i18n('can_not_create_asset_from_hot_spot')); 63 | } 64 | } 65 | 66 | else { 67 | let slice = sk.addSliceBeforeLayer(layer, name, exportFormats); 68 | if (!sk.isGroup(layer.parent)) { 69 | let group = sk.group([slice, layer]); 70 | group.name = name; 71 | } 72 | sk.exportGroupContentOnly(slice); 73 | } 74 | 75 | } 76 | } -------------------------------------------------------------------------------- /src/new_nine_patch_asset.js: -------------------------------------------------------------------------------- 1 | const sketch = require('sketch/dom'); 2 | const ui = require('sketch/ui'); 3 | const settings = require('sketch/settings'); 4 | const { Rectangle } = require('sketch/dom'); 5 | 6 | const i18n = require('./lib/i18n'); 7 | const android = require('./lib/android'); 8 | const sk = require('./lib/sk'); 9 | 10 | export default function() { 11 | 12 | const document = sketch.getSelectedDocument(); 13 | const selection = document.selectedLayers; 14 | 15 | if (selection.isEmpty) { 16 | ui.message(i18n('no_selection')); 17 | return; 18 | } 19 | 20 | let assetNameType = settings.settingForKey('asset_name_type') || 0; 21 | let exportFormats = [{ 22 | size: '1x', 23 | fileFormat: 'png' 24 | }]; 25 | 26 | selection.layers.forEach(layer => { 27 | let name = android.assetName(layer.name, assetNameType); 28 | let groupNinePatch; 29 | let groupContent; 30 | let groupPatch; 31 | if (sk.isArtboard(layer)) { 32 | ui.message(i18n('can_not_create_nine_patch_from_artboard_or_symbol_master')); 33 | } 34 | else if (sk.isSymbolMaster(layer)) { 35 | ui.message(i18n('can_not_create_nine_patch_from_artboard_or_symbol_master')); 36 | } 37 | else if (sk.isHotspot(layer)) { 38 | ui.message(i18n('can_not_create_asset_from_hot_spot')); 39 | } 40 | else if (sk.isSlice(layer)) { 41 | ui.message(i18n('can_not_create_asset_from_slice')); 42 | } 43 | else if (sk.isLayerGroup(layer)) { 44 | // No content and patch group 45 | if (sk.getLayerByNameFromParent('content', layer)) { 46 | groupContent = sk.getLayerByNameFromParent('content', layer); 47 | groupNinePatch = layer; 48 | } else { 49 | groupContent = layer; 50 | groupContent.name = 'content'; 51 | groupNinePatch = sk.group([groupContent]); 52 | groupNinePatch.name = name; 53 | } 54 | } 55 | else { 56 | groupContent = sk.group([layer]); 57 | groupContent.name = 'content'; 58 | groupNinePatch = sk.group([groupContent]); 59 | groupNinePatch.name = name; 60 | } 61 | 62 | sk.removeSliceInGroup(groupContent); 63 | 64 | if (sk.getLayerByNameFromParent('patch', groupNinePatch)) { 65 | let slice = sk.addSliceIntoGroup(groupNinePatch, '#9patch', exportFormats); 66 | slice.frame = new Rectangle( 67 | 1, 68 | 1, 69 | Math.ceil(groupNinePatch.frame.width) - 2, 70 | Math.ceil(groupNinePatch.frame.height) - 2 71 | ); 72 | sk.moveLayerIntoGroup(slice, groupContent); 73 | } else { 74 | let width = groupContent.frame.width; 75 | let height = groupContent.frame.height; 76 | let color = "#000000"; 77 | let patchTop = sk.addRectShape(groupNinePatch, {x: 0, y: -1, width, height: 1}, color, 'top'); 78 | let patchRight = sk.addRectShape(groupNinePatch, {x: width, y: 0, width: 1, height}, color, 'right'); 79 | let patchBottom = sk.addRectShape(groupNinePatch, {x: 0, y: height, width, height: 1}, color, 'bottom'); 80 | let patchLeft = sk.addRectShape(groupNinePatch, {x: -1, y: 0, width: 1, height}, color, 'left'); 81 | groupPatch = sk.group([patchLeft, patchBottom, patchRight, patchTop]); 82 | groupPatch.name = 'patch'; 83 | 84 | sk.addSliceIntoGroup(groupContent, '#9patch', exportFormats); 85 | } 86 | 87 | sk.fitGroup(groupNinePatch); 88 | sk.selectLayer(groupNinePatch); 89 | }); 90 | } -------------------------------------------------------------------------------- /src/preferences.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const settings = require('sketch/settings'); 3 | 4 | const i18n = require('./lib/i18n'); 5 | const android = require('./lib/android'); 6 | 7 | const html = require('../resources/preferences.html'); 8 | const webviewIdentifier = 'preferences.webview'; 9 | 10 | export default function() { 11 | 12 | const preferences = { 13 | 'export_dpi': settings.settingForKey('export_dpi') || Object.keys(android.DPIS), 14 | 'asset_name_type': settings.settingForKey('asset_name_type') || 0, 15 | 'vector_drawable_folder': settings.settingForKey('vector_drawable_folder') || 2, 16 | 'reveal_in_finder_after_export': settings.settingForKey('reveal_in_finder_after_export') || false, 17 | 'webp_quality': settings.globalSettingForKey('WebPQuality'), 18 | 'available_asset_name_type': [ 19 | i18n('asset_name_type_0'), 20 | i18n('asset_name_type_1'), 21 | i18n('asset_name_type_2'), 22 | i18n('asset_name_type_3'), 23 | ], 24 | 'available_folders': android.VECTORDRAWABLE_FOLDERS, 25 | 'version': String(__command.pluginBundle().version()), 26 | 'i18n': {} 27 | }; 28 | 29 | [ 30 | 'export_dpis', 'asset_name_type', 'vector_drawable_folder', 'others', 31 | 'reveal_in_finder_after_export', 'webp_quality', 'ok', 'cancel' 32 | ].forEach(key => { 33 | preferences.i18n[key] = i18n(key); 34 | }); 35 | 36 | const options = { 37 | identifier: webviewIdentifier, 38 | width: 400, 39 | height: 600, 40 | show: false, 41 | title: i18n('preferences'), 42 | resizable: false, 43 | minimizable: false, 44 | remembersWindowFrame: true, 45 | acceptsFirstMouse: true, 46 | alwaysOnTop: true 47 | }; 48 | 49 | const browserWindow = new BrowserWindow(options); 50 | 51 | browserWindow.once('ready-to-show', () => { 52 | browserWindow.show(); 53 | }); 54 | 55 | const webContents = browserWindow.webContents; 56 | 57 | // Main 58 | webContents.on('did-finish-load', () => { 59 | webContents.executeJavaScript(`main('${JSON.stringify(preferences)}')`); 60 | }); 61 | 62 | // Save 63 | webContents.on('save', (json) => { 64 | const preferences = JSON.parse(json); 65 | 66 | settings.setSettingForKey('export_dpi', preferences.export_dpi); 67 | settings.setSettingForKey('asset_name_type', preferences.asset_name_type); 68 | settings.setSettingForKey('vector_drawable_folder', preferences.vector_drawable_folder); 69 | settings.setSettingForKey('reveal_in_finder_after_export', preferences.reveal_in_finder_after_export); 70 | settings.setGlobalSettingForKey('WebPQuality', preferences.webp_quality); 71 | 72 | browserWindow.close(); 73 | }); 74 | 75 | // Close 76 | webContents.on('cancel', () => { 77 | browserWindow.close(); 78 | }); 79 | 80 | browserWindow.loadURL(html); 81 | } -------------------------------------------------------------------------------- /src/view_color_code_from_color_variables.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const { getWebview, sendToWebview } = require('sketch-module-web-view/remote'); 3 | const sketch = require('sketch/dom'); 4 | const ui = require('sketch/ui'); 5 | const settings = require('sketch/settings'); 6 | 7 | const i18n = require('./lib/i18n'); 8 | const android = require('./lib/android'); 9 | const { pasteboardCopy, saveToFolder, writeContentToFile, revealInFinder } = require('./lib/fs'); 10 | 11 | const html = require('../resources/view_code.html'); 12 | const webviewIdentifier = 'view_color_code_from_color_variables.webview'; 13 | 14 | const document = sketch.getSelectedDocument(); 15 | const selection = document.selectedLayers; 16 | const assetNameType = settings.settingForKey('asset_name_type') || 0; 17 | 18 | export default function() { 19 | 20 | if (document.swatches.length === 0) { 21 | ui.message(i18n('no_color_variables')); 22 | return; 23 | } 24 | 25 | let colors = colorsFromDocument(); 26 | 27 | const options = { 28 | identifier: webviewIdentifier, 29 | width: 600, 30 | height: 400, 31 | show: false, 32 | title: i18n('color_xml_from_color_variables'), 33 | resizable: false, 34 | minimizable: false, 35 | remembersWindowFrame: true, 36 | acceptsFirstMouse: true, 37 | alwaysOnTop: true 38 | }; 39 | 40 | const browserWindow = new BrowserWindow(options); 41 | 42 | browserWindow.once('ready-to-show', () => { 43 | browserWindow.show(); 44 | }); 45 | 46 | const webContents = browserWindow.webContents; 47 | 48 | // Main 49 | webContents.on('did-finish-load', () => { 50 | const xml = colorsToXml(colors); 51 | const langs = {}; 52 | ['save', 'cancel', 'copy'].forEach(key => langs[key] = i18n(key)); 53 | webContents.executeJavaScript(`main('${xml}', '${JSON.stringify(langs)}')`); 54 | }); 55 | 56 | // Copy 57 | webContents.on('copy', xml => { 58 | pasteboardCopy(xml); 59 | ui.message(i18n('copied')); 60 | }); 61 | 62 | // Save 63 | webContents.on('save', xml => { 64 | let filePath = saveToFolder(''); 65 | if (!/\.xml$/i.test(filePath)) { 66 | filePath += '.xml'; 67 | } 68 | const dir = writeContentToFile(filePath, xml); 69 | if (dir) { 70 | browserWindow.close(); 71 | if (settings.settingForKey('reveal_in_finder_after_export')) { 72 | revealInFinder(dir); 73 | } 74 | } else { 75 | ui.message(i18n('no_permission')); 76 | } 77 | }); 78 | 79 | // Close 80 | webContents.on('cancel', () => { 81 | browserWindow.close(); 82 | }); 83 | 84 | browserWindow.loadURL(html); 85 | }; 86 | 87 | export function onShutdown() { 88 | const existingWebview = getWebview(webviewIdentifier); 89 | if (existingWebview) { 90 | existingWebview.close(); 91 | } 92 | }; 93 | 94 | function colorsFromDocument() { 95 | let colors = {} 96 | let namesAndCount = {}; 97 | document.swatches.forEach(swatch => { 98 | let originalName = /^[A-E0-9]{6}$/.test(swatch.name) ? 'color_' + swatch.name : swatch.name; 99 | let name = android.assetName(originalName, assetNameType, 'color'); 100 | if (Object.keys(namesAndCount).includes(name)) { 101 | namesAndCount[name] += 1; 102 | } else { 103 | namesAndCount[name] = 1; 104 | } 105 | if (namesAndCount[name] > 1) { 106 | name += '_' + namesAndCount[name]; 107 | } 108 | colors[name] = android.colorToAndroid(swatch.color); 109 | }); 110 | return colors; 111 | } 112 | 113 | function colorsToXml(colors) { 114 | let xml = '\\n'; 115 | Object.keys(colors).forEach(key => { 116 | xml += ' ' + colors[key] + '\\n'; 117 | }); 118 | xml += ''; 119 | return xml; 120 | } 121 | -------------------------------------------------------------------------------- /src/view_color_code_from_selected_layers.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const { getWebview, sendToWebview } = require('sketch-module-web-view/remote'); 3 | const sketch = require('sketch/dom'); 4 | const ui = require('sketch/ui'); 5 | const settings = require('sketch/settings'); 6 | 7 | const i18n = require('./lib/i18n'); 8 | const android = require('./lib/android'); 9 | const { pasteboardCopy, saveToFolder, writeContentToFile, revealInFinder } = require('./lib/fs'); 10 | 11 | const html = require('../resources/view_code.html'); 12 | const webviewIdentifier = 'view_color_code_from_selected_layers.webview'; 13 | 14 | const document = sketch.getSelectedDocument(); 15 | const selection = document.selectedLayers; 16 | const assetNameType = settings.settingForKey('asset_name_type') || 0; 17 | 18 | export default function() { 19 | 20 | let colors = selection.isEmpty ? [] : colorsFromSelectedLayers(); 21 | 22 | const options = { 23 | identifier: webviewIdentifier, 24 | width: 600, 25 | height: 400, 26 | show: false, 27 | title: i18n('color_xml_from_layers'), 28 | resizable: false, 29 | minimizable: false, 30 | remembersWindowFrame: true, 31 | acceptsFirstMouse: true, 32 | alwaysOnTop: true 33 | }; 34 | 35 | const browserWindow = new BrowserWindow(options); 36 | 37 | browserWindow.once('ready-to-show', () => { 38 | browserWindow.show(); 39 | }); 40 | 41 | const webContents = browserWindow.webContents; 42 | 43 | // Main 44 | webContents.on('did-finish-load', () => { 45 | const xml = colorsToXml(colors); 46 | const langs = {}; 47 | ['save', 'cancel', 'copy'].forEach(key => langs[key] = i18n(key)); 48 | webContents.executeJavaScript(`main('${xml}', '${JSON.stringify(langs)}')`); 49 | }); 50 | 51 | // Copy 52 | webContents.on('copy', xml => { 53 | pasteboardCopy(xml); 54 | ui.message(i18n('copied')); 55 | }); 56 | 57 | // Save 58 | webContents.on('save', xml => { 59 | let filePath = saveToFolder(''); 60 | if (!/\.xml$/i.test(filePath)) { 61 | filePath += '.xml'; 62 | } 63 | const dir = writeContentToFile(filePath, xml); 64 | if (dir) { 65 | browserWindow.close(); 66 | if (settings.settingForKey('reveal_in_finder_after_export')) { 67 | revealInFinder(dir); 68 | } 69 | } else { 70 | ui.message(i18n('no_permission')); 71 | } 72 | }); 73 | 74 | // Close 75 | webContents.on('cancel', () => { 76 | browserWindow.close(); 77 | }); 78 | 79 | browserWindow.loadURL(html); 80 | }; 81 | 82 | export function onShutdown() { 83 | const existingWebview = getWebview(webviewIdentifier); 84 | if (existingWebview) { 85 | existingWebview.close(); 86 | } 87 | }; 88 | 89 | export function onSelectionChanged() { 90 | const existingWebview = getWebview(webviewIdentifier); 91 | if (existingWebview) { 92 | const colors = colorsFromSelectedLayers(); 93 | const xml = colorsToXml(colors); 94 | sendToWebview(webviewIdentifier, `main('${xml}')`); 95 | } 96 | }; 97 | 98 | function colorsFromSelectedLayers() { 99 | let colors = {} 100 | let namesAndCount = {}; 101 | selection.layers.forEach(layer => { 102 | let name = android.assetName(layer.name, assetNameType); 103 | if (Object.keys(namesAndCount).includes(name)) { 104 | namesAndCount[name] += 1; 105 | } else { 106 | namesAndCount[name] = 1; 107 | } 108 | if (namesAndCount[name] > 1) { 109 | name += '_' + namesAndCount[name]; 110 | } 111 | // Last enabled fill 112 | let fill = layer.style.fills.filter(fill => fill.enabled).pop(); 113 | if (fill) { 114 | if (fill.fillType === 'Color') { 115 | colors[name] = android.colorToAndroid(fill.color); 116 | } 117 | if (fill.fillType === 'Gradient') { 118 | fill.gradient.stops.forEach((stop, idx) => { 119 | colors[name + '_gradient_stop_' + idx] = android.colorToAndroid(stop.color); 120 | }); 121 | } 122 | } 123 | // Text layer 124 | if (layer.type === 'Text') { 125 | colors['text_' + name] = android.colorToAndroid(layer.style.textColor); 126 | } 127 | }); 128 | return colors; 129 | } 130 | 131 | function colorsToXml(colors) { 132 | let xml = '\\n'; 133 | Object.keys(colors).forEach(key => { 134 | xml += ' ' + colors[key] + '\\n'; 135 | }); 136 | xml += ''; 137 | return xml; 138 | } 139 | -------------------------------------------------------------------------------- /src/view_nine_patch.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const sketch = require('sketch/dom'); 3 | const ui = require('sketch/ui'); 4 | const util = require('util'); 5 | 6 | const i18n = require('./lib/i18n'); 7 | const sk = require('./lib/sk'); 8 | 9 | export default function() { 10 | 11 | const document = sketch.getSelectedDocument(); 12 | const selection = document.selectedLayers; 13 | 14 | if (selection.length !== 1) { 15 | ui.message(i18n('select_one_nine_patch_asset')); 16 | return; 17 | } 18 | 19 | // Checking nine-patch 20 | const layer = selection.layers[0]; 21 | let ninePatch; 22 | if (sk.isGroup(layer)) { 23 | if (layer.layers.length === 2 && sk.getLayerByNameFromParent('patch', layer) && sk.getLayerByNameFromParent('content', layer)) { 24 | let patch = sk.getLayerByNameFromParent('patch', layer); 25 | let content = sk.getLayerByNameFromParent('content', layer); 26 | let slice = sk.getLayerByNameFromParent('#9patch', content); 27 | if (patch.layers.length >= 4 && slice && sk.isSlice(slice)) { 28 | ninePatch = layer; 29 | } else { 30 | ui.message(i18n('nine_patch_layer_structure_is_wrong')); 31 | return; 32 | } 33 | } else { 34 | ui.message(i18n('select_one_nine_patch_asset')); 35 | return; 36 | } 37 | } 38 | if (!ninePatch) { 39 | ui.message(i18n('select_one_nine_patch_asset')); 40 | return; 41 | } 42 | 43 | // Nine-patch width and height 44 | let ninePatchWidth = (ninePatch.frame.width - 2) * 2; 45 | let ninePatchHeight = (ninePatch.frame.height - 2) * 2; 46 | 47 | // Get base64 code of nine-patch asset 48 | let base64 = sketch.export(ninePatch, {output: false, scales: '2', formats: 'png'}).toString('base64'); 49 | 50 | const options = { 51 | identifier: 'view_nine_patch.webview', 52 | width: 600, 53 | height: 400, 54 | show: false, 55 | title: i18n('view_nine_patch'), 56 | resizable: false, 57 | minimizable: false, 58 | remembersWindowFrame: true, 59 | acceptsFirstMouse: true, 60 | alwaysOnTop: true 61 | }; 62 | 63 | const browserWindow = new BrowserWindow(options); 64 | 65 | browserWindow.once('ready-to-show', () => { 66 | browserWindow.show(); 67 | }); 68 | 69 | const webContents = browserWindow.webContents; 70 | 71 | // Main 72 | webContents.on('did-finish-load', () => { 73 | const langs = {}; 74 | [ 75 | 'tip_bg_light', 'tip_bg_dark', 'tip_bg_white', 76 | 'width', 'height', 'content', 'export', 'cancel' 77 | ].forEach(key => langs[key] = i18n(key)); 78 | webContents.executeJavaScript(`main('${base64}', ${ninePatchWidth}, ${ninePatchHeight}, '${JSON.stringify(langs)}')`); 79 | }); 80 | 81 | // Save 82 | webContents.on('export', xml => { 83 | // TODO: Export 84 | // let filePath = saveToFolder(''); 85 | // writeContentToFile(filePath, xml); 86 | // ui.message(i18n('export_done')); 87 | }); 88 | 89 | // Close 90 | webContents.on('cancel', () => { 91 | browserWindow.close(); 92 | }); 93 | 94 | browserWindow.loadURL(require('../resources/view_nine_patch.html')); 95 | }; -------------------------------------------------------------------------------- /src/view_shape_code.js: -------------------------------------------------------------------------------- 1 | const BrowserWindow = require('sketch-module-web-view'); 2 | const { getWebview, sendToWebview } = require('sketch-module-web-view/remote'); 3 | const sketch = require('sketch/dom'); 4 | const ui = require('sketch/ui'); 5 | const util = require('util'); 6 | 7 | const i18n = require('./lib/i18n'); 8 | const android = require('./lib/android'); 9 | const { pasteboardCopy, saveToFolder, writeContentToFile, revealInFinder } = require('./lib/fs'); 10 | 11 | const html = require('../resources/view_code.html'); 12 | const webviewIdentifier = 'view_shape_code.webview'; 13 | 14 | const document = sketch.getSelectedDocument(); 15 | const selection = document.selectedLayers; 16 | 17 | export default function() { 18 | 19 | const options = { 20 | identifier: webviewIdentifier, 21 | width: 600, 22 | height: 400, 23 | show: false, 24 | title: i18n('view_shape_drawable_from_selected_layer'), 25 | resizable: false, 26 | minimizable: false, 27 | remembersWindowFrame: true, 28 | acceptsFirstMouse: true, 29 | alwaysOnTop: true 30 | }; 31 | 32 | const browserWindow = new BrowserWindow(options); 33 | 34 | browserWindow.once('ready-to-show', () => { 35 | browserWindow.show(); 36 | }); 37 | 38 | const webContents = browserWindow.webContents; 39 | 40 | // Main 41 | webContents.on('did-finish-load', () => { 42 | 43 | const layer = selection.layers[0]; 44 | let xml = ''; 45 | if (layer) { 46 | const layerInfo = getLayerInfo(layer.sketchObject); 47 | if (layerInfo.support === false) { 48 | ui.message(i18n(layerInfo.msg)); 49 | } else { 50 | xml = xmlFromLayerInfo(layerInfo); 51 | } 52 | } 53 | const langs = {}; 54 | ['save', 'cancel', 'copy'].forEach(key => langs[key] = i18n(key)); 55 | webContents.executeJavaScript(`main('${xml}', '${JSON.stringify(langs)}')`); 56 | }); 57 | 58 | // Copy 59 | webContents.on('copy', xml => { 60 | pasteboardCopy(xml); 61 | ui.message(i18n('copied')); 62 | }); 63 | 64 | // Save 65 | webContents.on('save', xml => { 66 | let filePath = saveToFolder(''); 67 | if (!/\.xml$/i.test(filePath)) { 68 | filePath += '.xml'; 69 | } 70 | const dir = writeContentToFile(filePath, xml); 71 | if (dir) { 72 | browserWindow.close(); 73 | if (settings.settingForKey('reveal_in_finder_after_export')) { 74 | revealInFinder(dir); 75 | } 76 | } else { 77 | ui.message(i18n('no_permission')); 78 | } 79 | }); 80 | 81 | // Close 82 | webContents.on('cancel', () => { 83 | browserWindow.close(); 84 | }); 85 | 86 | browserWindow.loadURL(html); 87 | }; 88 | 89 | export function onShutdown() { 90 | const existingWebview = getWebview(webviewIdentifier); 91 | if (existingWebview) { 92 | existingWebview.close(); 93 | } 94 | }; 95 | 96 | export function onSelectionChanged() { 97 | const existingWebview = getWebview(webviewIdentifier); 98 | if (existingWebview) { 99 | let xml = ''; 100 | if (selection.length === 1) { 101 | const layer = selection.layers[0]; 102 | const layerInfo = getLayerInfo(layer.sketchObject); 103 | if (layerInfo.support === false) { 104 | ui.message(i18n(layerInfo.msg)); 105 | } else { 106 | xml = xmlFromLayerInfo(layerInfo); 107 | } 108 | } 109 | sendToWebview(webviewIdentifier, `main('${xml}')`); 110 | } 111 | 112 | }; 113 | 114 | // XML from layer 115 | // Android Shape Drawable References 116 | // https://developer.android.com/guide/topics/resources/drawable-resource.html#Shape 117 | function getLayerInfo(layer) { 118 | var result = {}; 119 | if ( 120 | layer.class() == "MSShapeGroup" || 121 | layer.class() == "MSRectangleShape" || 122 | layer.class() == "MSOvalShape" 123 | ) { 124 | // Not support layer style 125 | if ( 126 | layer.style().hasEnabledShadow() || 127 | layer.style().enabledInnerShadows().count() > 0 || 128 | layer.style().blur().isEnabled() 129 | ) { 130 | result.support = false; 131 | result.msg = "not_support_layer_style"; 132 | return result; 133 | } 134 | 135 | // Fills 136 | var fills = layer.style().enabledFills(); 137 | if (fills.count() > 0) { 138 | if (fills.lastObject().fillType() == 0) { 139 | result.support = true; 140 | result.solid = android.mscolorToAndroid(fills.lastObject().color()); 141 | } else if (fills.lastObject().fillType() == 1) { 142 | result.support = true; 143 | var gradient = fills.lastObject().gradient(); 144 | if (gradient.stops().count() < 4) { 145 | 146 | // Gradient type 147 | var gradientType = gradient.gradientType(); 148 | switch (gradientType) { 149 | case 2: 150 | result.gradientType = "sweep"; 151 | break; 152 | case 1: 153 | result.gradientType = "radial"; 154 | result.gradientRadius = Math.round(layer.frame().width() / 2) + "dp"; 155 | break; 156 | case 0: 157 | result.gradientType = "linear"; 158 | break; 159 | default: 160 | result.gradientType = "linear"; 161 | } 162 | 163 | // Gradient stops 164 | var sortByPosition = NSSortDescriptor.sortDescriptorWithKey_ascending("position", true); 165 | gradient.stops().sortUsingDescriptors(NSArray.arrayWithObject(sortByPosition)); 166 | result.gradientStops = util.toArray(gradient.stops()).map(stop => { 167 | return android.mscolorToAndroid(stop.color()); 168 | }); 169 | 170 | // Sweep 171 | if (result.gradientType == "sweep" && gradient.stops().count() == 2) { 172 | var pos1 = gradient.stops().firstObject().position(); 173 | var pos2 = gradient.stops().lastObject().position(); 174 | var pos1Color = android.mscolorToAndroid(gradient.stops().firstObject().color()); 175 | var pos2Color = android.mscolorToAndroid(gradient.stops().lastObject().color()); 176 | if (pos1 > 0.3 && pos2 > 0.7) { 177 | result.gradientStops = [pos1Color].concat(result.gradientStops); 178 | } 179 | if (pos1 < 0.3 && pos2 < 0.7) { 180 | result.gradientStops.push(pos2Color); 181 | } 182 | } 183 | 184 | // Gradient angle 185 | if (result.gradientType == "linear") { 186 | var x1 = gradient.from().x * layer.frame().width(); 187 | var y1 = gradient.from().y * layer.frame().height(); 188 | var x2 = gradient.to().x * layer.frame().width(); 189 | var y2 = gradient.to().y * layer.frame().height(); 190 | var angle = Math.round(Math.atan(Math.abs(y1 - y2) / Math.abs(x1 - x2)) * 180 / Math.PI / 45) * 45; 191 | switch (true) { 192 | case x1 > x2 && y1 > y2: 193 | result.gradientAngle = angle + 90; 194 | break; 195 | case x1 >= x2 && y1 <= y2: 196 | result.gradientAngle = angle + 180; 197 | break; 198 | case x1 < x2 && y1 < y2: 199 | result.gradientAngle = angle + 275; 200 | break; 201 | default: 202 | // x1 <= x2 && y1 >= y2: 203 | result.gradientAngle = angle; 204 | } 205 | } 206 | 207 | } else { 208 | result.support = false; 209 | result.msg = "too_many_color_stop"; 210 | return result; 211 | } 212 | 213 | } else { 214 | result.support = false; 215 | result.msg = "not_support_fill_type"; 216 | return result; 217 | } 218 | } else { 219 | result.support = true; 220 | } 221 | 222 | // borders 223 | var borders = layer.style().enabledBorders(); 224 | if (borders.count() > 0) { 225 | if (borders.lastObject().fillType() == 0) { 226 | result.strokeWidth = borders.lastObject().thickness() + "dp"; 227 | result.strokeColor = android.mscolorToAndroid(borders.lastObject().color()); 228 | 229 | var dashPattern = layer.style().borderOptions().dashPattern(); 230 | if (dashPattern.count() > 0) { 231 | result.strokeDashWidth = dashPattern.firstObject() + "dp"; 232 | if (dashPattern.count() == 1) { 233 | result.strokeDashGap = dashPattern.firstObject() + "dp"; 234 | } else { 235 | result.strokeDashGap = dashPattern.objectAtIndex(1) + "dp"; 236 | } 237 | } 238 | 239 | } else { 240 | result.support = false; 241 | result.msg = "not_support_stroke_type"; 242 | return result; 243 | } 244 | } 245 | 246 | if (layer.children().count() == 2 || layer.children().count() == 1) { 247 | 248 | var shapePath; 249 | if (sketch.version.sketch >= 52) { 250 | shapePath = layer; 251 | } else if (sketch.version.sketch >= 49) { 252 | shapePath = layer.children().lastObject(); 253 | } else { 254 | shapePath = layer.children().firstObject(); 255 | } 256 | 257 | if (shapePath.class() == "MSRectangleShape" || shapePath.class() == "MSOvalShape") { 258 | 259 | if (result.support) { 260 | if (shapePath.class() == "MSRectangleShape") { 261 | result.type = "rectangle" 262 | 263 | // Radius 264 | var points; 265 | if (sketch.version.sketch >= 49) { 266 | points = shapePath.points(); 267 | } else { 268 | points = shapePath.path().points(); 269 | } 270 | 271 | var radius = 0, 272 | radiusTopLeft = Math.round(points.objectAtIndex(0).cornerRadius()), 273 | radiusTopRight = Math.round(points.objectAtIndex(1).cornerRadius()), 274 | radiusBottomRight = Math.round(points.objectAtIndex(2).cornerRadius()), 275 | radiusBottomLeft = Math.round(points.objectAtIndex(3).cornerRadius()); 276 | if ( 277 | radiusTopLeft == radiusTopRight && 278 | radiusTopLeft == radiusBottomRight && 279 | radiusTopLeft == radiusBottomLeft 280 | ) { 281 | radius = radiusTopLeft; 282 | } else { 283 | result.cornersRadiusTopLeft = radiusTopLeft + "dp"; 284 | result.cornersRadiusTopRight = radiusTopRight + "dp"; 285 | result.cornersRadiusBottomRight = radiusBottomRight + "dp"; 286 | result.cornersRadiusBottomLeft = radiusBottomLeft + "dp"; 287 | } 288 | if (radius != 0) { 289 | result.cornersRadius = radius + "dp"; 290 | } 291 | } else { 292 | result.type = "oval"; 293 | } 294 | } 295 | 296 | } else { 297 | result.support = false; 298 | result.msg = "not_support_shape"; 299 | return result; 300 | } 301 | } else if (layer.children().count() == 3) { 302 | 303 | if ( 304 | layer.children().objectAtIndex(0).class() == "MSOvalShape" && 305 | layer.children().objectAtIndex(1).class() == "MSOvalShape" 306 | ) { 307 | result.support = true; 308 | result.type = "ring"; 309 | 310 | var diameter1 = layer.children().objectAtIndex(0).frame().width(), 311 | diameter2 = layer.children().objectAtIndex(1).frame().width(); 312 | 313 | result.thickness = Math.abs(diameter1 - diameter2) + "dp"; 314 | result.innerRadius = Math.min(diameter1, diameter2) + "dp"; 315 | 316 | } else { 317 | result.support = false; 318 | result.msg = "not_support_shape"; 319 | return result; 320 | } 321 | 322 | } else { 323 | result.support = false; 324 | result.msg = "not_support_shape"; 325 | return result; 326 | } 327 | } else { 328 | result.support = false; 329 | result.msg = "no_shape_layer"; 330 | return result; 331 | } 332 | 333 | // Size 334 | result.width = Math.round(layer.frame().width()) + "dp"; 335 | result.height = Math.round(layer.frame().height()) + "dp"; 336 | 337 | return result; 338 | } 339 | 340 | function xmlFromLayerInfo(layerInfo) { 341 | let xml = `\\n\\n\\n`; 351 | } 352 | 353 | if (layerInfo.solid) { 354 | xml += ` \\n`; 355 | } 356 | 357 | if (layerInfo.gradientType) { 358 | xml += ` \\n`; 378 | } 379 | 380 | if (layerInfo.cornersRadiusTopLeft || layerInfo.cornersRadiusTopRight || layerInfo.cornersRadiusBottomRight || layerInfo.cornersRadiusBottomLeft) { 381 | xml += ' langs[key] = i18n(key)); 19 | const addXml = settings.settingForKey('add_xml_declaration') || false; 20 | const tint = settings.settingForKey('tint') || false; 21 | const defaultTint = settings.settingForKey('tint_color') || '000000'; 22 | const defaultAlpha = settings.settingForKey('tint_color_alpha') || 100; 23 | 24 | export default function() { 25 | 26 | const options = { 27 | identifier: 'view_vector_drawable_code.webview', 28 | width: 600, 29 | height: 400, 30 | show: false, 31 | title: i18n('view_vector_drawable_from_selected_layer'), 32 | resizable: false, 33 | minimizable: false, 34 | remembersWindowFrame: true, 35 | acceptsFirstMouse: true, 36 | alwaysOnTop: true 37 | }; 38 | 39 | const browserWindow = new BrowserWindow(options); 40 | 41 | // only show the window when the page has loaded to avoid a white flash 42 | browserWindow.once('ready-to-show', () => { 43 | browserWindow.show(); 44 | }); 45 | 46 | const webContents = browserWindow.webContents; 47 | 48 | // page loads 49 | webContents.on('did-finish-load', () => { 50 | let svg = ''; 51 | if (selection.length === 1) { 52 | const layer = selection.layers[0]; 53 | if (isSupported(layer)) { 54 | svg = sk.getSVGFromLayer(layer); 55 | }; 56 | } 57 | webContents.executeJavaScript(`main('${svg}', '${JSON.stringify(langs)}', ${addXml}, ${tint}, '${defaultTint}', ${defaultAlpha})`); 58 | }); 59 | 60 | // Save 61 | webContents.on('saveCode', xml => { 62 | let filePath = saveToFolder(''); 63 | if (!/\.xml$/i.test(filePath)) { 64 | filePath += '.xml'; 65 | } 66 | const dir = writeContentToFile(filePath, xml); 67 | if (dir) { 68 | browserWindow.close(); 69 | if (settings.settingForKey('reveal_in_finder_after_export')) { 70 | revealInFinder(dir); 71 | } 72 | } else { 73 | ui.message(i18n('no_permission')); 74 | } 75 | }); 76 | 77 | // Copy 78 | webContents.on('copyCode', xml => { 79 | pasteboardCopy(xml); 80 | ui.message(i18n('copied')); 81 | }); 82 | 83 | // Close 84 | webContents.on('cancel', () => { 85 | browserWindow.close(); 86 | }); 87 | 88 | webContents.on('add_xml_declaration', checked => { 89 | settings.setSettingForKey('add_xml_declaration', checked); 90 | }); 91 | 92 | webContents.on('tint_color', value => { 93 | settings.setSettingForKey('tint_color', value); 94 | }); 95 | 96 | webContents.on('tint_color_alpha', value => { 97 | settings.setSettingForKey('tint_color_alpha', parseInt(value)); 98 | }); 99 | 100 | webContents.on('tint', value => { 101 | settings.setSettingForKey('tint', value); 102 | }); 103 | 104 | browserWindow.loadURL(html); 105 | }; 106 | 107 | // When the plugin is shutdown by Sketch (for example when the user disable the plugin) 108 | // we need to close the webview if it's open 109 | export function onShutdown() { 110 | const existingWebview = getWebview(webviewIdentifier); 111 | if (existingWebview) { 112 | existingWebview.close(); 113 | } 114 | }; 115 | 116 | export function onSelectionChanged() { 117 | const existingWebview = getWebview(webviewIdentifier); 118 | if (existingWebview) { 119 | let svg = ''; 120 | if (selection.length === 1) { 121 | const layer = selection.layers[0]; 122 | if (isSupported(layer)) { 123 | svg = sk.getSVGFromLayer(layer); 124 | }; 125 | } else { 126 | if (selection.length > 1) { 127 | ui.message(i18n('select_one_layer')); 128 | } 129 | } 130 | sendToWebview(webviewIdentifier, `main('${svg}', '${JSON.stringify(langs)}', ${addXml}, ${tint}, '${defaultTint}', ${defaultAlpha})`); 131 | } 132 | }; 133 | 134 | function isSupported(layer) { 135 | if (layer.hidden) { 136 | ui.message(i18n('hidden_layer')); 137 | return false; 138 | } 139 | 140 | if (layer.frame.width > 200 || layer.frame.height > 200) { 141 | ui.message(i18n('vector_drawable_limit')); 142 | return false; 143 | } 144 | 145 | if (sk.countChildOfLayer(layer) > 20) { 146 | ui.message(i18n('vector_drawable_too_many_layer')); 147 | return false; 148 | } 149 | 150 | for (let child of sk.recursivelyChildOfLayer(layer)) { 151 | if (sk.isImage(child) && !layer.hidden) { 152 | ui.message(i18n('vector_drawable_not_support_bitmap_layer')); 153 | return false; 154 | } 155 | if ((sk.hasShadow(child) || sk.hasInnerShadow(child)) && !layer.hidden) { 156 | ui.message(i18n('vector_drawable_not_support_shadow')); 157 | return false; 158 | } 159 | if (sk.hasBlur(child) && !layer.hidden) { 160 | ui.message(i18n('vector_drawable_not_support_blur')); 161 | return false; 162 | } 163 | } 164 | 165 | return true; 166 | } 167 | -------------------------------------------------------------------------------- /webpack.skpm.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config, entry) { 2 | config.devtool = 'none'; 3 | config.node = entry.isPluginCommand ? false : { 4 | setImmediate: false 5 | }; 6 | config.mode = 'production'; 7 | config.module.rules.push({ 8 | test: /\.(html)$/, 9 | use: [{ 10 | loader: "@skpm/extract-loader", 11 | }, 12 | { 13 | loader: "html-loader", 14 | options: { 15 | attrs: [ 16 | 'img:src', 17 | 'link:href' 18 | ], 19 | interpolate: true, 20 | }, 21 | }, 22 | ] 23 | }); 24 | config.module.rules.push({ 25 | test: /\.(css)$/, 26 | use: [{ 27 | loader: "@skpm/extract-loader", 28 | }, 29 | { 30 | loader: "css-loader", 31 | }, 32 | ] 33 | }); 34 | } 35 | --------------------------------------------------------------------------------