├── settings.gradle ├── README_Assets ├── flr-install.png └── flr-usage-example.gif ├── src └── main │ ├── resources │ ├── icons │ │ ├── flr.png │ │ ├── flr_13.png │ │ ├── flr_16.png │ │ ├── flr_13@2x.png │ │ └── flr_16@2x.png │ └── META-INF │ │ ├── plugin.xml │ │ └── pluginIcon.svg │ └── java │ └── com │ └── flr │ ├── toolWindow │ ├── FlrToolWindowProvider.java │ ├── FlrToolWindowProvider.form │ └── FlrToolWindowFactory.java │ ├── FlrException.java │ ├── logConsole │ ├── FlrColoredLogEntity.java │ └── FlrLogConsole.java │ ├── actions │ ├── FlrVersionAction.java │ ├── FlrRecommendAction.java │ ├── FlrInitAction.java │ ├── FlrMonitorAction.java │ └── FlrGenerateAction.java │ ├── FlrConstant.java │ ├── messageBox │ └── FlrMessageBox.java │ ├── FlrApp.java │ ├── command │ ├── FlrListener.java │ ├── util │ │ ├── FlrUtil.java │ │ ├── FlrAssetUtil.java │ │ ├── FlrFileUtil.java │ │ └── FlrCodeUtil.java │ └── FlrChecker.java │ └── pubspecFileTree │ └── FlrPubspecFileTree.java ├── Docs ├── flr_plugin_icon_design.sketch ├── flr_plugin_workflow │ ├── assets │ │ ├── src │ │ │ ├── gitflow-model.src.key │ │ │ └── flr-plugin-workflow-model.key │ │ ├── flr-plugin-workflow-model.jpg │ │ └── flr-plugin-workflow-model.pdf │ └── README.md ├── flr_plugin_deployment_check_list.md └── flr_usage_example_gif_record_scripts.txt ├── .gitignore ├── LICENSE ├── README.zh-cn.md └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flr-as-plugin' 2 | 3 | -------------------------------------------------------------------------------- /README_Assets/flr-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/README_Assets/flr-install.png -------------------------------------------------------------------------------- /src/main/resources/icons/flr.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/src/main/resources/icons/flr.png -------------------------------------------------------------------------------- /Docs/flr_plugin_icon_design.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/Docs/flr_plugin_icon_design.sketch -------------------------------------------------------------------------------- /README_Assets/flr-usage-example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/README_Assets/flr-usage-example.gif -------------------------------------------------------------------------------- /src/main/resources/icons/flr_13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/src/main/resources/icons/flr_13.png -------------------------------------------------------------------------------- /src/main/resources/icons/flr_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/src/main/resources/icons/flr_16.png -------------------------------------------------------------------------------- /src/main/resources/icons/flr_13@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/src/main/resources/icons/flr_13@2x.png -------------------------------------------------------------------------------- /src/main/resources/icons/flr_16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/src/main/resources/icons/flr_16@2x.png -------------------------------------------------------------------------------- /Docs/flr_plugin_workflow/assets/src/gitflow-model.src.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/Docs/flr_plugin_workflow/assets/src/gitflow-model.src.key -------------------------------------------------------------------------------- /Docs/flr_plugin_workflow/assets/flr-plugin-workflow-model.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/Docs/flr_plugin_workflow/assets/flr-plugin-workflow-model.jpg -------------------------------------------------------------------------------- /Docs/flr_plugin_workflow/assets/flr-plugin-workflow-model.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/Docs/flr_plugin_workflow/assets/flr-plugin-workflow-model.pdf -------------------------------------------------------------------------------- /Docs/flr_plugin_workflow/assets/src/flr-plugin-workflow-model.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Fly-Mix/flr-as-plugin/HEAD/Docs/flr_plugin_workflow/assets/src/flr-plugin-workflow-model.key -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | .DS_Store 3 | 4 | # IntelliJ related 5 | *.iml 6 | *.ipr 7 | *.iws 8 | .idea/ 9 | build/ 10 | 11 | # gradle related 12 | .gradle 13 | gradle/wrapper/ 14 | gradlew 15 | gradlew.bat -------------------------------------------------------------------------------- /src/main/java/com/flr/toolWindow/FlrToolWindowProvider.java: -------------------------------------------------------------------------------- 1 | package com.flr.toolWindow; 2 | 3 | import javax.swing.*; 4 | 5 | public class FlrToolWindowProvider { 6 | public JPanel windowContent; 7 | public JSplitPane splitPane; 8 | public JPanel consoleContainer; 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/flr/FlrException.java: -------------------------------------------------------------------------------- 1 | package com.flr; 2 | 3 | /* 4 | * Flr异常 5 | * 当插件收到这个异常时,应该终止当前的任务程序 6 | * */ 7 | public class FlrException extends Exception { 8 | 9 | public FlrException(String message) { 10 | super(message); 11 | } 12 | 13 | public static final FlrException ILLEGAL_ENV = new FlrException("[*]: found illegal environment, you can get the details from Flr ToolWindow"); 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /Docs/flr_plugin_deployment_check_list.md: -------------------------------------------------------------------------------- 1 | ## Flr Plugin Engine Deployment Check List 2 | 3 | 1. 确定Deployment的版本号:$version 4 | 1. 编辑`build.gradle`,更新与`flr-plugin-engine`发布相关的字段: 5 | - 更新`pluginEngineVersion`为:$version 6 | - 更新`changeNotes`中的`Flr Plugin Engine Change Notes`选项 7 | 1. 合并到`master`分支,然后打tag,合并到各个`platform-master`分支 8 | 9 | ## Flr Plugin Deployment Check List 10 | 11 | 1. 编辑`build.gradle`,更新与`flr-plugin`发布相关的字段: 12 | - 更新`pluginProductPlatform`为当前`platform-master`分支对应的平台,如`PlatformType.IC_191` 13 | - 更新`changeNotes`中的`Flr Plugin Change Notes`选项 14 | 1. 在项目根目录下运行脚本验证插件:`./gradlew verifyPlugin` 15 | 1. 在项目根目录下运行脚本打包插件:`./gradlew buildPlugin` 16 | 17 | ## Publish Flr Plugin To Marketplace 18 | 19 | 前往[IntelliJ插件市场](https://plugins.jetbrains.com/),手动上传Flr Plugin。 20 | 21 | -------------------------------------------------------------------------------- /src/main/java/com/flr/logConsole/FlrColoredLogEntity.java: -------------------------------------------------------------------------------- 1 | package com.flr.logConsole; 2 | 3 | import com.sun.istack.NotNull; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | public class FlrColoredLogEntity { 9 | public static class Item { 10 | public String text; 11 | public FlrLogConsole.LogType logType = FlrLogConsole.LogType.normal; 12 | 13 | public Item(@NotNull String text, @NotNull FlrLogConsole.LogType logType) { 14 | this.text = text; 15 | this.logType = logType; 16 | } 17 | } 18 | 19 | public List items; 20 | 21 | public FlrColoredLogEntity() { 22 | items = new ArrayList(); 23 | } 24 | 25 | public FlrColoredLogEntity(@NotNull List items) { 26 | this.items = items; 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/flr/actions/FlrVersionAction.java: -------------------------------------------------------------------------------- 1 | package com.flr.actions; 2 | 3 | import com.flr.FlrApp; 4 | import com.flr.logConsole.FlrLogConsole; 5 | import com.flr.toolWindow.FlrToolWindowFactory; 6 | import com.intellij.openapi.actionSystem.AnAction; 7 | import com.intellij.openapi.actionSystem.AnActionEvent; 8 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 9 | import com.intellij.openapi.project.Project; 10 | 11 | public class FlrVersionAction extends AnAction { 12 | 13 | @Override 14 | public void actionPerformed(AnActionEvent e) { 15 | Project project = e.getData(PlatformDataKeys.PROJECT); 16 | if (project == null) return; 17 | 18 | FlrLogConsole flrLogConsole = FlrToolWindowFactory.getLogConsole(project); 19 | FlrToolWindowFactory.showCurLogConsole(project); 20 | flrLogConsole.clear(); 21 | 22 | FlrApp flrApp = FlrApp.getInstance(project); 23 | flrApp.getFlrCommand().displayVersion(e, flrLogConsole); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/flr/actions/FlrRecommendAction.java: -------------------------------------------------------------------------------- 1 | package com.flr.actions; 2 | 3 | import com.flr.FlrApp; 4 | import com.flr.logConsole.FlrLogConsole; 5 | import com.flr.toolWindow.FlrToolWindowFactory; 6 | import com.intellij.openapi.actionSystem.AnAction; 7 | import com.intellij.openapi.actionSystem.AnActionEvent; 8 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 9 | import com.intellij.openapi.project.Project; 10 | 11 | public class FlrRecommendAction extends AnAction { 12 | 13 | @Override 14 | public void actionPerformed(AnActionEvent e) { 15 | Project project = e.getData(PlatformDataKeys.PROJECT); 16 | if (project == null) return; 17 | 18 | FlrLogConsole flrLogConsole = FlrToolWindowFactory.getLogConsole(project); 19 | FlrToolWindowFactory.showCurLogConsole(project); 20 | flrLogConsole.clear(); 21 | 22 | FlrApp flrApp = FlrApp.getInstance(project); 23 | flrApp.getFlrCommand().displayRecommendedFlutterResourceStructure(e, flrLogConsole); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 York@Fly-Mix 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 | -------------------------------------------------------------------------------- /Docs/flr_usage_example_gif_record_scripts.txt: -------------------------------------------------------------------------------- 1 | 注意:录制前,请先还原pubspec.yaml 2 | --------------------------------------------- 3 | 4 | Flr Usage Example 5 | 6 | --------------------------------------------- 7 | 8 | 1. Click 9 | `Tools > Flr > Init` 10 | to init Flutter project 11 | 12 | --------------------------------------------- 13 | 14 | 2. Edit pubspec.yaml 15 | to configure the resource directory paths 16 | 17 | --------------------------------------------- 18 | 19 | 3. Click 20 | `Tools > Flr > Generate` 21 | to specify assets and generate r.g.dart 22 | 23 | --------------------------------------------- 24 | 25 | One More Thing: 26 | 27 | If you want Flr to auto generate when you change Flutter asssets, 28 | 29 | you can Click `Tools > Flr > Start Monitor` to launch a monitoring service. 30 | 31 | For example: 32 | 33 | --------------------------------------------- 34 | 35 | 1. Click 36 | `Tools > Flr > Start Monitor` 37 | to launch a monitoring service 38 | 39 | --------------------------------------------- 40 | 41 | 2. Add a new asset: 163.com.yaml 42 | 43 | --------------------------------------------- 44 | 45 | 3. Click 46 | `Tools > Flr > Stop Monitor` 47 | to terminate the monitoring service if you want 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/com/flr/toolWindow/FlrToolWindowProvider.form: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /src/main/java/com/flr/actions/FlrInitAction.java: -------------------------------------------------------------------------------- 1 | package com.flr.actions; 2 | 3 | import com.flr.logConsole.FlrLogConsole; 4 | import com.flr.toolWindow.FlrToolWindowFactory; 5 | import com.flr.FlrApp; 6 | import com.intellij.openapi.actionSystem.AnAction; 7 | import com.intellij.openapi.actionSystem.AnActionEvent; 8 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 9 | import com.intellij.openapi.progress.ProgressIndicator; 10 | import com.intellij.openapi.progress.ProgressManager; 11 | import com.intellij.openapi.progress.Task; 12 | import com.intellij.openapi.project.Project; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | public class FlrInitAction extends AnAction { 16 | 17 | @Override 18 | public void actionPerformed(AnActionEvent e) { 19 | Project project = e.getData(PlatformDataKeys.PROJECT); 20 | if (project == null) return; 21 | 22 | FlrLogConsole flrLogConsole = FlrToolWindowFactory.getLogConsole(project); 23 | FlrToolWindowFactory.showCurLogConsole(project); 24 | flrLogConsole.clear(); 25 | 26 | FlrApp flrApp = FlrApp.getInstance(project); 27 | 28 | // Java Code Examples for com.intellij.openapi.progress.ProgressIndicator 29 | // https://www.programcreek.com/java-api-examples/?api=com.intellij.openapi.progress.ProgressIndicator 30 | ProgressManager.getInstance().run(new Task.Backgroundable(project, "Flr Init", false) { 31 | @Override 32 | public void run(@NotNull ProgressIndicator indicator) { 33 | flrApp.getFlrCommand().initAll(e, flrLogConsole); 34 | } 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/flr/FlrConstant.java: -------------------------------------------------------------------------------- 1 | package com.flr; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | public class FlrConstant { 8 | 9 | // 插件ID 10 | public static String PLUGIN_ID = "com.fly-mix.flr"; 11 | 12 | // Flr的核心逻辑版本 13 | public static String CORE_VERSION = "3.2.0"; 14 | 15 | // 插件对外展示的名称 16 | public static String PLUGIN_DISPLAY_NAME = "Flr"; 17 | 18 | // Flr支持的非SVG类图片文件类型 19 | public static List NON_SVG_IMAGE_FILE_TYPES= Arrays.asList(".png", ".jpg", ".jpeg", ".gif", ".webp", ".icon", ".bmp", ".wbmp"); 20 | // Flr支持的SVG类图片文件类型 21 | public static List SVG_IMAGE_FILE_TYPES= Arrays.asList(".svg"); 22 | // Flr支持的图片文件类型 23 | public static List IMAGE_FILE_TYPES= new ArrayList(){ 24 | { 25 | addAll(NON_SVG_IMAGE_FILE_TYPES); 26 | addAll(SVG_IMAGE_FILE_TYPES); 27 | } 28 | }; 29 | // Flr支持的文本文件类型 30 | public static List TEXT_FILE_TYPES= Arrays.asList(".txt", ".json", ".yaml", ".xml"); 31 | // Flr支持的字体文件类型 32 | public static List FONT_FILE_TYPES= Arrays.asList(".ttf", ".otf", ".ttc"); 33 | 34 | // Flr优先考虑的非SVG类图片文件类型 35 | public static String PRIOR_NON_SVG_IMAGE_FILE_TYPE = ".png"; 36 | // Flr优先考虑的SVG类图片文件类型 37 | public static String PRIOR_SVG_IMAGE_FILE_TYPE = ".svg"; 38 | // Flr优先考虑的文本文件类型 39 | // 当前值为 ".*", 意味所有文本文件类型的优先级都一样 40 | public static String PRIOR_TEXT_FILE_TYPE = ".*"; 41 | // Flr优先考虑的字体文件类型 42 | // 当前值为 ".*", 意味所有文本文件类型的优先级都一样 43 | public static String PRIOR_FONT_FILE_TYPE = ".*"; 44 | 45 | // dartfmt工具的默认行长 46 | // 仅用于flr-cli 47 | public static int DARTFMT_LINE_LENGTH = 80; 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/flr/actions/FlrMonitorAction.java: -------------------------------------------------------------------------------- 1 | package com.flr.actions; 2 | 3 | import com.flr.logConsole.FlrLogConsole; 4 | import com.flr.toolWindow.FlrToolWindowFactory; 5 | import com.flr.FlrApp; 6 | import com.intellij.openapi.actionSystem.AnAction; 7 | import com.intellij.openapi.actionSystem.AnActionEvent; 8 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 9 | import com.intellij.openapi.actionSystem.Presentation; 10 | import com.intellij.openapi.project.Project; 11 | 12 | public class FlrMonitorAction extends AnAction { 13 | 14 | @Override 15 | public void actionPerformed(AnActionEvent e) { 16 | Project project = e.getData(PlatformDataKeys.PROJECT); 17 | if (project == null) return; 18 | 19 | FlrLogConsole flrLogConsole = FlrToolWindowFactory.getLogConsole(project); 20 | FlrToolWindowFactory.showCurLogConsole(project); 21 | flrLogConsole.clear(); 22 | 23 | FlrApp flrApp = FlrApp.getInstance(project); 24 | Presentation actionPresentation = e.getPresentation(); 25 | if(flrApp.getFlrCommand().isMonitoringAssets) { 26 | actionPresentation.setText("Start Monitor"); 27 | actionPresentation.setDescription("launch a monitoring service"); 28 | flrApp.getFlrCommand().stopMonitor(e, flrLogConsole); 29 | } else { 30 | actionPresentation.setText("Stop Monitor"); 31 | actionPresentation.setDescription("terminate the monitoring service"); 32 | Boolean isStartSuccess = flrApp.getFlrCommand().startMonitor(e, flrLogConsole); 33 | if(isStartSuccess == false) { 34 | actionPresentation.setText("Start Monitor"); 35 | actionPresentation.setDescription("launch a monitoring service"); 36 | } 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/flr/messageBox/FlrMessageBox.java: -------------------------------------------------------------------------------- 1 | package com.flr.messageBox; 2 | 3 | import com.intellij.execution.ui.ConsoleView; 4 | import com.intellij.notification.Notification; 5 | import com.intellij.notification.NotificationType; 6 | import com.intellij.notification.Notifications; 7 | import com.intellij.openapi.project.Project; 8 | import com.intellij.openapi.wm.ToolWindowManager; 9 | import icons.FlutterIcons; 10 | import org.jetbrains.annotations.NotNull; 11 | import org.jetbrains.annotations.Nullable; 12 | 13 | public class FlrMessageBox { 14 | public static final String FLR_NOTIFICATION_GROUP_ID = "Flr"; 15 | 16 | public static void showInfo(@Nullable Project project, @NotNull String title, @NotNull String content) { 17 | final Notification notification = new Notification( 18 | FLR_NOTIFICATION_GROUP_ID, 19 | title, 20 | content, 21 | NotificationType.INFORMATION); 22 | Notifications.Bus.notify(notification, project); 23 | } 24 | 25 | public static void showWarning(@Nullable Project project, @NotNull String title, @NotNull String content) { 26 | final Notification notification = new Notification( 27 | FLR_NOTIFICATION_GROUP_ID, 28 | title, 29 | content, 30 | NotificationType.WARNING); 31 | Notifications.Bus.notify(notification, project); 32 | } 33 | 34 | public static void showError(@Nullable Project project, @NotNull String title, @NotNull String content) { 35 | final Notification notification = new Notification( 36 | FLR_NOTIFICATION_GROUP_ID, 37 | title, 38 | content, 39 | NotificationType.ERROR); 40 | Notifications.Bus.notify(notification, project); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/flr/FlrApp.java: -------------------------------------------------------------------------------- 1 | package com.flr; 2 | 3 | import com.flr.command.FlrCommand; 4 | import com.intellij.openapi.project.Project; 5 | import com.intellij.openapi.components.Service; 6 | import com.intellij.openapi.components.ServiceManager; 7 | import com.intellij.openapi.components.State; 8 | import com.intellij.openapi.components.Storage; 9 | import com.intellij.openapi.project.ProjectManager; 10 | import com.intellij.openapi.project.ProjectManagerListener; 11 | import com.intellij.openapi.components.ProjectComponent; 12 | import org.jetbrains.annotations.NotNull; 13 | 14 | @Service(Service.Level.PROJECT) 15 | @State( 16 | name = "FlrApp", 17 | storages = {@Storage("flr.xml")} 18 | ) 19 | public final class FlrApp { 20 | private static FlrApp instance; 21 | private final Project curProject; 22 | private final FlrCommand flrCommand; 23 | 24 | private FlrApp(@NotNull Project project) { 25 | curProject = project; 26 | flrCommand = new FlrCommand(curProject); 27 | 28 | // 注册项目关闭监听器 29 | project.getMessageBus().connect().subscribe(ProjectManager.TOPIC, new ProjectManagerListener() { 30 | @Override 31 | public void projectClosing(@NotNull Project project) { 32 | if (project.equals(curProject)) { 33 | dispose(); 34 | } 35 | } 36 | }); 37 | } 38 | 39 | public static FlrApp getInstance(@NotNull Project project) { 40 | if (instance == null) { 41 | synchronized (FlrApp.class) { 42 | if (instance == null) { 43 | instance = project.getService(FlrApp.class); 44 | } 45 | } 46 | } 47 | return instance; 48 | } 49 | 50 | public void dispose() { 51 | if (flrCommand != null) { 52 | flrCommand.dispose(); 53 | } 54 | instance = null; 55 | } 56 | 57 | public FlrCommand getFlrCommand() { 58 | return flrCommand; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.fly-mix.flr 3 | Flr 4 | Fly-Mix 5 | 6 | Flutter Resource Manager Android Studio Plugin 7 | 8 | 10 | 11 | com.intellij.modules.platform 12 | com.intellij.modules.lang 13 | com.intellij.modules.java 14 | Dart 15 | io.flutter 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 31 | 33 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/main/java/com/flr/actions/FlrGenerateAction.java: -------------------------------------------------------------------------------- 1 | package com.flr.actions; 2 | 3 | import com.flr.logConsole.FlrLogConsole; 4 | import com.flr.toolWindow.FlrToolWindowFactory; 5 | import com.flr.FlrApp; 6 | import com.intellij.openapi.actionSystem.AnAction; 7 | import com.intellij.openapi.actionSystem.AnActionEvent; 8 | import com.intellij.openapi.actionSystem.PlatformDataKeys; 9 | import com.intellij.openapi.progress.ProgressIndicator; 10 | import com.intellij.openapi.progress.ProgressManager; 11 | import com.intellij.openapi.progress.Task; 12 | import com.intellij.openapi.project.Project; 13 | import org.jetbrains.annotations.NotNull; 14 | 15 | public class FlrGenerateAction extends AnAction { 16 | 17 | @Override 18 | public void actionPerformed(AnActionEvent e) { 19 | Project project = e.getData(PlatformDataKeys.PROJECT); 20 | if (project == null) return; 21 | 22 | FlrLogConsole flrLogConsole = FlrToolWindowFactory.getLogConsole(project); 23 | FlrToolWindowFactory.showCurLogConsole(project); 24 | 25 | FlrApp flrApp = FlrApp.getInstance(project); 26 | 27 | // 如果当前资源变化监控服务正在运行,则不清空当前日志,否则就清空 28 | if(flrApp.getFlrCommand().isMonitoringAssets == false) { 29 | flrLogConsole.clear(); 30 | } 31 | 32 | ProgressManager.getInstance().run(new Task.Backgroundable(project, "Flr Generate", false) { 33 | @Override 34 | public void run(@NotNull ProgressIndicator indicator) { 35 | flrApp.getFlrCommand().generateAll(e, flrLogConsole); 36 | 37 | // 如果当前资源变化监控服务正在运行,则在执行 generate 后,打印监控服务在运行的提示 38 | if(flrApp.getFlrCommand().isMonitoringAssets) { 39 | FlrLogConsole.LogType indicatorType = FlrLogConsole.LogType.normal; 40 | flrLogConsole.println("", indicatorType); 41 | String indicatorMessage = 42 | "[*]: the monitoring service is monitoring the asset changes, and then auto scan assets, specifies assets and generates \"r.g.dart\" ...\n" + 43 | "[*]: you can click menu \"Tools-Flr-Stop Monitor\" to terminate it\n"; 44 | flrLogConsole.println(indicatorMessage, FlrLogConsole.LogType.tips); 45 | } 46 | } 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Docs/flr_plugin_workflow/README.md: -------------------------------------------------------------------------------- 1 | # Flr Plugin Workflow 2 | 3 | 由于`flr-plugin`依赖了[flutter插件](https://plugins.jetbrains.com/plugin/9212-flutter)和[dart插件](https://plugins.jetbrains.com/plugin/6351-dart),`flr-plugin`需要为`Android Studio`的不同版本做适配和生产发布。 4 | 5 | 为了保证`flr-plugin`的具备良好的生产流程,所以为其定制了一个工作流,以指导如何对仓库分支进行管理以及如何发布生产版本。 6 | 7 | > 关于插件与IntelliJ平台产品的兼容性的更多细节,可参考[《Plugin Compatibility with IntelliJ Platform Products》](https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/plugin_compatibility.html)。 8 | 9 | ## 工作流 10 | 11 | 生产`flr-plugin`的工作流整体如下: 12 | 13 | ![flr-plugin-workflow-model.pdf](assets/flr-plugin-workflow-model.jpg) 14 | 15 | 下面将会做进一步详细介绍。 16 | 17 | #### 主分支类型 18 | 19 | 代码仓库中主要有2种主分支类型: 20 | 21 | - `master`分支 22 | 23 | - `master`分支在仓库创建之初就创建,其生命周期是无限的。 24 | - `master`分支主要用于`flr-plugin-engine`(Flr插件引擎)开发和发布;`flr-plugin-engine`的开发内容包括:实现`Flr-Core-Logic`(Flr核心逻辑)和其他与平台无关的功能,以及修复与平台无关的bug。 25 | 26 | - `platform-master`分支 27 | 28 | - `platform-master`分支在`flr-plugin`需要为`新版本的Android Studio`进行新版本开发时创建,其生命周期也是无限的。 29 | 30 | `新版本的Android Studio`的定义是:`Android Studio`升级了`IntelliJ IDEA Community Edition`内核版本。 31 | 32 | > `Android Studio`是基于`IntelliJ IDEA Community Edition`构建开发的。 33 | 34 | - `platform-master`分支主要用于`flr-plugin`(Flr插件产品)开发和发布;`flr-plugin`的开发内容包括:基于`flr-plugin-engine`进行平台适配和修复与平台相关的bug。 35 | 36 | - `platform-master`分支的命名规则是:`#{IC_branch_number}/master`。其中`#{IC_branch_number}`为当前的`Android Studio`的`IntelliJ IDEA Community Edition`内核版本对应的分支号。 37 | 38 | **Q:** 如何获取`#{IC_branch_number}`? 39 | 40 | **A:** 从`Android Studio`的版本信息获取。比如,`Android Studio v3.6`的版本信息是: 41 | 42 | ``` 43 | Android Studio 3.6 44 | Build #AI-192.7142.36.36.6200805, built on February 12, 2020 45 | ``` 46 | 47 | 其中`#AI-`后跟随的第一个数字`192`就是`#{IC_branch_number}`。 48 | 49 | 所以,为`Android Studio v3.6`创建的`platform-master`的分支名为:`192/master`。 50 | 51 | > 关于`branch_number`的更多细节,可参考[《Build Number Ranges》](https://www.jetbrains.org/intellij/sdk/docs/basics/getting_started/build_number_ranges.html)。 52 | 53 | #### 版本开发、管理和发布流程 54 | 55 | 当需要开发新版本时(功能升级或者兼容新版`Android Studio`),按照以下工作流进行代码开发提交和进行版本命名管理: 56 | 57 | - 使用[Git-Flow](https://nvie.com/posts/a-successful-git-branching-model/)或者[Github-Flow](https://guides.github.com/introduction/flow/)的工作流,在`master`分支上进行需求开发。完成需求开发后,打tag发布新版本的`flr-plugin-engine`给`platform-master`使用;`flr-plugin-engine`的版本号(`plugin_engine_version`)的命名遵守[语义化版本semver 2.0](http://semver.org/)规范; 58 | - 使用`non-fast-forward`的方式合并`master`分支新发布的`flr-plugin-engine`到各个`platform-master`分支; 59 | - 在各个`platform-master`分支上,进行平台适配开发。完成适配开发后,打tag发布`flr-plugin`; 60 | - 各个`platform-master`分支发布的`flr-plugin`的版本号(`plugin_product_version`)的命名规则是:`#{IC_branch_number}.#{plugin_engine_version}`,如当前最新发布的`flr-plugin-engine`的版本号为:`1.1.0`,在`192/master`分支发布的`flr-plugin`的版本号为:`192.1.1.0`。 61 | 62 | -------------------------------------------------------------------------------- /src/main/java/com/flr/command/FlrListener.java: -------------------------------------------------------------------------------- 1 | package com.flr.command; 2 | 3 | import com.intellij.openapi.Disposable; 4 | import com.intellij.openapi.application.ApplicationManager; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.vfs.VirtualFile; 7 | import com.intellij.openapi.vfs.VirtualFileManager; 8 | import com.intellij.openapi.vfs.newvfs.BulkFileListener; 9 | import com.intellij.openapi.vfs.newvfs.events.VFileEvent; 10 | import com.intellij.util.messages.MessageBusConnection; 11 | import org.jetbrains.annotations.NotNull; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | public class FlrListener implements BulkFileListener, Disposable { 17 | 18 | public interface AssetChangesEventCallback { 19 | void run(); 20 | } 21 | 22 | private MessageBusConnection connection; 23 | private final Project curProject; 24 | private List curMonitoredAssetDirFullPaths = new ArrayList(); 25 | private AssetChangesEventCallback curAssetChangesEventCallback; 26 | 27 | /* 28 | * 生成资源文件监控者 29 | * 30 | * @param project 31 | * @param monitoredAssetDirs 被监控的资源目录(相对路径) 32 | * @param assetChangesEventCallback 被监控的资源目录有资源变化更新后的回调操作 33 | * @return com.fly_mix.flr.FlrListener 34 | * */ 35 | public FlrListener(Project project, List monitoredAssetDirs, AssetChangesEventCallback assetChangesEventCallback) { 36 | curProject = project; 37 | String flutterProjectRootDir = curProject.getBasePath(); 38 | for(String assetDir: monitoredAssetDirs) { 39 | String assetDirFullPath = flutterProjectRootDir + "/" + assetDir; 40 | curMonitoredAssetDirFullPaths.add(assetDirFullPath); 41 | } 42 | curAssetChangesEventCallback = assetChangesEventCallback; 43 | connection = ApplicationManager.getApplication().getMessageBus().connect(); 44 | connection.subscribe(VirtualFileManager.VFS_CHANGES, this); 45 | } 46 | 47 | @Override 48 | public void dispose() { 49 | connection.dispose(); 50 | } 51 | 52 | @Override 53 | public void before(@NotNull List events) { 54 | 55 | } 56 | 57 | @Override 58 | public void after(@NotNull List events) { 59 | Boolean shouldCallback = false; 60 | 61 | System.out.println("\nFlr events"+ events); 62 | 63 | for (VFileEvent event: events) { 64 | VirtualFile eventFile = event.getFile(); 65 | String fileFullPath = eventFile.getPath(); 66 | 67 | System.out.println("\nVFileEvent"+ "\ngetPath:"+event.getPath()+ "\ngetFile:"+event.getFile()); 68 | 69 | for(String monitoredAssetDirFullPath: curMonitoredAssetDirFullPaths) { 70 | if(fileFullPath.contains(monitoredAssetDirFullPath)) { 71 | shouldCallback = true; 72 | break; 73 | } 74 | } 75 | 76 | if(shouldCallback) { 77 | break; 78 | } 79 | } 80 | 81 | if(shouldCallback) { 82 | curAssetChangesEventCallback.run(); 83 | } 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /src/main/java/com/flr/logConsole/FlrLogConsole.java: -------------------------------------------------------------------------------- 1 | package com.flr.logConsole; 2 | 3 | import com.intellij.execution.impl.ConsoleViewImpl; 4 | import com.intellij.execution.ui.ConsoleView; 5 | import com.intellij.execution.ui.ConsoleViewContentType; 6 | import com.intellij.openapi.application.ApplicationManager; 7 | import com.intellij.openapi.command.WriteCommandAction; 8 | import com.intellij.openapi.project.Project; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | public class FlrLogConsole { 12 | public enum LogType { 13 | normal, 14 | tips, 15 | warning, 16 | error 17 | } 18 | 19 | private ConsoleView curConsoleView; 20 | private Project curProject; 21 | 22 | public FlrLogConsole(@NotNull Project project, @NotNull ConsoleView consoleView) { 23 | curProject = project; 24 | curConsoleView = consoleView; 25 | } 26 | 27 | public void println(@NotNull String text, @NotNull FlrLogConsole.LogType logType) { 28 | if(curConsoleView == null) { 29 | System.out.println("FlrLogConsole: curConsoleView is null !!!"); 30 | return; 31 | } 32 | 33 | ApplicationManager.getApplication().invokeLater(new Runnable() { 34 | @Override 35 | public void run() { 36 | WriteCommandAction.runWriteCommandAction(curProject, new Runnable() { 37 | @Override 38 | public void run() { 39 | ConsoleViewContentType contentType = ConsoleViewContentType.SYSTEM_OUTPUT; 40 | switch (logType) { 41 | case normal: 42 | contentType = ConsoleViewContentType.SYSTEM_OUTPUT; 43 | break; 44 | case tips: 45 | contentType = ConsoleViewContentType.LOG_INFO_OUTPUT; 46 | break; 47 | case warning: 48 | contentType = ConsoleViewContentType.LOG_WARNING_OUTPUT; 49 | break; 50 | case error: 51 | contentType = ConsoleViewContentType.ERROR_OUTPUT; 52 | break; 53 | } 54 | curConsoleView.print( text + "\n", contentType); 55 | ConsoleViewImpl consoleViewImpl = (ConsoleViewImpl)curConsoleView; 56 | if(consoleViewImpl != null) { 57 | consoleViewImpl.scrollToEnd(); 58 | } 59 | } 60 | }); 61 | } 62 | }); 63 | } 64 | 65 | public void println(@NotNull FlrColoredLogEntity coloredLogEntity) { 66 | println("", LogType.normal); 67 | for (FlrColoredLogEntity.Item item : coloredLogEntity.items) { 68 | println(item.text, item.logType); 69 | } 70 | } 71 | 72 | public void clear() { 73 | if(curConsoleView == null) { 74 | System.out.println("FlrLogConsole: curConsoleView is null !!!"); 75 | return; 76 | } 77 | curConsoleView.clear(); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/flr/pubspecFileTree/FlrPubspecFileTree.java: -------------------------------------------------------------------------------- 1 | package com.flr.pubspecFileTree; 2 | 3 | import com.flr.command.util.FlrFileUtil; 4 | import com.intellij.openapi.fileEditor.FileEditorManager; 5 | import com.intellij.openapi.project.Project; 6 | import com.intellij.openapi.vfs.LocalFileSystem; 7 | import com.intellij.openapi.vfs.VirtualFile; 8 | import com.intellij.ui.components.JBScrollPane; 9 | import com.intellij.ui.treeStructure.Tree; 10 | import org.jetbrains.annotations.NotNull; 11 | 12 | import java.awt.*; 13 | import java.io.File; 14 | import java.util.List; 15 | import javax.swing.*; 16 | import javax.swing.event.TreeSelectionEvent; 17 | import javax.swing.event.TreeSelectionListener; 18 | import javax.swing.tree.DefaultMutableTreeNode; 19 | import javax.swing.tree.DefaultTreeCellRenderer; 20 | import javax.swing.tree.DefaultTreeModel; 21 | 22 | public class FlrPubspecFileTree extends JPanel { 23 | 24 | private Project curProject; 25 | 26 | private Tree fileTree; 27 | 28 | public FlrPubspecFileTree(@NotNull Project project) { 29 | curProject = project; 30 | 31 | setLayout(new BorderLayout()); 32 | 33 | DefaultMutableTreeNode rootTreeNode =new DefaultMutableTreeNode("All Pubspec.yaml Files"); 34 | 35 | fileTree = new Tree(rootTreeNode); 36 | DefaultTreeCellRenderer treeCellRenderer = new DefaultTreeCellRenderer(); 37 | fileTree.setCellRenderer(treeCellRenderer); 38 | fileTree.addTreeSelectionListener(new TreeSelectionListener() { 39 | @Override 40 | public void valueChanged(TreeSelectionEvent e) { 41 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.getPath().getLastPathComponent(); 42 | 43 | if(node.isLeaf() == false) { 44 | return; 45 | } 46 | 47 | String flutterMainProjectRootDir = curProject.getBasePath(); 48 | String pubspecFilePath = flutterMainProjectRootDir + "/" + node.getUserObject(); 49 | File pubspecFile = new File(pubspecFilePath); 50 | VirtualFile virtualPubspecFile = LocalFileSystem.getInstance().findFileByIoFile(pubspecFile); 51 | if(virtualPubspecFile == null) { 52 | return; 53 | } 54 | FileEditorManager.getInstance(curProject).openFile(virtualPubspecFile, true); 55 | } 56 | }); 57 | 58 | JBScrollPane scrollPane = new JBScrollPane(); 59 | scrollPane.getViewport().add(fileTree); 60 | add(BorderLayout.CENTER, scrollPane); 61 | } 62 | 63 | public void refreshContent() { 64 | // 检测当前flutter主工程根目录是否存在 pubspec.yaml;若不存在,则结束刷新 65 | String flutterMainProjectRootDir = curProject.getBasePath(); 66 | String mainPubspecFilePath = flutterMainProjectRootDir + "/pubspec.yaml"; 67 | File mainPubspecFile = new File(mainPubspecFilePath); 68 | if(mainPubspecFile.exists() == false) { 69 | return; 70 | } 71 | 72 | // 获取fileTree的数据源,做数据更新操作 73 | DefaultTreeModel model = (DefaultTreeModel)fileTree.getModel(); 74 | DefaultMutableTreeNode rootTreeNode = (DefaultMutableTreeNode)model.getRoot(); 75 | // 移除旧的数据 76 | rootTreeNode.removeAllChildren(); 77 | 78 | // 添加flutter主工程的pubspec.yaml 79 | DefaultMutableTreeNode mainPubspecFileTreeNode = new DefaultMutableTreeNode("pubspec.yaml"); 80 | rootTreeNode.add(mainPubspecFileTreeNode); 81 | 82 | // 获取flutter主工程根目录下所有的子工程目录,并添加它们的pubspec.yaml 83 | List flutterSubProjectRootDirArray = FlrFileUtil.getFlutterSubProjectRootDirs(flutterMainProjectRootDir); 84 | for(String flutterSubProjectRootDir : flutterSubProjectRootDirArray) { 85 | String relativeDir = flutterSubProjectRootDir.replaceFirst(flutterMainProjectRootDir + "/", ""); 86 | String relativePubspecFile = String.format("%s/pubspec.yaml", relativeDir); 87 | DefaultMutableTreeNode fileTreeNode = new DefaultMutableTreeNode(relativePubspecFile); 88 | rootTreeNode.add(fileTreeNode); 89 | } 90 | 91 | model.reload(rootTreeNode); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/flr/toolWindow/FlrToolWindowFactory.java: -------------------------------------------------------------------------------- 1 | package com.flr.toolWindow; 2 | 3 | import com.flr.logConsole.FlrLogConsole; 4 | import com.flr.pubspecFileTree.FlrPubspecFileTree; 5 | import com.intellij.execution.filters.TextConsoleBuilderFactory; 6 | import com.intellij.execution.ui.ConsoleView; 7 | import com.intellij.execution.ui.ConsoleViewContentType; 8 | import com.intellij.openapi.project.Project; 9 | import com.intellij.openapi.wm.ToolWindow; 10 | import com.intellij.openapi.wm.ToolWindowFactory; 11 | import com.intellij.openapi.wm.ToolWindowManager; 12 | import com.intellij.ui.content.Content; 13 | import com.intellij.ui.content.ContentManager; 14 | import org.jetbrains.annotations.NotNull; 15 | 16 | import javax.swing.*; 17 | import javax.swing.tree.DefaultMutableTreeNode; 18 | import javax.swing.tree.DefaultTreeModel; 19 | import java.awt.*; 20 | 21 | public class FlrToolWindowFactory implements ToolWindowFactory { 22 | 23 | @Override 24 | public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { 25 | FlrToolWindowProvider flrToolWindowProvider = new FlrToolWindowProvider(); 26 | 27 | // 添加FlrToolWindow内容到IDE的ID为Flr的toolWindow 28 | Content content = toolWindow.getContentManager().getFactory().createContent(flrToolWindowProvider.windowContent, "", true); 29 | content.setDisplayName(""); 30 | toolWindow.getContentManager().addContent(content, 0); 31 | 32 | // 添加FlrPubspecFileTree控件到flrToolWindowContainer-splitPane-left中 33 | FlrPubspecFileTree flrPubspecFileTree = new FlrPubspecFileTree(project); 34 | flrToolWindowProvider.splitPane.setLeftComponent(flrPubspecFileTree); 35 | flrPubspecFileTree.refreshContent(); 36 | 37 | // 添加consoleView到flrToolWindowContainer-splitPane-right中 38 | ConsoleView consoleView = TextConsoleBuilderFactory.getInstance().createBuilder(project).getConsole(); 39 | flrToolWindowProvider.consoleContainer.add(consoleView.getComponent(), BorderLayout.CENTER); 40 | 41 | String welcomeMessage = "Welcome to use Flr\nYou can get more details from https://github.com/Fly-Mix/flr-as-plugin"; 42 | consoleView.print(welcomeMessage, ConsoleViewContentType.SYSTEM_OUTPUT); 43 | 44 | 45 | // DefaultMutableTreeNode color =new DefaultMutableTreeNode("pubspec.yaml"); 46 | // DefaultMutableTreeNode font =new DefaultMutableTreeNode("example/pubspec.yaml"); 47 | // 48 | // DefaultTreeModel model = (DefaultTreeModel)flrToolWindowProvider.fileTree.getModel(); 49 | // DefaultMutableTreeNode root = (DefaultMutableTreeNode)model.getRoot(); 50 | // root.setUserObject("All pubspec.yaml files"); 51 | // root.removeAllChildren(); 52 | // root.add(color); 53 | // root.add(font); 54 | // model.reload(root); 55 | } 56 | 57 | public static FlrLogConsole getLogConsole(@NotNull Project project){ 58 | ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Flr"); 59 | if(toolWindow == null) { 60 | return null; 61 | } 62 | 63 | ContentManager contentManager = toolWindow.getContentManager(); 64 | Content content = contentManager.findContent(""); 65 | 66 | JPanel flrToolWindowContent = (JPanel) content.getComponent(); 67 | JSplitPane splitPane = (JSplitPane)flrToolWindowContent.getComponent(0); 68 | 69 | JPanel consoleContainer = (JPanel) splitPane.getRightComponent(); 70 | System.out.println("consoleContainer: "); 71 | System.out.println(consoleContainer); 72 | 73 | ConsoleView consoleView = (ConsoleView) consoleContainer.getComponent(0); 74 | System.out.println("consoleView: "); 75 | System.out.println(consoleView); 76 | 77 | FlrLogConsole flrLogConsole = new FlrLogConsole(project, consoleView); 78 | return flrLogConsole; 79 | } 80 | 81 | public static void showCurLogConsole(@NotNull Project project) { 82 | ToolWindow toolWindow = ToolWindowManager.getInstance(project).getToolWindow("Flr"); 83 | if(toolWindow == null) { 84 | return; 85 | } 86 | 87 | toolWindow.show(new Runnable() { 88 | @Override 89 | public void run() { 90 | 91 | } 92 | }); 93 | } 94 | } -------------------------------------------------------------------------------- /src/main/java/com/flr/command/util/FlrUtil.java: -------------------------------------------------------------------------------- 1 | package com.flr.command.util; 2 | 3 | 4 | import com.flr.FlrConstant; 5 | import com.flr.FlrException; 6 | import com.intellij.ide.plugins.PluginManager; 7 | import com.intellij.ide.plugins.PluginManagerCore; 8 | import com.intellij.openapi.actionSystem.AnAction; 9 | import com.intellij.openapi.actionSystem.AnActionEvent; 10 | import com.intellij.openapi.actionSystem.ActionManager; 11 | import com.intellij.openapi.actionSystem.IdeActions; 12 | import com.intellij.openapi.application.ApplicationManager; 13 | import com.intellij.openapi.command.WriteCommandAction; 14 | import com.intellij.openapi.extensions.PluginId; 15 | import com.intellij.openapi.project.Project; 16 | import com.intellij.openapi.vfs.LocalFileSystem; 17 | import com.intellij.openapi.vfs.VirtualFile; 18 | import com.jetbrains.lang.dart.ide.actions.DartStyleAction; 19 | import com.sun.istack.NotNull; 20 | import io.flutter.actions.FlutterPackagesGetAction; 21 | import org.yaml.snakeyaml.DumperOptions; 22 | import org.yaml.snakeyaml.Yaml; 23 | 24 | import java.io.*; 25 | import java.nio.file.Files; 26 | import java.nio.file.Paths; 27 | import java.util.*; 28 | import java.util.regex.Pattern; 29 | 30 | @SuppressWarnings("unchecked") 31 | public class FlrUtil { 32 | 33 | // MARK: - Version Util Methods 34 | /* 35 | * @param v1 36 | * @param v2 37 | * @return if v1 > v2, return 1 38 | * if v1 < v2, return 2 39 | * if equal, return 0 40 | * input error, return -1 41 | * */ 42 | public static int versionCompare(String v1, String v2) { 43 | Pattern pattern = Pattern.compile("\\d+(\\.\\d+)*"); 44 | 45 | if(!pattern.matcher(v1).matches() || !pattern.matcher(v2).matches()) { 46 | return -1; 47 | } 48 | 49 | String[] s1 = v1.split("\\."); 50 | String[] s2 = v2.split("\\."); 51 | 52 | int length = s1.length < s2.length ? s1.length : s2.length; 53 | for(int i = 0; i < length; i++) { 54 | int diff = Integer.valueOf(s1[i]) - Integer.valueOf(s2[i]); 55 | 56 | if(diff == 0) { 57 | continue; 58 | }else{ 59 | return diff > 0 ? 1 : 2; 60 | } 61 | } 62 | 63 | return 0; 64 | } 65 | 66 | /* 67 | * 获取当前插件的版本 68 | * */ 69 | public static String getFlrVersion() { 70 | PluginId flrPluginId = PluginId.getId(FlrConstant.PLUGIN_ID); 71 | return PluginManagerCore.getPlugin(flrPluginId).getVersion(); 72 | } 73 | 74 | // MARK: - Shell Util Methods 75 | 76 | // 在IDEA JetBrains IDE上可执行脚本成功,但是在 Android Studio(non-IDEA JetBrains IDE)上却执行失败, 77 | // 原因预估是IDE版本差异导致API差异导致执行脚本失败,具体待后面进一步确定 78 | /* 79 | * 运行shell脚本并返回运行结果 80 | * 注意:如果shell脚本中含有awk,一定要按new String[]{"/bin/sh","-c",shStr}写,才可以获得流 81 | * 82 | * @param shStr 需要运行的shell脚本 83 | * @return List shell脚本运行结果 84 | * */ 85 | public static List execShell(String shStr) { 86 | List outputLineList = new ArrayList(); 87 | System.out.println(String.format("Running shell command `%s` now ...", shStr)); 88 | try { 89 | Process process = Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",shStr},null,null); 90 | BufferedReader stdInput = new BufferedReader(new InputStreamReader(process.getInputStream())); 91 | BufferedReader stdError = new BufferedReader(new InputStreamReader(process.getErrorStream())); 92 | String line; 93 | 94 | // Read the output from the command 95 | System.out.println("Here is the standard output of the command:"); 96 | while ((line = stdInput.readLine()) != null){ 97 | System.out.println(" " + line); 98 | outputLineList.add(line); 99 | } 100 | 101 | // Read any errors from the attempted command 102 | System.out.println("Here is the standard error of the command (if any):"); 103 | while ((line = stdError.readLine()) != null) { 104 | System.out.println(" " + line); 105 | } 106 | } catch (Exception e) { 107 | e.printStackTrace(); 108 | } 109 | 110 | System.out.println(String.format("Running shell command `%s` done!", shStr)); 111 | return outputLineList; 112 | } 113 | 114 | // MARK: - Flutter Util Methods 115 | public static void runFlutterPubGet(AnActionEvent actionEvent) { 116 | // 从后台任务线程切换到UI线程 117 | ApplicationManager.getApplication().invokeLater(() -> { 118 | ApplicationManager.getApplication().runWriteAction(() -> { 119 | // 使用 ActionManager 来执行 FlutterPackagesGetAction 120 | AnAction action = ActionManager.getInstance().getAction("flutter.pub.get"); 121 | if (action != null) { 122 | action.actionPerformed(actionEvent); 123 | } 124 | }); 125 | }); 126 | } 127 | 128 | public static void formatDartFile(@NotNull Project project, @NotNull File dartFile) { 129 | ApplicationManager.getApplication().invokeLater(new Runnable() { 130 | @Override 131 | public void run() { 132 | WriteCommandAction.runWriteCommandAction(project, new Runnable() { 133 | @Override 134 | public void run() { 135 | // 格式化方案一:Android Studio(non-IDEA JetBrains IDE)和 IDEA JetBrains IDE 上均可行 136 | VirtualFile dartVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(dartFile); 137 | if (dartVirtualFile == null) { 138 | return; 139 | } 140 | List dartFiles = new ArrayList(); 141 | dartFiles.add(dartVirtualFile); 142 | DartStyleAction.runDartfmt(project, dartFiles); 143 | 144 | // 格式化方案二:Android Studio(non-IDEA JetBrains IDE)上不成功;在IDEA JetBrains IDE 上可行 145 | /* 146 | PsiFile dartPsiFile = PsiManager.getInstance(project).findFile(dartVirtualFile); 147 | CodeStyleManager.getInstance(project).reformat(dartPsiFile); 148 | */ 149 | 150 | dartVirtualFile.refresh(false, false); 151 | } 152 | }); 153 | } 154 | }); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/com/flr/command/FlrChecker.java: -------------------------------------------------------------------------------- 1 | package com.flr.command; 2 | 3 | import com.flr.FlrConstant; 4 | import com.flr.FlrException; 5 | import com.flr.logConsole.FlrLogConsole; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.File; 9 | import java.util.ArrayList; 10 | import java.util.LinkedHashSet; 11 | import java.util.List; 12 | import java.util.Map; 13 | 14 | // 条件检测器,提供检测各种条件是否合法的方法 15 | public class FlrChecker { 16 | /* 17 | * 检测当前flutter工程目录是否存在pubspec.yaml文件 18 | * 若存在返回true 19 | * 否则抛出异常 20 | * */ 21 | public static boolean checkPubspecFileIsExisted(@NotNull FlrLogConsole flrLogConsole, @NotNull String flutter_dir) throws FlrException { 22 | String pubspecFilePath = flutter_dir + "/pubspec.yaml"; 23 | File pubspecFile = new File(pubspecFilePath); 24 | 25 | if(pubspecFile.exists()) { 26 | return true; 27 | } 28 | 29 | flrLogConsole.println(String.format("[x]: %s not found", pubspecFilePath), FlrLogConsole.LogType.error); 30 | String tipsStr = String.format("[*]: please make sure %s is existed", pubspecFilePath); 31 | flrLogConsole.println(tipsStr, FlrLogConsole.LogType.tips); 32 | 33 | throw FlrException.ILLEGAL_ENV; 34 | } 35 | 36 | /* 37 | * 检测pubspec.yaml中是否存在flr的配置信息`flr_config`: 38 | * 若存在返回true 39 | * 否则抛出异常 40 | * 41 | * flr的配置: 42 | * 43 | * ``` yaml 44 | * flr: 45 | * core_version: 1.0.0 46 | * dartfmt_line_length: 80 47 | * assets: 48 | * fonts: 49 | * ``` 50 | * 51 | * */ 52 | public static boolean checkFlrConfigIsExisted(@NotNull FlrLogConsole flrLogConsole, @NotNull Map pubspecConfig) throws FlrException { 53 | Object flrConfig = pubspecConfig.get("flr"); 54 | if(flrConfig instanceof Map) { 55 | return true; 56 | } 57 | 58 | flrLogConsole.println("[x]: have no flr configuration in pubspec.yaml", FlrLogConsole.LogType.error); 59 | flrLogConsole.println("[*]: please click menu \"Tools-Flr-Init\" to fix it", FlrLogConsole.LogType.tips); 60 | 61 | throw FlrException.ILLEGAL_ENV; 62 | } 63 | 64 | /* 65 | * 检测当前flr配置信息中的assets配置是否合法 66 | * 若合法,返回资源目录结果三元组 resourceDirResultTuple 67 | * 否则抛出异常 68 | * 69 | * flutterProjectRootDir = "~/path/to/flutter_r_demo" 70 | * resourceDirResultTuple = [assetsLegalResourceDirArray, fontsLegalResourceDirArray, illegalResourceDirArray] 71 | * assetsLegalResourceDirArray = ["~/path/to/flutter_r_demo/lib/assets/images", "~/path/to/flutter_r_demo/lib/assets/texts"] 72 | * fontsLegalResourceDirArray = ["~/path/to/flutter_r_demo/lib/assets/fonts"] 73 | * illegalResourceDirArray = ["~/path/to/flutter_r_demo/to/non-existed_folder"] 74 | * 75 | * */ 76 | public static List> checkFlrAssetsIsLegal(@NotNull FlrLogConsole flrLogConsole, @NotNull Map flrConfig, @NotNull String flutterProjectRootDir) throws FlrException { 77 | String flrCoreVersion = FlrConstant.CORE_VERSION; 78 | if(flrConfig.containsKey("core_version")) { 79 | flrCoreVersion = String.format("%s", flrConfig.get("core_version")); 80 | } 81 | String dartfmtLineLengthStr = String.format("%d",FlrConstant.DARTFMT_LINE_LENGTH); 82 | if(flrConfig.containsKey("dartfmt_line_length")) { 83 | dartfmtLineLengthStr = String.format("%s", flrConfig.get("dartfmt_line_length")); 84 | } 85 | List assetsResourceDirArray = (List)flrConfig.get("assets"); 86 | List fontsResourceDirArray = (List)flrConfig.get("fonts"); 87 | 88 | if(assetsResourceDirArray instanceof List == false) { 89 | assetsResourceDirArray = new ArrayList(); 90 | } 91 | if(fontsResourceDirArray instanceof List == false) { 92 | fontsResourceDirArray = new ArrayList(); 93 | } 94 | 95 | // 移除非法的 resource_dir(nil,空字符串,空格字符串) 96 | assetsResourceDirArray.remove(""); 97 | assetsResourceDirArray.remove(null); 98 | fontsResourceDirArray.remove(""); 99 | fontsResourceDirArray.remove(null); 100 | // 过滤重复的 resource_dir 101 | LinkedHashSet tempSet = new LinkedHashSet(assetsResourceDirArray); 102 | assetsResourceDirArray = new ArrayList(tempSet); 103 | tempSet = new LinkedHashSet(fontsResourceDirArray); 104 | fontsResourceDirArray = new ArrayList(tempSet); 105 | 106 | // 筛选合法的和非法的resource_dir 107 | List assetsLegalResourceDirArray = new ArrayList(); 108 | List fontsLegalResourceDirArray = new ArrayList(); 109 | List illegalResourceDirArray = new ArrayList(); 110 | 111 | for (String relativeResourceDir : assetsResourceDirArray) { 112 | String resourceDir = flutterProjectRootDir + "/" + relativeResourceDir; 113 | File dir = new File(resourceDir); 114 | if(dir.isDirectory() && dir.exists()) { 115 | assetsLegalResourceDirArray.add(resourceDir); 116 | } else { 117 | illegalResourceDirArray.add(resourceDir); 118 | } 119 | } 120 | 121 | for (String relativeResourceDir : fontsResourceDirArray) { 122 | String resourceDir = flutterProjectRootDir + "/" + relativeResourceDir; 123 | File dir = new File(resourceDir); 124 | if(dir.isDirectory() && dir.exists()) { 125 | fontsLegalResourceDirArray.add(resourceDir); 126 | } else { 127 | illegalResourceDirArray.add(resourceDir); 128 | } 129 | } 130 | 131 | int legalResourceDirCount = assetsLegalResourceDirArray.size() + fontsLegalResourceDirArray.size(); 132 | if(legalResourceDirCount > 0) { 133 | List> resourceDirResultTuple = new ArrayList>(); 134 | resourceDirResultTuple.add(assetsLegalResourceDirArray); 135 | resourceDirResultTuple.add(fontsLegalResourceDirArray); 136 | resourceDirResultTuple.add(illegalResourceDirArray); 137 | 138 | return resourceDirResultTuple; 139 | } 140 | 141 | if(illegalResourceDirArray.size() > 0) { 142 | String warningText = "[!]: warning, found the following resource directory which is not existed: "; 143 | for (String resourceDir : illegalResourceDirArray) { 144 | warningText += "\n" + String.format(" - %s", resourceDir); 145 | } 146 | warningText += "\n"; 147 | flrLogConsole.println(warningText, FlrLogConsole.LogType.warning); 148 | } 149 | 150 | flrLogConsole.println("[x]: have no valid resource directories configuration in pubspec.yaml", FlrLogConsole.LogType.error); 151 | flrLogConsole.println(String.format( 152 | "[*]: please manually configure the resource directories to fix it, for example:\n" + 153 | "\u202D \n" + 154 | "\u202D flr:\n" + 155 | "\u202D core_version: %s\n" + 156 | "\u202D dartfmt_line_length: %s\n" + 157 | "\u202D # config the image and text resource directories that need to be scanned\n" + 158 | "\u202D assets:\n" + 159 | "\u202D - lib/assets/images\n" + 160 | "\u202D - lib/assets/texts\n" + 161 | "\u202D # config the font resource directories that need to be scanned\n" + 162 | "\u202D fonts:\n" + 163 | "\u202D - lib/assets/fonts\n", 164 | flrCoreVersion, dartfmtLineLengthStr 165 | ), FlrLogConsole.LogType.tips); 166 | 167 | throw FlrException.ILLEGAL_ENV; 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | # Flr Plugin 2 | 3 | ![java](https://img.shields.io/badge/language-java-orange.svg) [![jetbrains plugin version](https://img.shields.io/jetbrains/plugin/v/13789-flr) ![jetbrains plugin downloads](https://img.shields.io/jetbrains/plugin/d/13789-flr)](https://plugins.jetbrains.com/plugin/13789-flr) 4 | 5 | `Flr`(Flutter-R)Plugin:一个Flutter资源管理器AndroidStudio插件,用于帮助Flutter开发者在修改项目资源后,可以自动为资源添加声明到 `pubspec.yaml` 以及生成`r.g.dart`文件。借助`r.g.dart`,Flutter开发者可以在代码中通过资源ID函数的方式应用资源。 6 | 7 | ![Flr Usage Example](README_Assets/flr-usage-example.gif) 8 | 9 | 10 | 📖 *其他语言版本:[English](README.md)、 [简体中文](README.zh-cn.md)* 11 | 12 | ## Feature 13 | - 支持“自动添加资源声明到 `pubspec.yaml` 和自动生成`r.g.dart`文件”的自动化服务,该服务可以通过手动触发,也可以通过监控资源变化触发 14 | - 支持`R.x`(如 `R.image.test()`,`R.svg.test(width: 100, height: 100)`,`R.txt.test_json()`)的代码结构 15 | - 支持处理图片资源( `.png`、 `.jpg`、 `.jpeg`、`.gif`、 `.webp`、`.icon`、`.bmp`、`.wbmp`、`.svg` ) 16 | - 支持处理文本资源(`.txt`、`.json`、`.yaml`、`.xml`) 17 | - 支持处理字体资源(`.ttf`、`.otf`、`.ttc`) 18 | - 支持处理[图片资源变体](https://flutter.dev/docs/development/ui/assets-and-images#asset-variants) 19 | - 支持处理带有坏味道的文件名的资源: 20 | - 文件名带有非法字符,如空格、`~`、`#` 等(非法字符是指不在合法字符集合内的字符;合法字符集合的字符有:`0-9`、`A-Z`、 `a-z`、 `_`、`+`、`-`、`.`、`·`、 `!`、 `@`、 `&`、`$`、`¥`) 21 | - 文件名以数字或者`_`或者`$`字符开头 22 | - 支持处理文件名相同但路径不同的资源 23 | 24 | ## Install Flr plugin 25 | 26 | 使用IDE的插件管理器安装该插件: 27 | 28 | Preferences > Plugins > Marketplace > Search for "Flr" > Install Plugin 29 | 30 | ![Install Flr](README_Assets/flr-install.png) 31 | 32 | ## Usage 33 | 34 | 1. 初始化你的Flutter项目:点击 Tools > Flr > Init 35 | 36 | >`Flr Init` 动作将会检测当前项目是否是一个合法的Flutter项目,并在`pubspec.yaml`中添加`Flr`的配置和[r_dart_library](https://github.com/YK-Unit/r_dart_library) 依赖库的声明。 37 | > 38 | >**注意:** 39 | > 40 | >Flutter SDK目前处于不稳定的状态,因此若你遇到`r_dart_library`的编译错误,你可以尝试通过修改`r_dart_library`的依赖版本来修复它。 41 | > 42 | >你可以根据这个[依赖版本关系表](https://github.com/YK-Unit/r_dart_library#dependency-relationship-table)来选择`r_dart_library`的正确版本。 43 | 44 | 2. 打开`pubspec.yaml`文件,找到`Flr`的配置项,然后配置需要`Flr`扫描的资源目录路径,如: 45 | 46 | ```yaml 47 | flr: 48 | core_version: 1.0.0 49 | # just use for flr-cli and flr-vscode-extension 50 | dartfmt_line_length: 80 51 | # config the image and text resource directories that need to be scanned 52 | assets: 53 | - lib/assets/images 54 | - lib/assets/texts 55 | # config the font resource directories that need to be scanned 56 | fonts: 57 | - lib/assets/fonts 58 | ``` 59 | 60 | 3. 扫描资源,声明资源以及生成`r.g.dart`:点击 Tools > Flr > Generate 61 | 62 | > 调用`Flr Generate` 动作后,`Flr`会扫描配置在`pubspec.yaml`中资源目录,然后为扫描到的资源添加声明到`pubspec.yaml`,并生成`r.g.dart`文件。 63 | > 64 | > **若你希望每次资源有变化时,`Flr`就能自动执行上述操作,你可以调用`Flr Start Monitor`动作。**(点击Tools > Flr > Start Monitor ) 65 | > 66 | > 这时,`Flr`会启动一个对配置在`pubspec.yaml`中资源目录进行持续监控的服务。若该监控服务检测有资源变化,`Flr`将会自动扫描这些资源目录,然后为扫描到的资源添加声明到`pubspec.yaml`,并生成`r.g.dart`文件。 67 | > 68 | > **你可以通过调用以下这个动作来终止当前的监控服务:`Flr Stop Monitor`。**(点击Tools > Flr > Stop Monitor) 69 | 70 | ## 推荐的flutter资源目录组织结构 71 | 72 | `Flr`推荐如下的flutter资源目录组织结构方案: 73 | 74 | - 方案一: 75 | 76 | ``` 77 | flutter_project_root_dir 78 | ├── build 79 | │ ├── .. 80 | ├── lib 81 | │ ├── assets 82 | │ │ ├── images // 所有模块的图片资源总目录 83 | │ │ │ ├── #{module} // 某个模块的图片资源总目录 84 | │ │ │ │ ├── #{main_image_asset} 85 | │ │ │ │ ├── #{variant-dir} // 某个变体版本的图片资源总目录 86 | │ │ │ │ │ ├── #{image_asset_variant} 87 | │ │ │ │ 88 | │ │ │ ├── home // home模块的图片资源总目录 89 | │ │ │ │ ├── home_badge.svg 90 | │ │ │ │ ├── home_icon.png 91 | │ │ │ │ ├── 3.0x // 3.0倍分辨率版本的图片资源总目录 92 | │ │ │ │ │ ├── home_icon.png 93 | │ │ │ │ 94 | │ │ ├── texts // 文本资源总目录 95 | │ │ │ │ // (你也可以根据模块进一步细分) 96 | │ │ │ └── test.json 97 | │ │ │ └── test.yaml 98 | │ │ │ │ 99 | │ │ ├── fonts // 所有字体家族的字体资源总目录 100 | │ │ │ ├── #{font-family} // 某个字体家族的字体资源总目录 101 | │ │ │ │ ├── #{font-family}-#{font_weight_or_style}.ttf 102 | │ │ │ │ 103 | │ │ │ ├── Amiri // Amiri字体家族的字体资源总目录 104 | │ │ │ │ ├── Amiri-Regular.ttf 105 | │ │ │ │ ├── Amiri-Bold.ttf 106 | │ │ │ │ ├── Amiri-Italic.ttf 107 | │ │ │ │ ├── Amiri-BoldItalic.ttf 108 | │ ├── .. 109 | ``` 110 | - 方案二: 111 | 112 | ``` 113 | flutter_project_root_dir 114 | ├── build 115 | │ ├── .. 116 | ├── lib 117 | │ ├── .. 118 | ├── assets 119 | │ ├── images // 所有模块的图片资源总目录 120 | │ │ ├── #{module} // 某个模块的图片资源总目录 121 | │ │ │ ├── #{main_image_asset} 122 | │ │ │ ├── #{variant-dir} // 某个变体版本的图片资源总目录 123 | │ │ │ │ ├── #{image_asset_variant} 124 | │ │ │ 125 | │ │ ├── home // home模块的图片资源总目录 126 | │ │ │ ├── home_badge.svg 127 | │ │ │ ├── home_icon.png 128 | │ │ │ ├── 3.0x // 3.0倍分辨率版本的图片资源总目录 129 | │ │ │ │ ├── home_icon.png 130 | │ │ │ 131 | │ ├── texts // 文本资源总目录 132 | │ │ │ // (你也可以根据模块进一步细分) 133 | │ │ └── test.json 134 | │ │ └── test.yaml 135 | │ │ │ 136 | │ ├── fonts // 所有字体家族的字体资源总目录 137 | │ │ ├── #{font-family} // 某个字体家族的字体资源总目录 138 | │ │ │ ├── #{font-family}-#{font_weight_or_style}.ttf 139 | │ │ │ 140 | │ │ ├── Amiri // Amiri字体家族的字体资源总目录 141 | │ │ │ ├── Amiri-Regular.ttf 142 | │ │ │ ├── Amiri-Bold.ttf 143 | │ │ │ ├── Amiri-Italic.ttf 144 | │ │ │ ├── Amiri-BoldItalic.ttf 145 | │ ├── .. 146 | ``` 147 | 148 | 149 | **需要注意的是,字体资源根目录下的组织结构必须(MUST)采用上述的组织结构:** 以字体家族名称命名子目录,然后字体家族的字体资源放在子目录下。否则,`Flr`可能无法正确扫描字体资源。 150 | ## r.g.dart 151 | 152 | 在你调用`Flr Generate`动作或者`Flr Start Monitor`动作后,`Flr`会扫描`pubspec.yaml`中配置的资源目录,并为扫描到的资源添加声明到`pubspec.yaml`,以及生成`r.g.dart`。 153 | 154 | `r.g.dart`中定义了一个资源访问接口类:`R`,让Flutter开发者在代码中可通过资源ID函数的方式应用资源,如: 155 | 156 | ```dart 157 | import 'package:flutter_r_demo/r.g.dart'; 158 | 159 | // test_sameName.png 160 | var normalImageWidget = Image( 161 | width: 200, 162 | height: 120, 163 | image: R.image.test_sameName(), 164 | ); 165 | 166 | // test_sameName.gif 167 | var gifImageWidget = Image( 168 | image: R.mage.test_sameName_gif(), 169 | ); 170 | 171 | // test.svg 172 | var svgImageWidget = Image( 173 | width: 100, 174 | height: 100, 175 | image: R.svg.test(width: 100, height: 100), 176 | ); 177 | 178 | // test.json 179 | var jsonString = await R.text.test_json(); 180 | 181 | // test.yaml 182 | var yamlString = await R.text.test_yaml(); 183 | 184 | // Amiri Font Style 185 | var amiriTextStyle = TextStyle(fontFamily: R.fontFamily.amiri); 186 | ``` 187 | 188 | ### `_R_X` class 189 | 190 | `r.g.dart`中定义了几个私有的`_R_X`资源管理类:`_R_Image`、`_R_svg`、`_R_Text`、`_R_FontFamily`。这些私有的资源管理类用于管理各自资源类型的资源ID: 191 | 192 | - `_R_Image`:管理非SVG类的图片资源( `.png`、 `.jpg`、 `.jpeg`、`.gif`、 `.webp`、`.icon`、`.bmp`、`.wbmp`)的资源ID 193 | - `_R_Svg`:管理SVG类图片资源的资源ID 194 | - `_R_Text`:管理文本资源(`.txt`、`.json`、`.yaml`、`.xml`)的资源ID 195 | - `_R_FontFamily`:管理字体资源(`.ttf`、`.otf`、`.ttc`)的资源ID 196 | 197 | ### `R` class and `R.x` struct 198 | 199 | `r.g.dart`中定义了一个资源访问接口类:`R`,用来管理公共信息,聚合`_R_X`资源管理类,和实现`R.x`的代码结构方式: 200 | 201 | ```dart 202 | /// This `R` class is generated and contains references to static asset resources. 203 | class R { 204 | /// package name: flutter_r_demo 205 | static const package = "flutter_r_demo"; 206 | 207 | /// This `R.image` struct is generated, and contains static references to static non-svg type image asset resources. 208 | static const image = _R_Image(); 209 | 210 | /// This `R.svg` struct is generated, and contains static references to static svg type image asset resources. 211 | static const svg = _R_Svg(); 212 | 213 | /// This `R.text` struct is generated, and contains static references to static text asset resources. 214 | static const text = _R_Text(); 215 | } 216 | 217 | /// This `R.fontFamily` struct is generated, and contains static references to static font resources. 218 | static const fontFamily = _R_FontFamily(); 219 | ``` 220 | 221 | ## Example 222 | 223 | 这里提供了一些示例Demo来展示如何在Flutter项目中使用`Flr`工具和在代码中如何使用`R`类: 224 | 225 | - [Flutter-R Demo](https://github.com/Fly-Mix/flutter_r_demo) 226 | 227 | - [flutter_hello_app](https://github.com/Fly-Mix/flutter_hello_app) 228 | 229 | - [flutter_hello_module](https://github.com/Fly-Mix/flutter_hello_module) 230 | 231 | - [flutter_hello_package](https://github.com/Fly-Mix/flutter_hello_package) 232 | 233 | - [flutter_hello_plugin](https://github.com/Fly-Mix/flutter_hello_plugin) 234 | 235 | 236 | ## License 237 | 238 | The plugin is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 239 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flr Plugin 2 | 3 | ![java](https://img.shields.io/badge/language-java-orange.svg) [![jetbrains plugin version](https://img.shields.io/jetbrains/plugin/v/13789-flr) ![jetbrains plugin downloads](https://img.shields.io/jetbrains/plugin/d/13789-flr)](https://plugins.jetbrains.com/plugin/13789-flr) 4 | 5 | 6 | Flr (Flutter-R) Plugin: A Flutter Resource Manager Android Studio Plugin, which can help flutter developer to auto specify assets in `pubspec.yaml` and generate `r.g.dart` file after he changes the flutter project assets. With `r.g.dart`, flutter developer can apply the asset in code by referencing it's asset ID function. 7 | 8 | ![Flr Usage Example](README_Assets/flr-usage-example.gif) 9 | 10 | 11 | 📖 *Read this in other languages: [English](README.md), [简体中文](README.zh-cn.md)* 12 | 13 | ## Feature 14 | 15 | - Support auto service that automatically specify assets in `pubspec.yaml` and generate `r.g.dart` file, which can be triggered manually or by monitoring asset changes 16 | - Support `R.x` (such as `R.image.test()`, `R.svg.test(width: 100, height: 100)`, `R.txt.test_json()`) code struct 17 | - Support for processing image assets ( `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.icon`, `.bmp`, `.wbmp`, `.svg` ) 18 | - Support for processing text assets ( `.txt`, `.json`, `.yaml`, `.xml` ) 19 | - Support for processing font assets ( `.ttf`, `.otf`, `.ttc`) 20 | - Support for processing [image asset variants](https://flutter.dev/docs/development/ui/assets-and-images#asset-variants) 21 | - Support for processing asset which’s filename is bad: 22 | - filename has illegal character (such as `blank`, `~`, `@`, `#` ) which is outside the range of valid characters (`0-9`, `A-Z`, `a-z`, `_`, `+`, `-`, `.`, `·`, `!`, `@`, `&`, `$`, `¥`) 23 | - filename begins with a number or character `_` or character`$` 24 | 25 | - Support for processing assets with the same filename but different path 26 | 27 | ## Install Flr plugin 28 | 29 | Use the IDE's plugin manager to install the latest version of the plugin: 30 | 31 | Preferences > Plugins > Marketplace > Search for "Flr" > Install Plugin 32 | 33 | ![Install Flr](README_Assets/flr-install.png) 34 | 35 | ## Usage 36 | 37 | 1. Init your flutter project: Click Tools > Flr > Init 38 | 39 | > The `Flr Init` action will check to see if the current project is a legal flutter project, add flr configuration and dependency [r_dart_library](https://github.com/YK-Unit/r_dart_library) into `pubspec.yaml`. 40 | > 41 | > **Attention:** 42 | > 43 | > The Flutter SDK is currently in an unstable state, so if you get a build error of `r_dart_library` , you can fix it by modify the dependent version of `r_dart_library`. 44 | > 45 | > You can select the correct version of `r_dart_library` based on this [dependency relationship table](https://github.com/YK-Unit/r_dart_library#dependency-relationship-table). 46 | 47 | 2. Open `pubspec.yaml` file, find the configuration item for `Flr`, and then configure the resource directory that needs to be scanned by `Flr`, such as: 48 | 49 | ```yaml 50 | flr: 51 | core_version: 1.0.0 52 | # just use for flr-cli and flr-vscode-extension 53 | dartfmt_line_length: 80 54 | # config the image and text resource directories that need to be scanned 55 | assets: 56 | - lib/assets/images 57 | - lib/assets/texts 58 | # config the font resource directories that need to be scanned 59 | fonts: 60 | - lib/assets/fonts 61 | ``` 62 | 63 | 3. Scan assets, specify assets, and generate `r.g.dart`: Click Tools > Flr > Generate 64 | 65 | > After invoke `Flr Generate` action, `Flr` will scan the resource directories configured in `pubspec.yaml`, then specify scanned assets in `pubspec.yaml`, and generate `r.g.dart` file. 66 | > 67 | > **If you want `Flr` to do the above operations automatically every time a asset changes, you can invoke `Flr Start Monitor` action.** (Click Tools > Flr > Start Monitor ) 68 | > Then `Flr` will launche a monitoring service that continuously monitors resource directories configured in `pubspec.yaml`. If the service detects any asset changes, `Flr` will automatically scan the asset directories, then specify scanned assets in pubspec.yaml, and generate "r.g.dart" file. 69 | > 70 | > **You can terminate this monitoring service by invoke `Flr Stop Monitor` action.** (Click Tools > Flr > Stop Monitor ) 71 | 72 | ## Recommended Flutter Resource Structure 73 | 74 | `Flr` the following flutter resource structure schemes: 75 | 76 | - scheme 1: 77 | 78 | ``` 79 | flutter_project_root_dir 80 | ├── build 81 | │ ├── .. 82 | ├── lib 83 | │ ├── assets 84 | │ │ ├── images // image resource directory of all modules 85 | │ │ │ ├── #{module} // image resource directory of a module 86 | │ │ │ │ ├── #{main_image_asset} 87 | │ │ │ │ ├── #{variant-dir} // image resource directory of a variant 88 | │ │ │ │ │ ├── #{image_asset_variant} 89 | │ │ │ │ 90 | │ │ │ ├── home // image resource directory of home module 91 | │ │ │ │ ├── home_badge.svg 92 | │ │ │ │ ├── home_icon.png 93 | │ │ │ │ ├── 3.0x // image resource directory of a 3.0x-ratio-variant 94 | │ │ │ │ │ ├── home_icon.png 95 | │ │ │ │ 96 | │ │ ├── texts // text resource directory 97 | │ │ │ │ // (you can also break it down further by module) 98 | │ │ │ └── test.json 99 | │ │ │ └── test.yaml 100 | │ │ │ │ 101 | │ │ ├── fonts // font resource directory of all font-families 102 | │ │ │ ├── #{font-family} // font resource directory of a font-family 103 | │ │ │ │ ├── #{font-family}-#{font_weight_or_style}.ttf 104 | │ │ │ │ 105 | │ │ │ ├── Amiri // font resource directory of Amiri font-family 106 | │ │ │ │ ├── Amiri-Regular.ttf 107 | │ │ │ │ ├── Amiri-Bold.ttf 108 | │ │ │ │ ├── Amiri-Italic.ttf 109 | │ │ │ │ ├── Amiri-BoldItalic.ttf 110 | │ ├── .. 111 | 112 | ``` 113 | - scheme 2: 114 | ``` 115 | flutter_project_root_dir 116 | ├── build 117 | │ ├── .. 118 | ├── lib 119 | │ ├── .. 120 | ├── assets 121 | │ ├── images // image resource directory of all modules 122 | │ │ ├── #{module} // image resource directory of a module 123 | │ │ │ ├── #{main_image_asset} 124 | │ │ │ ├── #{variant-dir} // image resource directory of a variant 125 | │ │ │ │ ├── #{image_asset_variant} 126 | │ │ │ 127 | │ │ ├── home // image resource directory of home module 128 | │ │ │ ├── home_badge.svg 129 | │ │ │ ├── home_icon.png 130 | │ │ │ ├── 3.0x // image resource directory of a 3.0x-ratio-variant 131 | │ │ │ │ ├── home_icon.png 132 | │ │ │ 133 | │ ├── texts // text resource directory 134 | │ │ │ // (you can also break it down further by module) 135 | │ │ └── test.json 136 | │ │ └── test.yaml 137 | │ │ │ 138 | │ ├── fonts // font resource directory of all font-families 139 | │ │ ├── #{font-family} // font resource directory of a font-family 140 | │ │ │ ├── #{font-family}-#{font_weight_or_style}.ttf 141 | │ │ │ 142 | │ │ ├── Amiri // font resource directory of Amiri font-family 143 | │ │ │ ├── Amiri-Regular.ttf 144 | │ │ │ ├── Amiri-Bold.ttf 145 | │ │ │ ├── Amiri-Italic.ttf 146 | │ │ │ ├── Amiri-BoldItalic.ttf 147 | │ ├── .. 148 | 149 | ``` 150 | 151 | 152 | **Big Attention, the resource structure in the root directory of the font resource MUST follow the structure described above:** name the subdirectory with a font family name, and place the font resources of the font family in the subdirectory. Otherwise, `Flr` may not scan the font resource correctly. 153 | 154 | ## r.g.dart 155 | 156 | After you invoke `Flr Generate` action or `Flr Start Monitor` action, Flr will scan the asset directories configured in `pubspec.yaml`, then specify scanned assets in `pubspec.yaml`, and generate `r.g.dart` file. 157 | 158 | `r.g.dart` defines a asset access interface class: `R`, which allows flutter developer to apply the asset in code by referencing it's asset ID function, such as: 159 | 160 | ```dart 161 | import 'package:flutter_r_demo/r.g.dart'; 162 | 163 | // test_sameName.png 164 | var normalImageWidget = Image( 165 | width: 200, 166 | height: 120, 167 | image: R.image.test_sameName(), 168 | ); 169 | 170 | // test_sameName.gif 171 | var gifImageWidget = Image( 172 | image: R.mage.test_sameName_gif(), 173 | ); 174 | 175 | // test.svg 176 | var svgImageWidget = Image( 177 | width: 100, 178 | height: 100, 179 | image: R.svg.test(width: 100, height: 100), 180 | ); 181 | 182 | // test.json 183 | var jsonString = await R.text.test_json(); 184 | 185 | // test.yaml 186 | var yamlString = await R.text.test_yaml(); 187 | 188 | // Amiri Font Style 189 | var amiriTextStyle = TextStyle(fontFamily: R.fontFamily.amiri); 190 | ``` 191 | 192 | ### `_R_X` class 193 | 194 | `r.g.dart` defines several private `_R_X` asset management classes: `_R_Image`, `_R_Svg`, `_R_Text`, `_R_FontFamily`. These private asset management classes are used to manage the asset IDs of the respective asset types: 195 | 196 | - `_R_Image`: manage the asset IDs of non-svg type image assets ( `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.icon`, `.bmp`, `.wbmp` ) 197 | - `_R_Svg`: manage the asset IDs of svg type image assets 198 | - `_R_Text`: manage the asset IDs of text assets ( `.txt`, `.json`, `.yaml`, `.xml` ) 199 | - `_R_FontFamily`: manage the asset IDs of font assets ( `.ttf`, `.otf`, `.ttc`) 200 | 201 | ### `R` class and `R.x` struct 202 | 203 | `r.g.dart` defines a asset access interface class: `R`, which is used to manage common information, aggregate the `_R_X` asset management classes, and implement `R.x` code struct: 204 | 205 | ```dart 206 | /// This `R` class is generated and contains references to static asset resources. 207 | class R { 208 | /// package name: flutter_r_demo 209 | static const package = "flutter_r_demo"; 210 | 211 | /// This `R.image` struct is generated, and contains static references to static non-svg type image asset resources. 212 | static const image = _R_Image(); 213 | 214 | /// This `R.svg` struct is generated, and contains static references to static svg type image asset resources. 215 | static const svg = _R_Svg(); 216 | 217 | /// This `R.text` struct is generated, and contains static references to static text asset resources. 218 | static const text = _R_Text(); 219 | } 220 | 221 | /// This `R.fontFamily` struct is generated, and contains static references to static font resources. 222 | static const fontFamily = _R_FontFamily(); 223 | } 224 | ``` 225 | 226 | ## Example 227 | 228 | Here are some example demos to show how to use Flr tool in flutter project and show how to use `R` class in your code: 229 | 230 | - [Flutter-R Demo](https://github.com/Fly-Mix/flutter_r_demo) 231 | 232 | - [flutter_hello_app](https://github.com/Fly-Mix/flutter_hello_app) 233 | 234 | - [flutter_hello_module](https://github.com/Fly-Mix/flutter_hello_module) 235 | 236 | - [flutter_hello_package](https://github.com/Fly-Mix/flutter_hello_package) 237 | 238 | - [flutter_hello_plugin](https://github.com/Fly-Mix/flutter_hello_plugin) 239 | 240 | ## License 241 | 242 | The plugin is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT). 243 | -------------------------------------------------------------------------------- /src/main/java/com/flr/command/util/FlrAssetUtil.java: -------------------------------------------------------------------------------- 1 | package com.flr.command.util; 2 | 3 | // import com.intellij.history.core.Paths; 4 | import com.intellij.openapi.vfs.LocalFileSystem; 5 | import com.intellij.openapi.vfs.VirtualFile; 6 | import org.jetbrains.annotations.NotNull; 7 | 8 | import java.io.File; 9 | // import java.nio.file.Path; 10 | import java.util.*; 11 | import java.util.regex.Pattern; 12 | 13 | /* 14 | * 资产相关的工具类方法 15 | * */ 16 | public class FlrAssetUtil { 17 | 18 | /* 19 | * 判断当前的资源文件是不是资产变体(asset_variant)类型 20 | * 21 | * 判断的核心算法是: 22 | * - 获取资源文件的父目录; 23 | * - 判断父目录是否符合资产变体目录的特征 24 | * 资产变体映射的的资源文件要求存放在“与 main_asset 在同一个目录下的”、“符合指定特征的”子目录中; 25 | * 截止目前,Flutter只支持一种变体类型:倍率变体; 26 | * 倍率变体只适用于非SVG类图片资源; 27 | * 倍率变体目录特征可使用此正则来判断:“^((0\.[0-9]+)|([1-9]+[0-9]*(\.[0-9]+)?))[x]$”; 28 | * 倍率变体目录名称示例:“0.5x”、“1.5x”、“2.0x”、“3.0x”,“2x”、“3x”; 29 | * 30 | * */ 31 | public static boolean isAssetVariant(@NotNull VirtualFile legalResourceFile) { 32 | if(FlrFileUtil.isNonSvgImageResourceFile(legalResourceFile)) { 33 | VirtualFile parentDirFile = legalResourceFile.getParent(); 34 | String parentDirName = parentDirFile.getName(); 35 | 36 | String ratioRegex = "^((0\\.[0-9]+)|([1-9]+[0-9]*(\\.[0-9]+)?))[x]$"; 37 | Pattern pattern = Pattern.compile(ratioRegex); 38 | if(pattern.matcher(parentDirName).matches()) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | } 45 | 46 | /* 47 | * 判断当前资产是不是图片类资产 48 | * 49 | * === Examples 50 | * 51 | * === Example-1 52 | * asset = "packages/flutter_r_demo/assets/images/test.png" 53 | * @return true 54 | * 55 | * === Example-2 56 | * asset = "assets/images/test.png" 57 | * @return true 58 | * 59 | * */ 60 | public static boolean isImageAsset(@NotNull String asset) { 61 | File file = new File(asset); 62 | if(FlrFileUtil.isImageResourceFile(file)) { 63 | return true; 64 | } 65 | 66 | return false; 67 | } 68 | 69 | /* 70 | * 判断当前资产是不是package类资产 71 | * 72 | * === Examples 73 | * 74 | * === Example-1 75 | * asset = "packages/flutter_r_demo/assets/images/test.png" 76 | * @return true 77 | * 78 | * === Example-2 79 | * asset = "assets/images/test.png" 80 | * @return false 81 | * 82 | * */ 83 | public static boolean isPackageAsset(@NotNull String asset) { 84 | String packagePrefix = "packages/"; 85 | if(asset.startsWith(packagePrefix)) { 86 | return true; 87 | } 88 | 89 | return false; 90 | } 91 | 92 | /* 93 | * 判断当前资产是不是指定的package的资产 94 | * 95 | * === Examples 96 | * 97 | * === Example-1 98 | * asset = "packages/flutter_r_demo/assets/images/test.png" 99 | * @return true 100 | * 101 | * === Example-2 102 | * asset = "assets/images/test.png" 103 | * @return false 104 | * 105 | * */ 106 | public static boolean isSpecifiedPackageAsset(@NotNull String packageName, @NotNull String asset) { 107 | String specifiedPackagePrefix = "packages/" + packageName + "/"; 108 | if(asset.startsWith(specifiedPackagePrefix)) { 109 | return true; 110 | } 111 | 112 | return false; 113 | } 114 | 115 | /* 116 | * 获取指定flutter工程的asset对应的主资源文件 117 | * 注意:主资源文件不一定存在,比如图片资产可能只存在变体资源文件 118 | * 119 | * === Examples 120 | * flutter_project_dir = "~/path/to/flutter_r_demo" 121 | * package_name = "flutter_r_demo" 122 | * 123 | * === Example-1 124 | * asset = "packages/flutter_r_demo/assets/images/test.png" 125 | * main_resource_file = "~/path/to/flutter_r_demo/lib/assets/images/test.png" 126 | * 127 | * === Example-2 128 | * asset = "assets/images/test.png" 129 | * main_resource_file = "~/path/to/flutter_r_demo/assets/images/test.png" 130 | * 131 | * */ 132 | public static File getMainResourceFile(@NotNull String flutterProjectDir, @NotNull String packageName, @NotNull String asset) { 133 | if(isSpecifiedPackageAsset(packageName, asset)) { 134 | String specifiedPackagePrefix = "packages/" + packageName + "/"; 135 | 136 | // asset: packages/flutter_r_demo/assets/images/test.png 137 | // to get impliedRelativeResourceFile: lib/assets/images/test.png 138 | String impliedRelativeResourceFile = asset.replaceFirst(specifiedPackagePrefix, ""); 139 | impliedRelativeResourceFile = "lib/" + impliedRelativeResourceFile; 140 | 141 | // mainResourceFile: ~/path/to/flutter_r_demo/lib/assets/images/test.png 142 | String mainResourceFilePath = flutterProjectDir + "/" + impliedRelativeResourceFile; 143 | File mainResourceFile = new File(mainResourceFilePath); 144 | return mainResourceFile; 145 | } else { 146 | // asset: assets/images/test.png 147 | // mainResourceFile: ~/path/to/flutter_r_demo/assets/images/test.png 148 | String mainResourceFilePath = flutterProjectDir + "/" + asset; 149 | File mainResourceFile = new File(mainResourceFilePath); 150 | return mainResourceFile; 151 | } 152 | } 153 | 154 | /* 155 | * 判断指定flutter工程的asset是不是存在;存在的判断标准是:asset需要存在对应的资源文件 156 | * 157 | * === Examples 158 | * flutter_project_dir = "~/path/to/flutter_r_demo" 159 | * package_name = "flutter_r_demo" 160 | * 161 | * === Example-1 162 | * asset = "packages/flutter_r_demo/assets/images/test.png" 163 | * @return true 164 | * 165 | * === Example-2 166 | * asset = "packages/flutter_r_demo/404/not-existed.png" 167 | * @return false 168 | * 169 | * */ 170 | public static boolean isAssetExisted(@NotNull String flutterProjectDir, @NotNull String packageName, @NotNull String asset) { 171 | // 处理指定flutter工程的asset 172 | // 1. 获取asset对应的main_resource_file 173 | // 2. 若main_resource_file是非SVG类图片资源文件,判断asset是否存在的标准是:主资源文件或者至少一个变体资源文件存在 174 | // 3. 若main_resource_file是SVG类图片资源文件或者其他资源文件,判断asset是否存在的标准是:主资源文件存在 175 | // 176 | 177 | File mainResourceFile = getMainResourceFile(flutterProjectDir, packageName, asset); 178 | if(FlrFileUtil.isNonSvgImageResourceFile(mainResourceFile)) { 179 | if(mainResourceFile.exists()) { 180 | return true; 181 | } 182 | 183 | String fileBaseName = mainResourceFile.getName(); 184 | String fileDir = mainResourceFile.getParent(); 185 | boolean didFindVariantResourceFile = false; 186 | File resourceDirFile = new File(fileDir); 187 | 188 | VirtualFile resourceDirVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(resourceDirFile); 189 | if(resourceDirVirtualFile == null) { 190 | return false; 191 | } 192 | VirtualFile[] resourceDirChildren = resourceDirVirtualFile.getChildren(); 193 | for(VirtualFile resourceDirChild: resourceDirChildren) { 194 | if(resourceDirChild.isDirectory()) { 195 | VirtualFile[] subResourceDirChildren = resourceDirChild.getChildren(); 196 | for (VirtualFile subResourceDirChild: subResourceDirChildren) { 197 | if(subResourceDirChild.isDirectory()) { 198 | continue; 199 | } 200 | 201 | if(!isAssetVariant(subResourceDirChild)) { 202 | continue; 203 | } 204 | 205 | String variantResourceFileBaseName = subResourceDirChild.getName(); 206 | if(fileBaseName.equals(variantResourceFileBaseName)) { 207 | didFindVariantResourceFile = true; 208 | } 209 | } 210 | } 211 | } 212 | 213 | if(didFindVariantResourceFile) { 214 | return true; 215 | } 216 | 217 | } else { 218 | if(mainResourceFile.exists()) { 219 | return true; 220 | } 221 | } 222 | 223 | return false; 224 | } 225 | 226 | /* 227 | * 为当前资源文件生成 main_asset 228 | * 229 | * === Examples 230 | * flutterProjectDir = "~/path/to/flutter_r_demo" 231 | * packageName = "flutter_r_demo" 232 | * 233 | * === Example-1 234 | * legalResourceFile = "~/path/to/flutter_r_demo/lib/assets/images/test.png" 235 | * mainAsset = "packages/flutter_r_demo/assets/images/test.png" 236 | * 237 | * === Example-2 238 | * legalResourceFile = "~/path/to/flutter_r_demo/lib/assets/images/3.0x/test.png" 239 | * mainAsset = "packages/flutter_r_demo/assets/images/test.png" 240 | * 241 | * === Example-3 242 | * legalResourceFile = "~/path/to/flutter_r_demo/lib/assets/texts/3.0x/test.json" 243 | * mainAsset = "packages/flutter_r_demo/assets/texts/3.0x/test.json" 244 | * 245 | * === Example-3 246 | * legalResourceFile = "~/path/to/flutter_r_demo/lib/assets/fonts/Amiri/Amiri-Regular.ttf" 247 | * mainAsset = "packages/flutter_r_demo/fonts/Amiri/Amiri-Regular.ttf" 248 | * 249 | * === Example-4 250 | * legalResourceFile = "~/path/to/flutter_r_demo/assets/images/test.png" 251 | * mainAsset = "assets/images/test.png" 252 | * 253 | * === Example-5 254 | * legalResourceFile = "~/path/to/flutter_r_demo/assets/images/3.0x/test.png" 255 | * mainAsset = "assets/images/test.png" 256 | * 257 | * */ 258 | public static String generateMainAsset(@NotNull String flutterProjectDir, @NotNull String packageName, @NotNull VirtualFile legalResourceFile) { 259 | // legalResourceFile: ~/path/to/flutter_r_demo/lib/assets/images/3.0x/test.png 260 | // to get mainResourceFile: ~/path/to/flutter_r_demo/lib/assets/images/test.png 261 | String mainResourceFile = legalResourceFile.getPath(); 262 | if(isAssetVariant(legalResourceFile)) { 263 | // test.png 264 | String fileBasename = legalResourceFile.getName(); 265 | // ~/path/to/flutter_r_demo/lib/assets/images/3.0x 266 | VirtualFile parentDirVirtualFile = legalResourceFile.getParent(); 267 | 268 | //to get mainResourceFileDir: ~/path/to/flutter_r_demo/lib/assets/images 269 | VirtualFile mainResourceFileDirVirtualFile = parentDirVirtualFile.getParent(); 270 | String mainResourceFileDir = mainResourceFileDirVirtualFile.getPath(); 271 | 272 | // ~/path/to/flutter_r_demo/lib/assets/images/test.png 273 | mainResourceFile = String.format("%s/%s",mainResourceFileDir,fileBasename); 274 | } 275 | 276 | // mainResourceFile: ~/path/to/flutter_r_demo/lib/assets/images/test.png 277 | // to get mainRelativeResourceFile: lib/assets/images/test.png 278 | String mainRelativeResourceFile = mainResourceFile.replaceFirst(flutterProjectDir + "/", ""); 279 | 280 | // 判断 mainRelativeResourceFile 是不是 impliedResourceFile 类型 281 | // impliedResourceFile 的定义是:放置在 "lib/" 目录内 resource_file 282 | // 具体实现是:mainRelativeResourceFile 的前缀若是 "lib/" ,则其是 impliedResourceFile 类型; 283 | // 284 | // impliedResourceFile 生成 mainAsset 的算法是: mainAsset = "packages/#{packageName}/#{assetName}" 285 | // non-impliedResourceFile 生成 mainAsset 的算法是: mainAsset = "#{assetName}" 286 | // 287 | String libPrefix = "lib/"; 288 | if(mainRelativeResourceFile.startsWith(libPrefix)) { 289 | // mainRelativeResourceFile: lib/assets/images/test.png 290 | // to get assetName: assets/images/test.png 291 | String assetName = mainRelativeResourceFile.replaceFirst(libPrefix, ""); 292 | 293 | // mainAsset: packages/flutter_r_demo/assets/images/test.png 294 | String mainAsset = "packages/" + packageName + "/" + assetName; 295 | return mainAsset; 296 | } else { 297 | // mainRelativeResourceFile: assets/images/test.png 298 | // to get assetName: assets/images/test.png 299 | String assetName = mainRelativeResourceFile; 300 | 301 | // mainAsset: assets/images/test.png 302 | String mainAsset = assetName; 303 | return mainAsset; 304 | } 305 | } 306 | 307 | /* 308 | * 遍历指定资源目录下扫描找到的legalImageFile数组生成imageAsset数组 309 | * */ 310 | public static List generateImageAssets(@NotNull String flutterProjectDir, @NotNull String packageName, @NotNull List legalImageFileArray) { 311 | Set imageAssetSet = new LinkedHashSet(); 312 | 313 | for (VirtualFile imageVirtualFile : legalImageFileArray) { 314 | String imageAsset = generateMainAsset(flutterProjectDir, packageName, imageVirtualFile); 315 | imageAssetSet.add(imageAsset); 316 | } 317 | 318 | List imageAssetArray = new ArrayList(imageAssetSet); 319 | return imageAssetArray; 320 | } 321 | 322 | /* 323 | * 遍历指定资源目录下扫描找到的legalTextFile数组生成textAsset数组 324 | * */ 325 | public static List generateTextAssets(@NotNull String flutterProjectDir, @NotNull String packageName, @NotNull List legalTextFileArray) { 326 | Set textAssetSet = new LinkedHashSet(); 327 | 328 | for (VirtualFile textVirtualFile : legalTextFileArray) { 329 | String textAsset = generateMainAsset(flutterProjectDir, packageName, textVirtualFile); 330 | textAssetSet.add(textAsset); 331 | } 332 | 333 | List textAssetArray = new ArrayList(textAssetSet); 334 | return textAssetArray; 335 | } 336 | 337 | /* 338 | * 遍历指定资源目录下扫描找到的legalFontFile数组生成fontAssetConfig数组 339 | * 340 | * fontAssetConfig = {"asset": "packages/flutter_r_demo/assets/fonts/Amiri/Amiri-Regular.ttf"} 341 | * */ 342 | public static List generateFontAssetConfigs(@NotNull String flutterProjectDir, @NotNull String packageName, @NotNull List legalFontFileArray) { 343 | List fontAssetConfigArray = new ArrayList(); 344 | 345 | for (VirtualFile fontVirtualFile : legalFontFileArray) { 346 | String fontAsset = generateMainAsset(flutterProjectDir, packageName, fontVirtualFile); 347 | 348 | Map fontAssetConfig = new LinkedHashMap(); 349 | fontAssetConfig.put("asset", fontAsset); 350 | 351 | fontAssetConfigArray.add(fontAssetConfig); 352 | } 353 | 354 | return fontAssetConfigArray; 355 | } 356 | 357 | /* 358 | * 合并新旧2个asset数组: 359 | * - old_asset_array - new_asset_array = diff_asset_array,获取old_asset_array与new_asset_array的差异集合 360 | * - 遍历diff_asset_array,筛选合法的asset得到legal_old_asset_array;合法的asset标准是:非图片资源 + 存在对应的资源文件 361 | * - 按照字典序对legal_old_asset_array进行排序,并追加到new_asset_array 362 | * - 返回合并结果merged_asset_array 363 | * 364 | * === Examples 365 | * flutter_project_dir = "~/path/to/flutter_r_demo" 366 | * package_name = "flutter_r_demo" 367 | * new_asset_array = ["packages/flutter_r_demo/assets/images/test.png", "packages/flutter_r_demo/assets/jsons/test.json"] 368 | * old_asset_array = ["packages/flutter_r_demo/assets/htmls/test.html"] 369 | * merged_asset_array = ["packages/flutter_r_demo/assets/images/test.png", "packages/flutter_r_demo/assets/jsons/test.json", "packages/flutter_r_demo/assets/htmls/test.html"] 370 | * 371 | * */ 372 | public static List mergeFlutterAssets(@NotNull String flutterProjectDir, @NotNull String packageName, @NotNull List newAssetArray, @NotNull List oldAssetArray) { 373 | List legalOldAssetArray = new ArrayList<>(); 374 | 375 | List diffAssetArray = oldAssetArray; 376 | diffAssetArray.removeAll(newAssetArray); 377 | for(String asset: diffAssetArray) { 378 | // 若是第三方package的资源,newAssetArray 379 | // 引用第三方package的资源的推荐做法是:通过引用第三方package的R类来访问 380 | if(isPackageAsset(asset)) { 381 | if(!isSpecifiedPackageAsset(packageName, asset)) { 382 | legalOldAssetArray.add(asset); 383 | continue; 384 | } 385 | } 386 | 387 | // 处理指定flutter工程的asset 388 | // 1. 判断asset是否存在 389 | // 2. 若asset存在,则合并到new_asset_array 390 | // 391 | if(isAssetExisted(flutterProjectDir, packageName, asset)) { 392 | legalOldAssetArray.add(asset); 393 | } 394 | } 395 | 396 | Collections.sort(legalOldAssetArray); 397 | List mergedAssetArray = newAssetArray; 398 | mergedAssetArray.addAll(legalOldAssetArray); 399 | return mergedAssetArray; 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/main/java/com/flr/command/util/FlrFileUtil.java: -------------------------------------------------------------------------------- 1 | package com.flr.command.util; 2 | 3 | import com.flr.FlrConstant; 4 | import com.flr.FlrException; 5 | import com.flr.logConsole.FlrLogConsole; 6 | import com.intellij.openapi.project.Project; 7 | import com.intellij.openapi.vfs.*; 8 | import org.jetbrains.annotations.NotNull; 9 | import org.yaml.snakeyaml.DumperOptions; 10 | import org.yaml.snakeyaml.Yaml; 11 | 12 | import java.io.*; 13 | import java.nio.file.Files; 14 | import java.nio.file.Paths; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.regex.Pattern; 19 | 20 | /* 21 | * 资源文件相关的工具类方法 22 | * */ 23 | public class FlrFileUtil { 24 | 25 | /* 26 | * 获取flutter主工程的所有子工程的根目录 27 | * */ 28 | public static List getFlutterSubProjectRootDirs(@NotNull String flutterMainProjectRootDir) { 29 | List flutterSubProjectRootDirArray = new ArrayList<>(); 30 | 31 | File resourceDirFile = new File(flutterMainProjectRootDir); 32 | VirtualFile resourceDirVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(resourceDirFile); 33 | if(resourceDirVirtualFile == null) { 34 | return flutterSubProjectRootDirArray; 35 | } 36 | 37 | String pubspecFileName = "pubspec.yaml"; 38 | VirtualFile[] resourceDirChildren = resourceDirVirtualFile.getChildren(); 39 | for(VirtualFile resourceDirChild: resourceDirChildren) { 40 | if(resourceDirChild.isDirectory()) { 41 | VirtualFile[] subResourceDirChildren = resourceDirChild.getChildren(); 42 | for (VirtualFile subResourceDirChild: subResourceDirChildren) { 43 | if(subResourceDirChild.isDirectory()) { 44 | continue; 45 | } 46 | 47 | String fileBaseName = subResourceDirChild.getName(); 48 | if(fileBaseName.equals(pubspecFileName)) { 49 | String flutterSubProjectRootDir = subResourceDirChild.getParent().getPath(); 50 | flutterSubProjectRootDirArray.add(flutterSubProjectRootDir); 51 | break; 52 | } 53 | } 54 | } 55 | } 56 | 57 | return flutterSubProjectRootDirArray; 58 | } 59 | 60 | /* 61 | * 获取指定flutter工程的pubspec.yaml文件的路径 62 | * */ 63 | public static String getPubspecFilePath(@NotNull String flutterProjectDir) { 64 | String pubspecFilePath = flutterProjectDir + "/pubspec.yaml"; 65 | return pubspecFilePath; 66 | } 67 | 68 | /* 69 | * 判断指定flutter工程的工程类型是不是Package工程类型 70 | * 71 | * flutter工程共有4种工程类型: 72 | * - app:Flutter App工程,用于开发纯Flutter的App 73 | * - module:Flutter Component工程,用于开发Flutter组件以嵌入iOS和Android原生工程 74 | * - package:General Dart Package工程,用于开发一个供应用层开发者使用的包 75 | * - plugin:Plugin Package工程(属于特殊的Dart Package工程),用于开发一个调用特定平台API的包* 76 | * 77 | * flutter工程的工程类型可从flutter工程目录的 .metadata 文件中读取获得 78 | * 如果不存在 .metadata 文件,则判断 pubspec.yaml 是否存在 author 配置,若存在,说明是一个 Package工程 79 | * */ 80 | public static boolean isPackageProjectType(@NotNull FlrLogConsole flrLogConsole, @NotNull String flutterProjectDir) { 81 | String metadataFilePath = flutterProjectDir + "/.metadata"; 82 | File metadataFile = new File(metadataFilePath); 83 | 84 | if(metadataFile.exists()) { 85 | try { 86 | Yaml yaml = new Yaml(); 87 | InputStream inputStream = new FileInputStream(metadataFile); 88 | Iterable itr = yaml.loadAll(inputStream); 89 | Map metadataConfig = null; 90 | for (Object obj : itr) { 91 | if(obj instanceof Map) { 92 | metadataConfig = (Map)obj; 93 | break; 94 | } 95 | } 96 | 97 | String projectType = (String) metadataConfig.get("project_type"); 98 | if(projectType instanceof String == false) { 99 | projectType = "unknown"; 100 | } 101 | 102 | if(projectType.equals("package") || projectType.equals("plugin")) { 103 | return true; 104 | } 105 | } catch (Exception e) { 106 | e.printStackTrace(); 107 | flrLogConsole.println(e.getMessage(), FlrLogConsole.LogType.normal); 108 | flrLogConsole.println("", FlrLogConsole.LogType.normal); 109 | } 110 | } else { 111 | String warningStr = "[!]: warning, metadata file is missed, flr can not make sure to get a right project type of this flutter project" 112 | + "\n" 113 | + "[!]: then flr maybe generate buggy r.g.dart"; 114 | flrLogConsole.println(warningStr, FlrLogConsole.LogType.warning); 115 | String tipsStr = String.format("[*]: to fix it, you can manually copy the metadata file of a flutter project with same project type to %s\n", metadataFilePath); 116 | flrLogConsole.println(tipsStr, FlrLogConsole.LogType.tips); 117 | 118 | 119 | String pubspecFilePath = FlrFileUtil.getPubspecFilePath(flutterProjectDir); 120 | File pubspecFile = new File(pubspecFilePath); 121 | try { 122 | Map pubspecConfig = FlrFileUtil.loadPubspecConfigFromFile(flrLogConsole, pubspecFile); 123 | if(pubspecConfig.containsKey("author")) { 124 | return true; 125 | } 126 | } catch (FlrException e) { 127 | e.printStackTrace(); 128 | flrLogConsole.println(e.getMessage(), FlrLogConsole.LogType.normal); 129 | flrLogConsole.println("", FlrLogConsole.LogType.normal); 130 | } 131 | } 132 | 133 | return false; 134 | } 135 | 136 | public static String getFileBasename(File file) { 137 | return file.getName(); 138 | } 139 | 140 | public static String getFileBasenameWithoutExtension(File file) { 141 | if(file == null) { 142 | return null; 143 | } 144 | 145 | String fileBasename = file.getName(); 146 | int lastIndexOf = fileBasename.lastIndexOf("."); 147 | if (lastIndexOf == -1) { 148 | return fileBasename; 149 | } 150 | String fileBasenameWithoutExtension = fileBasename.substring(0, lastIndexOf); 151 | return fileBasenameWithoutExtension; 152 | } 153 | 154 | public static String getFileExtension(File file) { 155 | if(file == null) { 156 | return null; 157 | } 158 | 159 | String fileBasename = file.getName(); 160 | int lastIndexOf = fileBasename.lastIndexOf("."); 161 | if (lastIndexOf == -1) { 162 | return ""; // empty extension 163 | } 164 | String fileExtension = fileBasename.substring(lastIndexOf); 165 | return fileExtension; 166 | } 167 | 168 | /* 169 | * 把resourceDir转换为relativeResourceDir 170 | * 171 | * === Examples 172 | * flutterProjectDir = "~/path/to/flutter_r_demo" 173 | * resourceDir = "~/path/to/flutter_r_demo/lib/assets/images" 174 | * relativeResourceDir = "lib/assets/images" 175 | * */ 176 | public static String convertToRelativeResourceDir(@NotNull String flutterProjectDir, @NotNull String resourceDir) { 177 | String relativeResourceDir = resourceDir.replaceFirst(flutterProjectDir + "/", ""); 178 | return relativeResourceDir; 179 | } 180 | 181 | /* 182 | * 把resourceDir数组转换为relativeResourceDir数组 183 | * 184 | * === Examples 185 | * flutterProjectDir = "~/path/to/flutter_r_demo" 186 | * resourceDirArray = ["~/path/to/flutter_r_demo/lib/assets/images"] 187 | * relativeResourceDirArray = ["lib/assets/images"] 188 | * */ 189 | public static List convertToRelativeResourceDirs(@NotNull String flutterProjectDir, @NotNull List resourceDirs) { 190 | List relativeResourceDirArray = new ArrayList<>(); 191 | for(String resourceDir : resourceDirs) { 192 | String relativeResourceDir = convertToRelativeResourceDir(flutterProjectDir, resourceDir); 193 | relativeResourceDirArray.add(relativeResourceDir); 194 | } 195 | return relativeResourceDirArray; 196 | } 197 | 198 | /* 199 | * 写文件,并刷新文件 200 | * */ 201 | public static void writeContentToFile(@com.sun.istack.NotNull Project project, @com.sun.istack.NotNull String content, @com.sun.istack.NotNull File file) throws FlrException { 202 | if(file.exists() == false) { 203 | try { 204 | // 创建文件,并同步加载文件到工程 205 | file.createNewFile(); 206 | List ioFiles = new ArrayList(); 207 | ioFiles.add(file); 208 | LocalFileSystem.getInstance().refreshIoFiles(ioFiles); 209 | } catch (IOException e) { 210 | e.printStackTrace(); 211 | FlrException flrException = new FlrException(e.getMessage()); 212 | throw flrException; 213 | } 214 | } 215 | //Use try-with-resource to get auto-closeable writer instance 216 | try (BufferedWriter writer = Files.newBufferedWriter(Paths.get(file.getPath()))) 217 | { 218 | writer.write(content); 219 | } catch (IOException e) { 220 | e.printStackTrace(); 221 | FlrException flrException = new FlrException(e.getMessage()); 222 | throw flrException; 223 | } 224 | 225 | VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file); 226 | if(virtualFile == null) { 227 | return; 228 | } 229 | virtualFile.refresh(false, false); 230 | } 231 | 232 | /* 233 | * 读取pubspec.yaml到 pubspecConfig 234 | * 若读取成功,返回一个字典对象pubspecConfig 235 | * 若读取失败,则返回null 236 | * */ 237 | public static Map loadPubspecConfigFromFile(@NotNull FlrLogConsole flrLogConsole, @NotNull File pubspecFile) throws FlrException { 238 | try { 239 | Yaml yaml = new Yaml(); 240 | InputStream inputStream = new FileInputStream(pubspecFile); 241 | Iterable itr = yaml.loadAll(inputStream); 242 | Map pubspecConfig = null; 243 | for (Object obj : itr) { 244 | if(obj instanceof Map) { 245 | pubspecConfig = (Map)obj; 246 | break; 247 | } 248 | } 249 | return pubspecConfig; 250 | } catch (Exception e) { 251 | e.printStackTrace(); 252 | flrLogConsole.println(e.getMessage(), FlrLogConsole.LogType.normal); 253 | flrLogConsole.println("", FlrLogConsole.LogType.normal); 254 | flrLogConsole.println("[x]: pubspec.yaml is damaged, maybe it has some syntax errors", FlrLogConsole.LogType.error); 255 | flrLogConsole.println(String.format("[*]: please correct the pubspec.yaml file at %s", pubspecFile), FlrLogConsole.LogType.tips); 256 | throw FlrException.ILLEGAL_ENV; 257 | } 258 | } 259 | 260 | /* 261 | * 保存pubspecConfig到pubspec.yaml,并刷新pubspec.yaml 262 | * */ 263 | public static void dumpPubspecConfigToFile(Map pubspecConfig, File pubspecFile) { 264 | try { 265 | DumperOptions dumperOptions = new DumperOptions(); 266 | dumperOptions.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); 267 | dumperOptions.setIndent(2); 268 | dumperOptions.setIndicatorIndent(0); 269 | Yaml yaml = new Yaml(dumperOptions); 270 | FileWriter writer = new FileWriter(pubspecFile); 271 | yaml.dump(pubspecConfig, writer); 272 | 273 | VirtualFile pubspecVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(pubspecFile); 274 | if(pubspecVirtualFile == null) { 275 | return; 276 | } 277 | pubspecVirtualFile.refresh(false, false); 278 | } catch (Exception e) { 279 | e.printStackTrace(); 280 | } 281 | } 282 | 283 | /* 284 | * 判断当前资源文件是否合法 285 | * 286 | * 判断资源文件合法的标准是: 287 | * 其file_basename_no_extension 由字母(a-z、A-Z)、数字(0-9)、其他合法字符('_', '+', '-', '.', '·', '!', '@', '&', '$', '¥')组成 288 | * 289 | * */ 290 | public static boolean isLegalResourceFile(@NotNull VirtualFile virtualFile) { 291 | String fileBasenameWithoutExtension = virtualFile.getNameWithoutExtension(); 292 | 293 | Pattern pattern = Pattern.compile("^[a-zA-Z0-9_\\+\\-\\.·!@&$¥]+$"); 294 | if(pattern.matcher(fileBasenameWithoutExtension).matches()) { 295 | return true; 296 | } 297 | 298 | return false; 299 | } 300 | 301 | public static boolean isLegalResourceFile(@NotNull File file) { 302 | String fileBasenameWithoutExtension = getFileBasenameWithoutExtension(file); 303 | 304 | Pattern pattern = Pattern.compile("^[a-zA-Z0-9_\\+\\-\\.·!@&$¥]+$"); 305 | if(pattern.matcher(fileBasenameWithoutExtension).matches()) { 306 | return true; 307 | } 308 | 309 | return false; 310 | } 311 | 312 | public static boolean isNonSvgImageResourceFile(@NotNull VirtualFile virtualFile) { 313 | // virtualFileExtension 不带“.”,如 path_to/test.png 的 virtualFileExtension 是 png 314 | String virtualFileExtension = virtualFile.getExtension(); 315 | String fullVirtualFileExtension = "." + virtualFileExtension; 316 | fullVirtualFileExtension = fullVirtualFileExtension.toLowerCase(); 317 | if(FlrConstant.NON_SVG_IMAGE_FILE_TYPES.contains(fullVirtualFileExtension)) { 318 | return true; 319 | } 320 | return false; 321 | } 322 | 323 | public static boolean isNonSvgImageResourceFile(@NotNull File file) { 324 | String fileExtName = getFileExtension(file).toLowerCase(); 325 | if(FlrConstant.NON_SVG_IMAGE_FILE_TYPES.contains(fileExtName)) { 326 | return true; 327 | } 328 | return false; 329 | } 330 | 331 | public static boolean isSvgImageResourceFile(@NotNull VirtualFile virtualFile) { 332 | // virtualFileExtension 不带“.”,如 path_to/test.png 的 virtualFileExtension 是 png 333 | String virtualFileExtension = virtualFile.getExtension(); 334 | String fullVirtualFileExtension = "." + virtualFileExtension; 335 | fullVirtualFileExtension = fullVirtualFileExtension.toLowerCase(); 336 | if(FlrConstant.SVG_IMAGE_FILE_TYPES.contains(fullVirtualFileExtension)) { 337 | return true; 338 | } 339 | return false; 340 | } 341 | 342 | public static boolean isSvgImageResourceFile(@NotNull File file) { 343 | String fileExtName = getFileExtension(file).toLowerCase(); 344 | if(FlrConstant.SVG_IMAGE_FILE_TYPES.contains(fileExtName)) { 345 | return true; 346 | } 347 | return false; 348 | } 349 | 350 | public static boolean isImageResourceFile(@NotNull VirtualFile virtualFile) { 351 | // virtualFileExtension 不带“.”,如 path_to/test.png 的 virtualFileExtension 是 png 352 | String virtualFileExtension = virtualFile.getExtension(); 353 | String fullVirtualFileExtension = "." + virtualFileExtension; 354 | fullVirtualFileExtension = fullVirtualFileExtension.toLowerCase(); 355 | if(FlrConstant.IMAGE_FILE_TYPES.contains(fullVirtualFileExtension)) { 356 | return true; 357 | } 358 | return false; 359 | } 360 | 361 | public static boolean isImageResourceFile(@NotNull File file) { 362 | String fileExtName = getFileExtension(file).toLowerCase(); 363 | if(FlrConstant.IMAGE_FILE_TYPES.contains(fileExtName)) { 364 | return true; 365 | } 366 | return false; 367 | } 368 | 369 | public static boolean isTextResourceFile(@NotNull VirtualFile virtualFile) { 370 | // virtualFileExtension 不带“.”,如 path_to/test.png 的 virtualFileExtension 是 png 371 | String virtualFileExtension = virtualFile.getExtension(); 372 | String fullVirtualFileExtension = "." + virtualFileExtension; 373 | fullVirtualFileExtension = fullVirtualFileExtension.toLowerCase(); 374 | if(FlrConstant.TEXT_FILE_TYPES.contains(fullVirtualFileExtension)) { 375 | return true; 376 | } 377 | return false; 378 | } 379 | 380 | public static boolean isTextResourceFile(@NotNull File file) { 381 | String fileExtName = getFileExtension(file).toLowerCase(); 382 | if(FlrConstant.TEXT_FILE_TYPES.contains(fileExtName)) { 383 | return true; 384 | } 385 | return false; 386 | } 387 | 388 | public static boolean isFontResourceFile(@NotNull VirtualFile virtualFile) { 389 | // virtualFileExtension 不带“.”,如 path_to/test.png 的 virtualFileExtension 是 png 390 | String virtualFileExtension = virtualFile.getExtension(); 391 | String fullVirtualFileExtension = "." + virtualFileExtension; 392 | fullVirtualFileExtension = fullVirtualFileExtension.toLowerCase(); 393 | if(FlrConstant.FONT_FILE_TYPES.contains(fullVirtualFileExtension)) { 394 | return true; 395 | } 396 | return false; 397 | } 398 | 399 | public static boolean isFontResourceFile(@NotNull File file) { 400 | String fileExtName = getFileExtension(file).toLowerCase(); 401 | if(FlrConstant.FONT_FILE_TYPES.contains(fileExtName)) { 402 | return true; 403 | } 404 | return false; 405 | } 406 | 407 | /* 408 | * 扫描指定的资源目录和其所有层级的子目录,查找所有图片文件 409 | * 返回文本文件结果二元组 imageFileResultTuple 410 | * imageFileResultTuple = [legalImageFileArray, illegalImageFileArray] 411 | * 412 | * 判断资源文件合法的标准参考:isLegalResourceFile 方法 413 | * 414 | * === Examples 415 | * resourceDir = "~/path/to/flutter_project/lib/assets/images" 416 | * legalImageFileArray = ["~/path/to/flutter_project/lib/assets/images/test.png", "~/path/to/flutter_project/lib/assets/images/2.0x/test.png"] 417 | * illegalImageFileArray = ["~/path/to/flutter_project/lib/assets/images/~.png"] 418 | * */ 419 | public static List> findImageFiles(@NotNull String resourceDir) { 420 | List legalImageFileArray = new ArrayList(); 421 | List illegalImageFileArray = new ArrayList(); 422 | 423 | File resourceDirFile = new File(resourceDir); 424 | VirtualFile resourceDirVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(resourceDirFile); 425 | VfsUtilCore.visitChildrenRecursively(resourceDirVirtualFile, new VirtualFileVisitor(){ 426 | @Override 427 | public boolean visitFile(@NotNull VirtualFile file) { 428 | if (file.isDirectory() == false && isImageResourceFile(file)) { 429 | if(isLegalResourceFile(file)) { 430 | legalImageFileArray.add(file); 431 | } else { 432 | illegalImageFileArray.add(file); 433 | } 434 | return true; 435 | } 436 | return super.visitFile(file); 437 | } 438 | }); 439 | 440 | List> imageFileResultTuple = new ArrayList>(); 441 | imageFileResultTuple.add(legalImageFileArray); 442 | imageFileResultTuple.add(illegalImageFileArray); 443 | return imageFileResultTuple; 444 | } 445 | 446 | /* 447 | * 扫描指定的资源目录和其所有层级的子目录,查找所有文本文件 448 | * 返回图片文件结果二元组 textFileResultTuple 449 | * textFileResultTuple = [legalTextFileArray, illegalTextFileArray] 450 | * 451 | * 判断资源文件合法的标准参考:isLegalResourceFile 方法 452 | * 453 | * === Examples 454 | * resourceDir = "~/path/to/flutter_project/lib/assets/jsons" 455 | * legalTextFileArray = ["~/path/to/flutter_project/lib/assets/jsons/city.json", "~/path/to/flutter_project/lib/assets/jsons/mock/city.json"] 456 | * illegalTextFileArray = ["~/path/to/flutter_project/lib/assets/jsons/~.json"] 457 | * */ 458 | public static List> findTextFiles(@NotNull String resourceDir) { 459 | List legalTextFileArray = new ArrayList(); 460 | List illegalTextFileArray = new ArrayList(); 461 | 462 | File resourceDirFile = new File(resourceDir); 463 | VirtualFile resourceDirVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(resourceDirFile); 464 | VfsUtilCore.visitChildrenRecursively(resourceDirVirtualFile, new VirtualFileVisitor(){ 465 | @Override 466 | public boolean visitFile(@NotNull VirtualFile file) { 467 | if (file.isDirectory() == false && isTextResourceFile(file)) { 468 | if(isLegalResourceFile(file)) { 469 | legalTextFileArray.add(file); 470 | } else { 471 | illegalTextFileArray.add(file); 472 | } 473 | return true; 474 | } 475 | return super.visitFile(file); 476 | } 477 | }); 478 | 479 | List> textFileResultTuple = new ArrayList>(); 480 | textFileResultTuple.add(legalTextFileArray); 481 | textFileResultTuple.add(illegalTextFileArray); 482 | return textFileResultTuple; 483 | } 484 | 485 | /* 486 | * 扫描指定的资源目录,返回其所有第一级子目录 487 | * 488 | * === Examples 489 | * resourceDir = "~/path/to/flutter_project/lib/assets/fonts" 490 | * topChildDirArray = ["~/path/to/flutter_project/lib/assets/fonts/Amiri", "~/path/to/flutter_project/lib/assets/fonts/Open_Sans"] 491 | * */ 492 | public static List findTopChildDirs(@NotNull String resourceDir) { 493 | List topChildDirArray = new ArrayList(); 494 | 495 | File resourceDirFile = new File(resourceDir); 496 | VirtualFile resourceDirVirtualFile = LocalFileSystem.getInstance().findFileByIoFile(resourceDirFile); 497 | VirtualFile[] resourceDirChildren = resourceDirVirtualFile.getChildren(); 498 | for(VirtualFile resourceDirChild: resourceDirChildren) { 499 | if(resourceDirChild.isDirectory()) { 500 | topChildDirArray.add(resourceDirChild); 501 | } 502 | } 503 | 504 | return topChildDirArray; 505 | } 506 | 507 | /* 508 | * 扫描指定的字体家族目录和其所有层级的子目录,查找所有字体文件 509 | * 返回字体文件结果二元组 fontFileResultTuple 510 | * textFileResultTuple = [legalFontFileArray, illegalFontFileArray] 511 | * 512 | * 判断资源文件合法的标准参考:isLegalResourceFile 方法 513 | * 514 | * === Examples 515 | * fontFamilyDirFile = "~/path/to/flutter_project/lib/assets/fonts/Amiri" 516 | * legalFontFileArray = ["~/path/to/flutter_project/lib/assets/fonts/Amiri/Amiri-Regular.ttf"] 517 | * illegalFontFileArray = ["~/path/to/flutter_project/lib/assets/fonts/Amiri/~.ttf"] 518 | * */ 519 | public static List> findFontFilesInFontFamilyDir(@NotNull VirtualFile fontFamilyDirFile) { 520 | List legalFontFileArray = new ArrayList(); 521 | List illegalFontFileArray = new ArrayList(); 522 | 523 | VfsUtilCore.visitChildrenRecursively(fontFamilyDirFile, new VirtualFileVisitor(){ 524 | @Override 525 | public boolean visitFile(@NotNull VirtualFile file) { 526 | if (file.isDirectory() == false && isFontResourceFile(file)) { 527 | if(isLegalResourceFile(file)) { 528 | legalFontFileArray.add(file); 529 | } else { 530 | illegalFontFileArray.add(file); 531 | } 532 | return true; 533 | } 534 | return super.visitFile(file); 535 | } 536 | }); 537 | 538 | List> fontFileResultTuple = new ArrayList>(); 539 | fontFileResultTuple.add(legalFontFileArray); 540 | fontFileResultTuple.add(illegalFontFileArray); 541 | return fontFileResultTuple; 542 | } 543 | } 544 | -------------------------------------------------------------------------------- /src/main/java/com/flr/command/util/FlrCodeUtil.java: -------------------------------------------------------------------------------- 1 | package com.flr.command.util; 2 | 3 | import com.flr.FlrConstant; 4 | import org.jetbrains.annotations.NotNull; 5 | 6 | import java.io.File; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.regex.Pattern; 10 | import java.util.stream.Collectors; 11 | 12 | /* 13 | * 代码生成相关的工具类方法 14 | * */ 15 | public class FlrCodeUtil { 16 | /* 17 | * 根据模板生成 R class 的代码 18 | * */ 19 | public static String generate_R_class(@NotNull String packageName) { 20 | String code = String.format("// IT IS GENERATED BY FLR - DO NOT MODIFY BY HAND\n" + 21 | "// YOU CAN GET MORE DETAILS ABOUT FLR FROM:\n" + 22 | "// - https://github.com/Fly-Mix/flr-cli\n" + 23 | "// - https://github.com/Fly-Mix/flr-vscode-extension\n" + 24 | "// - https://github.com/Fly-Mix/flr-as-plugin\n" + 25 | "//\n" + 26 | "\n" + 27 | "// ignore: unused_import\n" + 28 | "import 'package:flutter/widgets.dart';\n" + 29 | "// ignore: unused_import\n" + 30 | "import 'package:flutter/services.dart' show rootBundle;\n" + 31 | "// ignore: unused_import\n" + 32 | "import 'package:path/path.dart' as path;\n" + 33 | "// ignore: unused_import\n" + 34 | "import 'package:flutter_svg/flutter_svg.dart';\n" + 35 | "// ignore: unused_import\n" + 36 | "import 'package:r_dart_library/asset_svg.dart';\n" + 37 | "\n" + 38 | "/// This `R` class is generated and contains references to static asset resources.\n" + 39 | "class R {\n" + 40 | " /// package name: %s\n" + 41 | " static const package = \"%s\";\n" + 42 | "\n" + 43 | " /// This `R.image` struct is generated, and contains static references to static non-svg type image asset resources.\n" + 44 | " static const image = _R_Image();\n" + 45 | "\n" + 46 | " /// This `R.svg` struct is generated, and contains static references to static svg type image asset resources.\n" + 47 | " static const svg = _R_Svg();\n" + 48 | "\n" + 49 | " /// This `R.text` struct is generated, and contains static references to static text asset resources.\n" + 50 | " static const text = _R_Text();\n" + 51 | "\n" + 52 | " /// This `R.fontFamily` struct is generated, and contains static references to static font asset resources.\n" + 53 | " static const fontFamily = _R_FontFamily();\n" + 54 | "}", 55 | packageName, 56 | packageName); 57 | 58 | return code; 59 | } 60 | 61 | /* 62 | * 根据模板生成 AssetResource class 的代码 63 | * */ 64 | public static String generate_AssetResource_class(@NotNull String packageName, @NotNull boolean shouldSupportNullsafety) { 65 | String code = "/// Asset resource’s metadata class.\n" + 66 | "/// For example, here is the metadata of `packages/flutter_demo/assets/images/example.png` asset:\n" + 67 | "/// - packageName:flutter_demo\n" + 68 | "/// - assetName:assets/images/example.png\n" + 69 | "/// - fileDirname:assets/images\n" + 70 | "/// - fileBasename:example.png\n" + 71 | "/// - fileBasenameNoExtension:example\n" + 72 | "/// - fileExtname:.png\n"; 73 | if (shouldSupportNullsafety) { 74 | code += "class AssetResource {\n" + 75 | " /// Creates an object to hold the asset resource’s metadata.\n" + 76 | " const AssetResource(this.assetName, {this.packageName});\n" + 77 | "\n" + 78 | " /// The name of the main asset from the set of asset resources to choose from.\n" + 79 | " final String assetName;\n" + 80 | "\n" + 81 | " /// The name of the package from which the asset resource is included.\n" + 82 | " final String? packageName;\n" + 83 | "\n" + 84 | " /// The name used to generate the key to obtain the asset resource. For local assets\n" + 85 | " /// this is [assetName], and for assets from packages the [assetName] is\n" + 86 | " /// prefixed 'packages//'.\n" + 87 | " String get keyName => packageName == null ? assetName : \"packages/$packageName/$assetName\";\n" + 88 | "\n" + 89 | " /// The file basename of the asset resource.\n" + 90 | " String get fileBasename {\n" + 91 | " final basename = path.basename(assetName);\n" + 92 | " return basename;\n" + 93 | " }\n" + 94 | "\n" + 95 | " /// The no extension file basename of the asset resource.\n" + 96 | " String get fileBasenameNoExtension {\n" + 97 | " final basenameWithoutExtension = path.basenameWithoutExtension(assetName);\n" + 98 | " return basenameWithoutExtension;\n" + 99 | " }\n" + 100 | "\n" + 101 | " /// The file extension name of the asset resource.\n" + 102 | " String get fileExtname {\n" + 103 | " final extension = path.extension(assetName);\n" + 104 | " return extension;\n" + 105 | " }\n" + 106 | "\n" + 107 | " /// The directory path name of the asset resource.\n" + 108 | " String get fileDirname {\n" + 109 | " var dirname = assetName;\n" + 110 | " if (packageName != null) {\n" + 111 | " final packageStr = \"packages/$packageName/\";\n" + 112 | " dirname = dirname.replaceAll(packageStr, \"\");\n" + 113 | " }\n" + 114 | " final filenameStr = \"$fileBasename/\";\n" + 115 | " dirname = dirname.replaceAll(filenameStr, \"\");\n" + 116 | " return dirname;\n" + 117 | " }\n" + 118 | "}"; 119 | } else { 120 | code += "class AssetResource {\n" + 121 | " /// Creates an object to hold the asset resource’s metadata.\n" + 122 | " const AssetResource(this.assetName, {this.packageName}) : assert(assetName != null);\n" + 123 | "\n" + 124 | " /// The name of the main asset from the set of asset resources to choose from.\n" + 125 | " final String assetName;\n" + 126 | "\n" + 127 | " /// The name of the package from which the asset resource is included.\n" + 128 | " final String packageName;\n" + 129 | "\n" + 130 | " /// The name used to generate the key to obtain the asset resource. For local assets\n" + 131 | " /// this is [assetName], and for assets from packages the [assetName] is\n" + 132 | " /// prefixed 'packages//'.\n" + 133 | " String get keyName => packageName == null ? assetName : \"packages/$packageName/$assetName\";\n" + 134 | "\n" + 135 | " /// The file basename of the asset resource.\n" + 136 | " String get fileBasename {\n" + 137 | " final basename = path.basename(assetName);\n" + 138 | " return basename;\n" + 139 | " }\n" + 140 | "\n" + 141 | " /// The no extension file basename of the asset resource.\n" + 142 | " String get fileBasenameNoExtension {\n" + 143 | " final basenameWithoutExtension = path.basenameWithoutExtension(assetName);\n" + 144 | " return basenameWithoutExtension;\n" + 145 | " }\n" + 146 | "\n" + 147 | " /// The file extension name of the asset resource.\n" + 148 | " String get fileExtname {\n" + 149 | " final extension = path.extension(assetName);\n" + 150 | " return extension;\n" + 151 | " }\n" + 152 | "\n" + 153 | " /// The directory path name of the asset resource.\n" + 154 | " String get fileDirname {\n" + 155 | " var dirname = assetName;\n" + 156 | " if (packageName != null) {\n" + 157 | " final packageStr = \"packages/$packageName/\";\n" + 158 | " dirname = dirname.replaceAll(packageStr, \"\");\n" + 159 | " }\n" + 160 | " final filenameStr = \"$fileBasename/\";\n" + 161 | " dirname = dirname.replaceAll(filenameStr, \"\");\n" + 162 | " return dirname;\n" + 163 | " }\n" + 164 | "}"; 165 | } 166 | 167 | return code; 168 | } 169 | 170 | /* 171 | * 为asset生成assetId;其中priorAssetType为优先的资产类型,其决定了当前asset的assetId是否带有资产类型信息。 172 | * 如priorAssetType为".png", 173 | * 这时候若asset为“packages/flutter_demo/assets/images/test.png”,这时其生成assetId为“test”,不带有类型信息; 174 | * 这时候若asset为“packages/flutter_demo/assets/images/test.jpg”,这时其生成assetId为“test_jpg”,带有类型信息; 175 | * 176 | * @param asset 指定的资产 177 | * @param usedAssetIdArray 已使用的assetId数组 178 | * @param priorAssetType 优先的资产类型,默认值为null,代表“.*”,意味生成的assetId总是带有资产类型信息 179 | * @return assetId 资产ID 180 | * */ 181 | public static String generateAssetId(@NotNull String asset, List usedAssetIdArray, String priorAssetType) { 182 | File assetFile = new File(asset); 183 | String fileExtName = FlrFileUtil.getFileExtension(assetFile).toLowerCase(); 184 | String fileBasenameWithoutExtension = FlrFileUtil.getFileBasenameWithoutExtension(assetFile); 185 | 186 | String assetId = fileBasenameWithoutExtension; 187 | if(priorAssetType == null || priorAssetType.equals(fileExtName) == false) { 188 | String extInfo = "_" + fileExtName.substring(1); 189 | assetId = fileBasenameWithoutExtension + extInfo; 190 | } 191 | 192 | // 过滤非法字符 193 | assetId = assetId.replaceAll("[^a-zA-Z0-9_$]", "_"); 194 | 195 | // 检测首字符是不是字母; 196 | // 若是字母,则检测其是不是大写字母,若是,则转换为小写字母; 197 | // 若不是字母,则添加一个前缀字母“a” 198 | Character firstChar = assetId.charAt(0); 199 | if(Character.isLetter(firstChar)) { 200 | if(Character.isUpperCase(firstChar)) { 201 | String firstCharStr = firstChar.toString().toLowerCase(); 202 | assetId = firstCharStr + assetId.substring(1); 203 | } 204 | } else { 205 | String firstCharStr = "a"; 206 | assetId = firstCharStr + assetId; 207 | } 208 | 209 | // 处理 asset_id 重名的情况 210 | if(usedAssetIdArray != null 211 | && usedAssetIdArray.isEmpty() == false 212 | && usedAssetIdArray.contains(assetId)) { 213 | // 当前asset_id重名次数,初始值为1 214 | int repeatCount = 1; 215 | 216 | // 查找当前asset_id衍生出来的asset_id_brother(id兄弟) 217 | // asset_id_brother = #{asset_id}$#{repeat_count} 218 | // 其中,repeat_count >= 1 219 | // 220 | // Example: 221 | // asset_id = test 222 | // asset_id_brother = test$1 223 | // 224 | 225 | String idBrotherRegex = String.format("^%s\\$[1-9][0-9]*$", assetId); 226 | Pattern pattern = Pattern.compile(idBrotherRegex); 227 | List curAssetIdBrothers = usedAssetIdArray.stream().filter(pattern.asPredicate()).collect(Collectors.toList()); 228 | 229 | if(curAssetIdBrothers != null && curAssetIdBrothers.isEmpty() == false) { 230 | repeatCount += curAssetIdBrothers.size(); 231 | } 232 | 233 | assetId = String.format("%s$%d",assetId, repeatCount); 234 | } 235 | 236 | return assetId; 237 | } 238 | 239 | /* 240 | * 为当前asset生成注释 241 | * 242 | * === Examples 243 | * packageName = "flutter_r_demo" 244 | * 245 | * === Example-1 246 | * asset = "packages/flutter_r_demo/assets/images/test.png" 247 | * assetComment = "asset: lib/assets/images/test.png" 248 | * 249 | * === Example-2 250 | * asset = "assets/images/test.png" 251 | * assetComment = "asset: assets/images/test.png" 252 | * 253 | * */ 254 | public static String generateAssetComment(@NotNull String asset,@NotNull String packageName) { 255 | String packagesPrefix = "packages/" + packageName + "/"; 256 | 257 | if(asset.startsWith(packagesPrefix)) { 258 | // asset: packages/flutter_r_demo/assets/images/test.png 259 | // to get assetName: assets/images/test.png 260 | String assetName = asset.replaceFirst(packagesPrefix, ""); 261 | 262 | String assetComment = "asset: lib/" + assetName; 263 | return assetComment; 264 | } else { 265 | // asset: assets/images/test.png 266 | // to get assetName: assets/images/test.png 267 | String assetName = asset; 268 | 269 | String assetComment = "asset: " + assetName; 270 | return assetComment; 271 | } 272 | } 273 | 274 | /* 275 | * 为当前 asset 生成 AssetResource property 的代码 276 | * */ 277 | public static String generate_AssetResource_property(@NotNull String asset, @NotNull Map assetIdDict, @NotNull String packageName, boolean isPackageProjectType, String priorAssetType) { 278 | String assetId = assetIdDict.get(asset); 279 | String assetComment = generateAssetComment(asset, packageName); 280 | 281 | String assetName = ""; 282 | boolean needPackage = false; 283 | 284 | String packagesPrefix = "packages/" + packageName + "/"; 285 | if(asset.startsWith(packagesPrefix)) { 286 | // asset: packages/flutter_r_demo/assets/images/test.png 287 | // to get assetName: assets/images/test.png 288 | assetName = asset.replaceFirst(packagesPrefix, "");; 289 | needPackage = true; 290 | } else { 291 | // asset: assets/images/test.png 292 | // to get assetName: assets/images/test.png 293 | assetName = asset; 294 | 295 | if (isPackageProjectType) { 296 | needPackage = true; 297 | } else { 298 | needPackage = false; 299 | } 300 | } 301 | 302 | // 对字符串中的 '$' 进行转义处理:'$' -> '\$' 303 | // assetName: assets/images/test$.png 304 | // to get escapedAssetName: assets/images/test\$.png 305 | String escapedAssetName = assetName.replace("$", "\\$"); 306 | 307 | if(needPackage) { 308 | String code = String.format(" /// %s\n" + 309 | " // ignore: non_constant_identifier_names\n" + 310 | " final %s = const AssetResource(\"%s\", packageName: R.package);", 311 | assetComment, 312 | assetId, 313 | escapedAssetName) ; 314 | 315 | return code; 316 | } else { 317 | String code = String.format(" /// %s\n" + 318 | " // ignore: non_constant_identifier_names\n" + 319 | " final %s = const AssetResource(\"%s\", packageName: null);", 320 | assetComment, 321 | assetId, 322 | escapedAssetName) ; 323 | 324 | return code; 325 | } 326 | } 327 | 328 | /* 329 | * 根据模板,为 nonSvgImageAssetArray(非svg类的图片资产数组)生成 _R_Image_AssetResource class 的代码 330 | * */ 331 | public static String generate__R_Image_AssetResource_class(@NotNull List nonSvgImageAssetArray, @NotNull Map nonSvgImageAssetIdDict, @NotNull String packageName, boolean isPackageProjectType) { 332 | 333 | String all_g_AssetResource_property_code = ""; 334 | 335 | for (String asset : nonSvgImageAssetArray) { 336 | all_g_AssetResource_property_code += "\n"; 337 | String g_AssetResource_property_code = generate_AssetResource_property(asset, nonSvgImageAssetIdDict, packageName, isPackageProjectType, FlrConstant.PRIOR_NON_SVG_IMAGE_FILE_TYPE); 338 | all_g_AssetResource_property_code += g_AssetResource_property_code; 339 | } 340 | 341 | String code = String.format("// ignore: camel_case_types\n" + 342 | "class _R_Image_AssetResource {\n" + 343 | " const _R_Image_AssetResource();\n" + 344 | "%s\n" + 345 | "}", 346 | all_g_AssetResource_property_code); 347 | return code; 348 | } 349 | 350 | /* 351 | * 根据模板,为 svgImageAssetArray(svg类的图片资产数组)生成 _R_Svg_AssetResource class 的代码 352 | * */ 353 | public static String generate__R_Svg_AssetResource_class(@NotNull List svgImageAssetArray, @NotNull Map svgImageAssetIdDict, @NotNull String packageName, boolean isPackageProjectType) { 354 | 355 | String all_g_AssetResource_property_code = ""; 356 | 357 | for (String asset : svgImageAssetArray) { 358 | all_g_AssetResource_property_code += "\n"; 359 | String g_AssetResource_property_code = generate_AssetResource_property(asset, svgImageAssetIdDict, packageName, isPackageProjectType, FlrConstant.PRIOR_SVG_IMAGE_FILE_TYPE); 360 | all_g_AssetResource_property_code += g_AssetResource_property_code; 361 | } 362 | 363 | String code = String.format("// ignore: camel_case_types\n" + 364 | "class _R_Svg_AssetResource {\n" + 365 | " const _R_Svg_AssetResource();\n" + 366 | "%s\n" + 367 | "}", 368 | all_g_AssetResource_property_code); 369 | return code; 370 | } 371 | 372 | /* 373 | * 根据模板,为 textAssetArray(文本资产数组)生成 _R_Text_AssetResource class 的代码 374 | * */ 375 | public static String generate__R_Text_AssetResource_class(@NotNull List textAssetArray, @NotNull Map textAssetIdDict, @NotNull String packageName, boolean isPackageProjectType) { 376 | 377 | String all_g_AssetResource_property_code = ""; 378 | 379 | for (String asset : textAssetArray) { 380 | all_g_AssetResource_property_code += "\n"; 381 | String g_AssetResource_property_code = generate_AssetResource_property(asset, textAssetIdDict, packageName, isPackageProjectType, FlrConstant.PRIOR_TEXT_FILE_TYPE); 382 | all_g_AssetResource_property_code += g_AssetResource_property_code; 383 | } 384 | 385 | String code = String.format("// ignore: camel_case_types\n" + 386 | "class _R_Text_AssetResource {\n" + 387 | " const _R_Text_AssetResource();\n" + 388 | "%s\n" + 389 | "}", 390 | all_g_AssetResource_property_code); 391 | return code; 392 | } 393 | 394 | /* 395 | * 根据模板,为 nonSvgImageAssetArray(非svg类的图片资产数组)生成 _R_Image class 的代码 396 | * */ 397 | public static String generate__R_Image_class(@NotNull List nonSvgImageAssetArray, @NotNull Map nonSvgImageAssetIdDict, @NotNull String packageName) { 398 | String all_g_Asset_method_code = ""; 399 | 400 | for (String asset : nonSvgImageAssetArray) { 401 | all_g_Asset_method_code += "\n"; 402 | 403 | String assetId = nonSvgImageAssetIdDict.get(asset); 404 | String assetComment = generateAssetComment(asset, packageName); 405 | 406 | String g_Asset_method_code = String.format(" /// %s\n" + 407 | " // ignore: non_constant_identifier_names\n" + 408 | " AssetImage %s() {\n" + 409 | " return AssetImage(asset.%s.keyName);\n" + 410 | " }", 411 | assetComment, 412 | assetId, 413 | assetId); 414 | 415 | all_g_Asset_method_code += g_Asset_method_code; 416 | } 417 | 418 | String code = String.format("/// This `_R_Image` class is generated and contains references to static non-svg type image asset resources.\n" + 419 | "// ignore: camel_case_types\n" + 420 | "class _R_Image {\n" + 421 | " const _R_Image();\n" + 422 | "\n" + 423 | " final asset = const _R_Image_AssetResource();\n" + 424 | "%s\n" + 425 | "}", 426 | all_g_Asset_method_code); 427 | 428 | return code; 429 | } 430 | 431 | /* 432 | * 根据模板,为 svgImageAssetArray(svg类的图片资产数组)生成 _R_Svg class 的代码 433 | * */ 434 | public static String generate__R_Svg_class(@NotNull List svgImageAssetArray, @NotNull Map svgImageAssetIdDict, @NotNull String packageName, @NotNull boolean shouldSupportNullsafety) { 435 | String all_g_Asset_method_code = ""; 436 | 437 | for (String asset : svgImageAssetArray) { 438 | all_g_Asset_method_code += "\n"; 439 | 440 | String assetId = svgImageAssetIdDict.get(asset); 441 | String assetComment = generateAssetComment(asset, packageName); 442 | String g_Asset_method_code = ""; 443 | 444 | if (shouldSupportNullsafety) { 445 | g_Asset_method_code = String.format(" /// %s\n" + 446 | " // ignore: non_constant_identifier_names\n" + 447 | " AssetSvg %s({required double width, required double height}) {\n" + 448 | " final imageProvider = AssetSvg(asset.%s.keyName, width: width, height: height);\n" + 449 | " return imageProvider;\n" + 450 | " }", 451 | assetComment, 452 | assetId, 453 | assetId); 454 | } else { 455 | g_Asset_method_code = String.format(" /// %s\n" + 456 | " // ignore: non_constant_identifier_names\n" + 457 | " AssetSvg %s({@required double width, @required double height}) {\n" + 458 | " final imageProvider = AssetSvg(asset.%s.keyName, width: width, height: height);\n" + 459 | " return imageProvider;\n" + 460 | " }", 461 | assetComment, 462 | assetId, 463 | assetId); 464 | } 465 | 466 | all_g_Asset_method_code += g_Asset_method_code; 467 | } 468 | 469 | String code = String.format("/// This `_R_Svg` class is generated and contains references to static svg type image asset resources.\n" + 470 | "// ignore: camel_case_types\n" + 471 | "class _R_Svg {\n" + 472 | " const _R_Svg();\n" + 473 | "\n" + 474 | " final asset = const _R_Svg_AssetResource();\n" + 475 | "%s\n" + 476 | "}", 477 | all_g_Asset_method_code); 478 | 479 | return code; 480 | } 481 | 482 | /* 483 | * 根据模板,为 textAssetArray(文本资产数组)生成 _R_Text class 的代码 484 | * */ 485 | public static String generate__R_Text_class(@NotNull List textAssetArray, @NotNull Map textAssetIdDict, @NotNull String packageName) { 486 | String all_g_Asset_method_code = ""; 487 | 488 | for (String asset : textAssetArray) { 489 | all_g_Asset_method_code += "\n"; 490 | 491 | String assetId = textAssetIdDict.get(asset); 492 | String assetComment = generateAssetComment(asset, packageName); 493 | 494 | String g_Asset_method_code = String.format(" /// %s\n" + 495 | " // ignore: non_constant_identifier_names\n" + 496 | " Future %s() {\n" + 497 | " final str = rootBundle.loadString(asset.%s.keyName);\n" + 498 | " return str;\n" + 499 | " }", 500 | assetComment, 501 | assetId, 502 | assetId); 503 | 504 | all_g_Asset_method_code += g_Asset_method_code; 505 | } 506 | 507 | String code = String.format("/// This `_R_Text` class is generated and contains references to static text asset resources.\n" + 508 | "// ignore: camel_case_types\n" + 509 | "class _R_Text {\n" + 510 | " const _R_Text();\n" + 511 | "\n" + 512 | " final asset = const _R_Text_AssetResource();\n" + 513 | "%s\n" + 514 | "}", 515 | all_g_Asset_method_code); 516 | 517 | return code; 518 | } 519 | 520 | /* 521 | * 为当前 font_family_name 生成 font_family_id;font_family_id 一般为 asset 的 font_family_name; 522 | * 但是为了保证 font_family_id 的健壮性,需要对 font_family_name 做以下加工处理: 523 | * - 处理非法字符:把除了字母(a-z, A-Z)、数字(0-9)、'_' 字符、'$' 字符之外的字符转换为 '_' 字符 524 | * - 首字母转化为小写 525 | * - 处理首字符异常情况:检测首字符是不是数字、'_'、'$',若是则添加前缀字符"a" 526 | * 527 | * === Examples 528 | * a_font_family_name = "Amiri" 529 | * b_font_family_name = "Baloo-Thambi-2" 530 | * a_font_family_id = "amiri" 531 | * b_font_family_id = "baloo_Thambi_2" 532 | * */ 533 | public static String generateFontFamilyId(@NotNull String fontFamilyName) { 534 | String fontFamilyId = fontFamilyName; 535 | 536 | // 过滤非法字符 537 | fontFamilyId = fontFamilyId.replaceAll("[^a-zA-Z0-9_$]", "_"); 538 | 539 | // 检测首字符是不是字母; 540 | // 若是字母,则检测其是不是大写字母,若是,则转换为小写字母; 541 | // 若不是字母,则添加一个前缀字母“a” 542 | Character firstChar = fontFamilyId.charAt(0); 543 | if(Character.isLetter(firstChar)) { 544 | if(Character.isUpperCase(firstChar)) { 545 | String firstCharStr = firstChar.toString().toLowerCase(); 546 | fontFamilyId = firstCharStr + fontFamilyId.substring(1); 547 | } 548 | } else { 549 | String firstCharStr = "a"; 550 | fontFamilyId = firstCharStr + fontFamilyId; 551 | } 552 | 553 | return fontFamilyId; 554 | } 555 | 556 | /* 557 | * 根据模板,为 fontFamilyConfigArray(字体家族配置数组)生成 _R_FontFamily class 的代码 558 | * */ 559 | public static String generate__R_FontFamily_class(@NotNull List fontFamilyConfigArray, @NotNull String packageName) { 560 | String all_g_AssetResource_property_code = ""; 561 | 562 | for (Map fontFamilyConfig : fontFamilyConfigArray) { 563 | all_g_AssetResource_property_code += "\n"; 564 | 565 | String fontFamilyName = (String)fontFamilyConfig.get("family"); 566 | String fontFamilyId = generateFontFamilyId(fontFamilyName); 567 | 568 | String fontFamilyComment = String.format("font family: %s", fontFamilyName); 569 | 570 | String g_AssetResource_property_code = String.format(" /// %s\n" + 571 | " // ignore: non_constant_identifier_names\n" + 572 | " final %s = \"%s\";", 573 | fontFamilyComment, 574 | fontFamilyId, 575 | fontFamilyName); 576 | 577 | all_g_AssetResource_property_code += g_AssetResource_property_code; 578 | } 579 | 580 | String code = String.format("/// This `_R_FontFamily` class is generated and contains references to static font asset resources.\n" + 581 | "// ignore: camel_case_types\n" + 582 | "class _R_FontFamily {\n" + 583 | " const _R_FontFamily();\n" + 584 | "%s\n" + 585 | "}", all_g_AssetResource_property_code); 586 | return code; 587 | } 588 | } 589 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Flr_Plugin_Icon 5 | Created with Sketch. 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 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | xi 101 | wang 102 | baba 103 | zao 104 | ri 105 | kang 106 | fu 107 | 108 | 109 | 110 | --------------------------------------------------------------------------------