├── .gitignore ├── CHANGELOG.md ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle └── src └── main ├── java └── com │ └── crzsc │ └── plugin │ ├── actions │ ├── GenerateAction.kt │ └── GenerateDirAction.kt │ ├── listener │ ├── MyProjectManagerListener.kt │ └── PsiTreeListener.kt │ ├── provider │ └── AssetsLineMarkerProvider.kt │ ├── setting │ ├── AppSettingsComponent.kt │ ├── AppSettingsConfigurable.kt │ └── PluginSetting.kt │ └── utils │ ├── Constants.kt │ ├── FileGenerator.kt │ ├── FileHelperNew.kt │ ├── Messages.kt │ ├── PluginUtils.kt │ └── StringExt.kt └── resources ├── META-INF ├── plugin.xml └── pluginIcon.svg └── messages ├── MessagesBun_zh.properties └── MessagesBundle.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | .idea/ 3 | build/ 4 | .gradle/ 5 | /local.properties 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | [//]: # (# FlutterAssetsGenerator Changelog) 4 | ## [2.5.0] 5 | ### Fixed 6 | - Bug fix. 7 | ### Added 8 | - add enable leading with package name option 9 | ## [2.4.2] 10 | ### Added 11 | - `Flutter: Configuring Paths` operation deletes paths that don't exist. 12 | ## [2.4.1] 13 | ### Fixed 14 | - Bug fix. 15 | ## [2.4.0] 16 | ### Added 17 | - Global configuration. 18 | - Register assets to pubspec with one click. 19 | ## [2.3.0] 20 | ### Added 21 | - Configuring ignore paths. 22 | ## [2.2.0] 23 | ### Fixed 24 | - Svg preview not displaying. 25 | ### Added 26 | - Multi module flutter project support. 27 | ## [2.1.0] 28 | ### Fixed 29 | - Bug fix. 30 | ### Added 31 | - Filename split supports config. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | It's a plugin that generates an asset index which we can easily find.It can be used on Android Studio or Idea. 2 | 3 | ## How to use 4 | 5 | ### 1.Configuring paths in pubspec.yaml 6 | 7 | Plugin now supports automatic configuration: 8 | - Right-click on folder or file, then click 9 |
`Flutter: Configuring Paths`. 10 |
![](https://tva1.sinaimg.cn/large/008vxvgGly1h9h9nxz6ttg30ed0ootl5.gif) 11 | ### 2.Generate file 12 | 13 | You can generate file by these ways: 14 | 15 | - `Build` => `Generate Flutter Assets` 16 |
![](https://tva1.sinaimg.cn/large/008vxvgGly1h9h9rnd51mg30hv0orjz6.gif) 17 | - Press `Option`(mac)/`Alt`(win) + `G`,It will generate assets.dart on lib/generated. 18 | 19 | Simply use it like: 20 | 21 | ```dart 22 | Image.asset( 23 | Assets.imageLoading, 24 | width: 24, 25 | height: 24, 26 | fit: BoxFit.contain, 27 | ) 28 | ``` 29 | 30 | ### 3.Extras 31 | 32 | - You can locate file quickly by click line-marker. 33 |
![](https://tva1.sinaimg.cn/large/008vxvgGly1h9h9vyjccyg30hv0ordpz.gif) 34 | - Plugin will observe your changes on assets path and update file. 35 |
36 | 37 | ## Settings 38 | ### Global 39 | `Preferences` => `Tools` => `FlutterAssetsGenerator` 40 | ### Module based 41 | You can change default settings by add following contents in your `pubspec.yaml`. 42 | 43 | ```yaml 44 | flutter_assets_generator: 45 | # Optional. Sets the directory of generated localization files. Provided value should be a valid path on lib dir. Default: generated 46 | output_dir: generated 47 | # Optional. Sets whether utomatic monitoring of file changes. Default: true 48 | auto_detection: true 49 | # Optional. Sets file name conversion rules. Default: true 50 | named_with_parent: true 51 | # Optional. Sets the name for the generated localization file. Default: assets 52 | output_filename: assets 53 | # Optional. Sets the name for the generated localization class. Default: Assets 54 | class_name: Assets 55 | # Optional. Sets the filename split pattern for filename split. Default: [-_] 56 | filename_split_pattern: "[-_]" 57 | # Optional. Configuring ignore paths. Default: [],e.g: ["assets/fonts", "assets/images/dark", ...] 58 | path_ignore: [] 59 | ``` 60 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.ExtensionsKt 2 | 3 | plugins { 4 | id 'java' 5 | id "org.jetbrains.intellij" version "1.1.6" 6 | id 'org.jetbrains.kotlin.jvm' version '1.7.20' 7 | // Gradle Changelog Plugin 8 | id 'org.jetbrains.changelog' version "1.3.1" 9 | } 10 | 11 | apply plugin: 'org.jetbrains.changelog' 12 | 13 | String properties(String key) { 14 | return project.findProperty(key).toString() 15 | } 16 | 17 | 18 | group = properties("pluginGroup") 19 | version = properties("pluginVersion") 20 | 21 | compileKotlin { 22 | kotlinOptions.jvmTarget = "1.8" 23 | } 24 | 25 | compileTestKotlin { 26 | kotlinOptions.jvmTarget = "1.8" 27 | } 28 | 29 | repositories { 30 | mavenCentral() 31 | } 32 | 33 | dependencies { 34 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" 35 | } 36 | 37 | // See https://github.com/JetBrains/gradle-intellij-plugin/ 38 | intellij { 39 | pluginName = properties("pluginName") 40 | 41 | version = properties("platformVersion") 42 | type = properties("platformType") 43 | downloadSources = properties("platformDownloadSources").toBoolean() 44 | updateSinceUntilBuild = true 45 | 46 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. 47 | 48 | plugins = properties("platformPlugins").split(',').toList() 49 | } 50 | 51 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 52 | changelog { 53 | version = properties("pluginVersion") 54 | path = "${project.projectDir}/CHANGELOG.md" 55 | // header = "[${-> version.get()}] - ${new SimpleDateFormat("yyyy-MM-dd").format(new Date())}" 56 | // headerParserRegex = ~/(\d+\.\d+)/ 57 | // itemPrefix = "-" 58 | keepUnreleasedSection = true 59 | unreleasedTerm = "[Unreleased]" 60 | groups = ["Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"] 61 | } 62 | 63 | runIde { 64 | // Absolute path to installed target 3.5 Android Studio to use as 65 | // IDE Development Instance (the "Contents" directory is macOS specific): 66 | // ideDir.set(file("/Applications/Android Studio.app/Contents")) 67 | } 68 | 69 | patchPluginXml { 70 | version = properties("pluginVersion") 71 | sinceBuild = properties("pluginSinceBuild") 72 | untilBuild = properties("pluginUntilBuild") 73 | pluginDescription = ExtensionsKt.markdownToHTML(new File(rootDir, "README.md").text) 74 | // Get the latest available change notes from the changelog file 75 | changeNotes = ExtensionsKt.markdownToHTML(new File(rootDir, "CHANGELOG.md").text) 76 | } 77 | 78 | runPluginVerifier { 79 | ideVersions = properties("pluginVerifierIdeVersions").split(',').toList() 80 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | org.gradle.caching=true 4 | pluginGroup = com.crzsc 5 | pluginName = FlutterAssetsGenerator 6 | pluginVersion = 2.5.0 7 | 8 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 9 | # for insight into build numbers and IntelliJ Platform versions. 10 | pluginSinceBuild = 203 11 | pluginUntilBuild = 12 | 13 | # Plugin Verifier integration -> https://github.com/JetBrains/gradle-intellij-plugin#plugin-verifier-dsl 14 | # See https://jb.gg/intellij-platform-builds-list for available build versions. 15 | pluginVerifierIdeVersions = 2020.3.4,2021.1.3,2021.2.1,2021.3.1 16 | 17 | platformType = IC 18 | platformVersion = 2021.3.2 19 | platformDownloadSources = true 20 | 21 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 22 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 23 | #fit 2021.3.2 24 | platformPlugins = yaml,java,Dart:213.7433,io.flutter:70.2.3,Kotlin 25 | #platformPlugins = yaml, java, Dart:212.5080.8, io.flutter:61.2.4, Kotlin 26 | 27 | # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 28 | javaVersion = 11 29 | 30 | gradleVersion = 7.2 31 | 32 | #changeNotes = 1.Fix bugs. \n2.Filename split supports config. 33 | 34 | # Opt-out flag for bundling Kotlin standard library. 35 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 36 | # suppress inspection "UnusedProperty" 37 | kotlin.stdlib.default.dependency = false 38 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cr1992/FlutterAssetsGenerator/63466940e10fab11478fcab4987d5a27218fcbfa/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto init 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto init 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :init 68 | @rem Get command-line arguments, handling Windows variants 69 | 70 | if not "%OS%" == "Windows_NT" goto win9xME_args 71 | 72 | :win9xME_args 73 | @rem Slurp the command line arguments. 74 | set CMD_LINE_ARGS= 75 | set _SKIP=2 76 | 77 | :win9xME_args_slurp 78 | if "x%~1" == "x" goto execute 79 | 80 | set CMD_LINE_ARGS=%* 81 | 82 | :execute 83 | @rem Setup the command line 84 | 85 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 86 | 87 | 88 | @rem Execute Gradle 89 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 90 | 91 | :end 92 | @rem End local scope for the variables with windows NT shell 93 | if "%ERRORLEVEL%"=="0" goto mainEnd 94 | 95 | :fail 96 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 97 | rem the _cmd.exe /c_ return code! 98 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 99 | exit /b 1 100 | 101 | :mainEnd 102 | if "%OS%"=="Windows_NT" endlocal 103 | 104 | :omega 105 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'Flutter_assets_generator' 2 | 3 | -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/actions/GenerateAction.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.actions 2 | 3 | import com.crzsc.plugin.utils.FileGenerator 4 | import com.crzsc.plugin.utils.FileHelperNew.shouldActivateFor 5 | import com.crzsc.plugin.utils.PluginUtils.showNotify 6 | import com.crzsc.plugin.utils.message 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.PlatformDataKeys 10 | 11 | class GenerateAction : AnAction() { 12 | 13 | override fun actionPerformed(e: AnActionEvent) { 14 | val project = e.getData(PlatformDataKeys.PROJECT) 15 | if (shouldActivateFor(project!!)) { 16 | FileGenerator(project).generateAll() 17 | } else { 18 | showNotify(message("notFlutterProject")) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/actions/GenerateDirAction.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.actions 2 | 3 | import com.crzsc.plugin.utils.FileGenerator 4 | import com.crzsc.plugin.utils.FileHelperNew 5 | import com.crzsc.plugin.utils.PluginUtils.showNotify 6 | import com.crzsc.plugin.utils.message 7 | import com.intellij.openapi.actionSystem.AnAction 8 | import com.intellij.openapi.actionSystem.AnActionEvent 9 | import com.intellij.openapi.actionSystem.PlatformDataKeys 10 | 11 | /** 12 | * 同步文件 文件夹到yaml 13 | */ 14 | class GenerateDirAction : AnAction() { 15 | 16 | override fun actionPerformed(e: AnActionEvent) { 17 | val project = e.getData(PlatformDataKeys.PROJECT) 18 | if (FileHelperNew.shouldActivateFor(project!!)) { 19 | val file = e.getData(PlatformDataKeys.VIRTUAL_FILE) 20 | if (file != null) { 21 | FileGenerator(project).buildYaml(file) 22 | } 23 | } else { 24 | showNotify(message("notFlutterProject")) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/listener/MyProjectManagerListener.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.listener 2 | 3 | import com.intellij.openapi.project.Project 4 | import com.intellij.openapi.project.ProjectManagerListener 5 | import com.intellij.psi.PsiManager 6 | 7 | class MyProjectManagerListener : ProjectManagerListener { 8 | private val eventsMap = mutableMapOf() 9 | override fun projectClosed(project: Project) { 10 | super.projectClosed(project) 11 | } 12 | 13 | override fun projectOpened(project: Project) { 14 | super.projectOpened(project) 15 | val treeListener = PsiTreeListener(project) 16 | eventsMap[project] = treeListener 17 | PsiManager.getInstance(project).addPsiTreeChangeListener( 18 | treeListener) 19 | } 20 | 21 | override fun projectClosing(project: Project) { 22 | super.projectClosing(project) 23 | eventsMap.remove(project)?.let { 24 | PsiManager.getInstance(project).removePsiTreeChangeListener(it) 25 | } 26 | } 27 | 28 | override fun projectClosingBeforeSave(project: Project) { 29 | super.projectClosingBeforeSave(project) 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/listener/PsiTreeListener.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.listener 2 | 3 | import com.crzsc.plugin.utils.FileGenerator 4 | import com.crzsc.plugin.utils.FileHelperNew 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.psi.PsiDirectory 7 | import com.intellij.psi.PsiTreeChangeEvent 8 | import com.intellij.psi.PsiTreeChangeListener 9 | import com.intellij.util.castSafelyTo 10 | import java.util.* 11 | import kotlin.concurrent.timerTask 12 | 13 | class PsiTreeListener(private val project: Project) : PsiTreeChangeListener { 14 | private val fileGenerator = FileGenerator(project) 15 | 16 | override fun beforePropertyChange(event: PsiTreeChangeEvent) { 17 | } 18 | 19 | override fun childReplaced(event: PsiTreeChangeEvent) { 20 | handleEvent(event) 21 | } 22 | 23 | override fun childrenChanged(event: PsiTreeChangeEvent) { 24 | handleEvent(event) 25 | } 26 | 27 | override fun beforeChildAddition(event: PsiTreeChangeEvent) { 28 | } 29 | 30 | override fun beforeChildReplacement(event: PsiTreeChangeEvent) { 31 | } 32 | 33 | override fun propertyChanged(event: PsiTreeChangeEvent) { 34 | handleEvent(event) 35 | } 36 | 37 | override fun beforeChildrenChange(event: PsiTreeChangeEvent) { 38 | } 39 | 40 | override fun childMoved(event: PsiTreeChangeEvent) { 41 | handleEvent(event) 42 | } 43 | 44 | override fun childRemoved(event: PsiTreeChangeEvent) { 45 | handleEvent(event) 46 | } 47 | 48 | override fun beforeChildMovement(event: PsiTreeChangeEvent) { 49 | } 50 | 51 | override fun childAdded(event: PsiTreeChangeEvent) { 52 | handleEvent(event) 53 | } 54 | 55 | override fun beforeChildRemoval(event: PsiTreeChangeEvent) { 56 | } 57 | 58 | private fun handleEvent(event: PsiTreeChangeEvent) { 59 | val assets = FileHelperNew.getAssets(project) 60 | for (config in assets) { 61 | if (FileHelperNew.isAutoDetectionEnable(config)) { 62 | // 该Module开启了自动检测 63 | event.child?.let { changedFile -> 64 | changedFile.parent.castSafelyTo()?.let { dir -> 65 | //assets目录发生改变 这里延迟生成避免报错 66 | for (file in config.assetVFiles) { 67 | if (dir.virtualFile.path.startsWith(file.path)) { 68 | Timer().schedule(timerTask { 69 | fileGenerator.generateOne(config) 70 | }, 300) 71 | break 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/provider/AssetsLineMarkerProvider.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.provider 2 | 3 | import com.crzsc.plugin.utils.FileHelperNew 4 | import com.crzsc.plugin.utils.PluginUtils.openFile 5 | import com.crzsc.plugin.utils.isSvgExtension 6 | import com.intellij.codeInsight.daemon.LineMarkerInfo 7 | import com.intellij.codeInsight.daemon.LineMarkerProvider 8 | import com.intellij.openapi.editor.markup.GutterIconRenderer 9 | import com.intellij.openapi.util.Iconable 10 | import com.intellij.openapi.vfs.LocalFileSystem 11 | import com.intellij.openapi.vfs.VirtualFile 12 | import com.intellij.psi.PsiElement 13 | import com.intellij.psi.impl.source.tree.LeafPsiElement 14 | import com.intellij.psi.util.PsiTreeUtil 15 | import com.intellij.ui.scale.ScaleContext 16 | import com.intellij.util.IconUtil 17 | import com.intellij.util.SVGLoader 18 | import io.flutter.utils.FlutterModuleUtils 19 | import org.jetbrains.kotlin.idea.util.module 20 | import javax.swing.Icon 21 | import javax.swing.ImageIcon 22 | 23 | 24 | /** 25 | * Svgs.dart显示图标在路径左侧 26 | */ 27 | class AssetsLineMarkerProvider : LineMarkerProvider { 28 | 29 | override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? { 30 | val module = element.module 31 | if (!FlutterModuleUtils.isFlutterModule(module)) return null 32 | val elementType = (element as? LeafPsiElement?)?.elementType?.toString() 33 | if (elementType == "REGULAR_STRING_PART" 34 | ) { 35 | // 这里会被多次调用 尽量减少调用次数 36 | var assetName: String? = null 37 | var leadingWithPackageName: String? = null; 38 | if (module != null) { 39 | FileHelperNew.getPubSpecConfig(module)?.let { 40 | assetName = FileHelperNew.getGeneratedFileName(it) 41 | leadingWithPackageName = it.getLeadingWithPackageNameIfChecked() 42 | } 43 | } 44 | val filenameCorrect = element.containingFile.name.equals( 45 | assetName, true 46 | ) || element.containingFile.name.equals( 47 | "assets.dart", true 48 | ) 49 | if (filenameCorrect) { 50 | // println("filenameCorrect showMakeByType : $element") 51 | return showMakeByType(element, leadingWithPackageName) 52 | } 53 | } 54 | return null 55 | } 56 | 57 | private fun showMakeByType(element: PsiElement, leadingWithPackageName: String?): LineMarkerInfo<*>? { 58 | val assetsPath = element.text 59 | val anchor = PsiTreeUtil.getDeepestFirst(element) 60 | // 先用默认的path找文件 61 | var filePath = element.project.basePath + "/" + element.text 62 | if (!leadingWithPackageName.isNullOrEmpty()) { 63 | filePath = filePath.replace(leadingWithPackageName, "") 64 | } 65 | var vFile = LocalFileSystem.getInstance().findFileByPath(filePath) 66 | if (vFile == null) { 67 | // 如果没找到,尝试根据当前文件向上查找 68 | var file = element.containingFile.viewProvider.virtualFile 69 | while (file.name != "lib" && file.parent != null) { 70 | file = file.parent 71 | } 72 | filePath = file.parent.path + "/" + element.text 73 | if (!leadingWithPackageName.isNullOrEmpty()) { 74 | filePath = filePath.replace(leadingWithPackageName, "") 75 | } 76 | vFile = LocalFileSystem.getInstance().findFileByPath(filePath) 77 | } 78 | // println("showMakeByType assetsPath $assetsPath") 79 | if (vFile != null) { 80 | return when { 81 | assetsPath.isSvgExtension -> showSvgMark(element, anchor, vFile) 82 | else -> showIconMark(element, anchor, vFile) 83 | } 84 | } 85 | return null 86 | } 87 | 88 | 89 | private fun showSvgMark( 90 | element: PsiElement, anchor: PsiElement, 91 | vFile: VirtualFile 92 | ): LineMarkerInfo<*> { 93 | val icon: Icon = 94 | ImageIcon( 95 | SVGLoader.load( 96 | null, 97 | vFile.inputStream, 98 | ScaleContext.createIdentity(), 99 | 16.0, 100 | 16.0 101 | ) 102 | ) 103 | return LineMarkerInfo(anchor, anchor.textRange, icon, { 104 | //悬停,会多次调用 105 | return@LineMarkerInfo "" 106 | }, { _, _ -> element.openFile(vFile) }, GutterIconRenderer.Alignment.LEFT) 107 | 108 | } 109 | 110 | 111 | private fun showIconMark( 112 | element: PsiElement, anchor: PsiElement, 113 | vFile: VirtualFile 114 | ): LineMarkerInfo<*> { 115 | val icon = IconUtil.getIcon(vFile, Iconable.ICON_FLAG_VISIBILITY, element.project) 116 | //其他文件展示文件格式 117 | return LineMarkerInfo( 118 | anchor, anchor.textRange, 119 | icon, { 120 | //悬停,会多次调用 121 | return@LineMarkerInfo "" 122 | }, { _, _ -> element.openFile(vFile) }, GutterIconRenderer.Alignment.LEFT 123 | ) 124 | 125 | } 126 | 127 | } -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/setting/AppSettingsComponent.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.setting 2 | 3 | import com.crzsc.plugin.utils.message 4 | import com.intellij.ui.components.JBCheckBox 5 | import com.intellij.ui.components.JBLabel 6 | import com.intellij.ui.components.JBTextField 7 | import com.intellij.util.ui.FormBuilder 8 | import javax.swing.JComponent 9 | import javax.swing.JPanel 10 | 11 | class AppSettingsComponent { 12 | val panel: JPanel 13 | private val filePath = JBTextField() 14 | private val fileName = JBTextField() 15 | private val className = JBTextField() 16 | private val filenameSplitPattern = JBTextField() 17 | private val autoDetection = JBCheckBox(message("settingsAutoDetection")) 18 | private val namedWithParent = JBCheckBox(message("settingsNamed")) 19 | private val leadingWithPackageName = JBCheckBox(message("settingsLeadingWithPackageName")) 20 | 21 | init { 22 | panel = FormBuilder.createFormBuilder() 23 | .addLabeledComponent(JBLabel(message("settingsFilePath")), filePath, 1, false) 24 | .addLabeledComponent(JBLabel(message("settingsFileName")), fileName, 1, false) 25 | .addLabeledComponent(JBLabel(message("settingsClassName")), className, 1, false) 26 | .addLabeledComponent(JBLabel(message("settingsSplitPattern")), filenameSplitPattern, 1, false) 27 | .addComponent(autoDetection, 1) 28 | .addComponent(namedWithParent, 1) 29 | .addComponent(leadingWithPackageName, 1) 30 | .addComponentFillVertically(JPanel(), 0) 31 | .panel 32 | } 33 | 34 | val preferredFocusedComponent: JComponent 35 | get() = filePath 36 | 37 | fun getFilePath(): String { 38 | return filePath.text 39 | } 40 | 41 | fun setFilePath(text: String?) { 42 | filePath.text = text 43 | } 44 | 45 | fun getFileName(): String { 46 | return fileName.text 47 | } 48 | 49 | 50 | fun setFileName(text: String?) { 51 | fileName.text = text 52 | } 53 | 54 | fun getClassName(): String { 55 | return className.text 56 | } 57 | 58 | fun setClassName(text: String?) { 59 | className.text = text 60 | } 61 | 62 | fun getFilenameSplitPattern(): String { 63 | return filenameSplitPattern.text 64 | } 65 | 66 | fun setFilenameSplitPattern(text: String?) { 67 | filenameSplitPattern.text = text 68 | } 69 | 70 | fun getAutoDetection(): Boolean { 71 | return autoDetection.isSelected 72 | } 73 | 74 | fun setAutoDetection(newStatus: Boolean) { 75 | autoDetection.isSelected = newStatus 76 | } 77 | 78 | fun getNamedWithParent(): Boolean { 79 | return namedWithParent.isSelected 80 | } 81 | 82 | fun setNamedWithParent(newStatus: Boolean) { 83 | namedWithParent.isSelected = newStatus 84 | } 85 | 86 | fun getLeadingWithPackageName(): Boolean { 87 | return leadingWithPackageName.isSelected 88 | } 89 | 90 | fun setLeadingWithPackageName(newStatus: Boolean) { 91 | leadingWithPackageName.isSelected = newStatus 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/setting/AppSettingsConfigurable.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.setting 2 | 3 | import com.intellij.openapi.options.Configurable 4 | import javax.swing.JComponent 5 | 6 | class AppSettingsConfigurable : Configurable { 7 | private var mySettingsComponent: AppSettingsComponent? = null 8 | 9 | // A default constructor with no arguments is required because this implementation 10 | // is registered as an applicationConfigurable EP 11 | override fun getDisplayName(): String { 12 | return "FlutterAssetsGenerator" 13 | } 14 | 15 | override fun getPreferredFocusedComponent(): JComponent { 16 | return mySettingsComponent!!.preferredFocusedComponent 17 | } 18 | 19 | override fun createComponent(): JComponent { 20 | mySettingsComponent = AppSettingsComponent() 21 | return mySettingsComponent!!.panel 22 | } 23 | 24 | override fun isModified(): Boolean { 25 | val settings = PluginSetting.instance 26 | var modified = mySettingsComponent!!.getAutoDetection() != settings.autoDetection 27 | modified = modified or (mySettingsComponent!!.getFileName() != settings.fileName) 28 | modified = modified or (mySettingsComponent!!.getClassName() != settings.className) 29 | modified = modified or (mySettingsComponent!!.getFilenameSplitPattern() != settings.filenameSplitPattern) 30 | modified = modified or (mySettingsComponent!!.getFilePath() != settings.filePath) 31 | modified = modified or (mySettingsComponent!!.getNamedWithParent() != settings.namedWithParent) 32 | modified = modified or (mySettingsComponent!!.getLeadingWithPackageName() != settings.leadingWithPackageName) 33 | return modified 34 | } 35 | 36 | override fun apply() { 37 | val settings = PluginSetting.instance 38 | settings.autoDetection = mySettingsComponent!!.getAutoDetection() 39 | settings.fileName = mySettingsComponent!!.getFileName() 40 | settings.className = mySettingsComponent!!.getClassName() 41 | settings.filePath = mySettingsComponent!!.getFilePath() 42 | settings.filenameSplitPattern = mySettingsComponent!!.getFilenameSplitPattern() 43 | settings.namedWithParent = mySettingsComponent!!.getNamedWithParent() 44 | settings.leadingWithPackageName = mySettingsComponent!!.getLeadingWithPackageName() 45 | } 46 | 47 | override fun reset() { 48 | val settings = PluginSetting.instance 49 | mySettingsComponent!!.setAutoDetection(settings.autoDetection) 50 | mySettingsComponent!!.setFileName(settings.fileName) 51 | mySettingsComponent!!.setClassName(settings.className) 52 | mySettingsComponent!!.setFilePath(settings.filePath) 53 | mySettingsComponent!!.setFilenameSplitPattern(settings.filenameSplitPattern) 54 | mySettingsComponent!!.setNamedWithParent(settings.namedWithParent) 55 | mySettingsComponent!!.setLeadingWithPackageName(settings.leadingWithPackageName) 56 | } 57 | 58 | override fun disposeUIResources() { 59 | mySettingsComponent = null 60 | } 61 | } -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/setting/PluginSetting.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.setting 2 | 3 | import com.crzsc.plugin.utils.Constants 4 | import com.intellij.openapi.application.ApplicationManager 5 | import com.intellij.openapi.components.* 6 | import com.intellij.util.xmlb.XmlSerializerUtil 7 | 8 | /** 9 | * Supports storing the application settings in a persistent way. 10 | * The [State] and [Storage] annotations define the name of the data and the file name where 11 | * these persistent application settings are stored. 12 | */ 13 | @State(name = "com.crzsc.plugin.setting.PluginSetting", storages = [Storage("FlutterAssetsGenerator.xml")]) 14 | class PluginSetting : PersistentStateComponent { 15 | var className: String? = Constants.DEFAULT_CLASS_NAME 16 | var fileName: String? = Constants.DEFAULT_CLASS_NAME.lowercase() 17 | var filePath: String? = Constants.DEFAULT_OUTPUT_DIR 18 | var filenameSplitPattern: String? = Constants.DEFAULT_FILENAME_SPLIT_PATTERN 19 | var namedWithParent = true 20 | var autoDetection = true 21 | var leadingWithPackageName = true; 22 | override fun getState(): PluginSetting { 23 | return this 24 | } 25 | 26 | override fun loadState(state: PluginSetting) { 27 | XmlSerializerUtil.copyBean(state, this) 28 | } 29 | 30 | companion object { 31 | val instance: PluginSetting 32 | get() = ApplicationManager.getApplication().getService(PluginSetting::class.java) 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/utils/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.utils 2 | 3 | object Constants { 4 | /** 5 | * 配置map的key 6 | */ 7 | const val KEY_CONFIGURATION_MAP = "flutter_assets_generator" 8 | /** 9 | * 输出目录的key 10 | */ 11 | const val KEY_OUTPUT_DIR = "output_dir" 12 | 13 | /** 14 | * 输出文件的类名 15 | */ 16 | const val KEY_CLASS_NAME = "class_name" 17 | 18 | /** 19 | * 是否自动检测 20 | */ 21 | const val KEY_AUTO_DETECTION = "auto_detection" 22 | 23 | /** 24 | * 命名是否根据上级目录决定 25 | */ 26 | const val KEY_NAMED_WITH_PARENT = "named_with_parent" 27 | 28 | const val KEY_LEADING_WITH_PACKAGE_NAME = "leading_with_package_name" 29 | 30 | /** 31 | * 输出的文件名 32 | */ 33 | const val KEY_OUTPUT_FILENAME = "output_filename" 34 | 35 | /** 36 | * 分割文件的正则 37 | */ 38 | const val FILENAME_SPLIT_PATTERN = "filename_split_pattern" 39 | 40 | /** 41 | * 忽略的目录 42 | */ 43 | const val PATH_IGNORE = "path_ignore" 44 | 45 | /** 46 | * 默认目录 47 | */ 48 | const val DEFAULT_OUTPUT_DIR = "generated" 49 | const val DEFAULT_CLASS_NAME = "Assets" 50 | 51 | const val DEFAULT_FILENAME_SPLIT_PATTERN = "[-_]" 52 | } -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/utils/FileGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.utils 2 | 3 | import com.crzsc.plugin.utils.PluginUtils.showNotify 4 | import com.crzsc.plugin.utils.PluginUtils.toLowCamelCase 5 | import com.crzsc.plugin.utils.PluginUtils.upperCaseFirst 6 | import com.intellij.openapi.application.ApplicationManager 7 | import com.intellij.openapi.command.WriteCommandAction 8 | import com.intellij.openapi.project.Project 9 | import com.intellij.openapi.project.guessModuleDir 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.psi.PsiDocumentManager 12 | import com.intellij.psi.PsiManager 13 | import com.intellij.psi.util.PsiTreeUtil 14 | import io.flutter.utils.FlutterModuleUtils 15 | import org.jetbrains.kotlin.idea.core.util.toPsiFile 16 | import org.jetbrains.kotlin.idea.util.findModule 17 | import org.jetbrains.yaml.YAMLElementGenerator 18 | import org.jetbrains.yaml.psi.YAMLFile 19 | import org.jetbrains.yaml.psi.YAMLMapping 20 | import org.jetbrains.yaml.psi.YAMLSequence 21 | import java.io.File 22 | 23 | class FileGenerator(private val project: Project) { 24 | private val ignoreDir = listOf("2.0x", "3.0x", "Mx", "Nx") 25 | 26 | /** 27 | * 为所有模块重新生成 28 | */ 29 | fun generateAll() { 30 | WriteCommandAction.runWriteCommandAction(project) { 31 | val assets = FileHelperNew.getAssets(project) as MutableList 32 | assets.removeAll { 33 | println("module : ${it.module} assets ${it.assetVFiles}") 34 | it.assetVFiles.isEmpty() 35 | } 36 | if (assets.isEmpty()) { 37 | showNotify("Please configure your assets path in pubspec.yaml") 38 | return@runWriteCommandAction 39 | } 40 | for (config in assets) { 41 | generateWithConfig(config) 42 | } 43 | } 44 | } 45 | 46 | 47 | /** 48 | * 生成单个模块文件 49 | */ 50 | fun generateOne(config: ModulePubSpecConfig) { 51 | WriteCommandAction.runWriteCommandAction(project) { 52 | generateWithConfig(config) 53 | } 54 | } 55 | 56 | private fun generateWithConfig(config: ModulePubSpecConfig) { 57 | println(config) 58 | val module = config.module 59 | val map = mutableMapOf() 60 | val ignorePath = FileHelperNew.getPathIgnore(config) 61 | // 这里可能有多个目录或者文件 62 | for (file in config.assetVFiles) { 63 | println("generate file map in $file") 64 | generateFileMap( 65 | file, 66 | config, 67 | map, 68 | ignorePath, 69 | ) 70 | } 71 | if (map.isEmpty()) { 72 | // showNotify("assets path is empty") 73 | println("${config.module} assets map is empty, skip") 74 | return 75 | } 76 | val content = StringBuilder() 77 | content.append("///This file is automatically generated. DO NOT EDIT, all your changes would be lost.\n") 78 | val className = FileHelperNew.getGeneratedClassName(config) 79 | content.append("class $className {\n $className._();\n\n") 80 | map.toSortedMap().forEach { 81 | content.append(" static const String ${it.key} = '${config.getLeadingWithPackageNameIfChecked()}${it.value}';\n") 82 | } 83 | content.append("\n}\n") 84 | val psiManager = PsiManager.getInstance(project) 85 | val psiDocumentManager = PsiDocumentManager.getInstance(project) 86 | FileHelperNew.getGeneratedFile(config).let { generated -> 87 | psiManager.findFile(generated)?.let { dartFile -> 88 | psiDocumentManager.getDocument(dartFile)?.let { document -> 89 | if (document.text != content.toString()) { 90 | document.setText(content) 91 | psiDocumentManager.commitDocument(document) 92 | showNotify("$module : assets generate succeed") 93 | } else { 94 | showNotify("$module : nothing changed") 95 | } 96 | } 97 | } 98 | } 99 | } 100 | 101 | private fun generateFileMap( 102 | root: VirtualFile, 103 | config: ModulePubSpecConfig, 104 | map: MutableMap, 105 | ignorePath: List, 106 | ) { 107 | val namedWithParent = FileHelperNew.isNamedWithParent(config) 108 | val pattern = FileHelperNew.getFilenameSplitPattern(config) 109 | val basePath = config.module.guessModuleDir()?.path 110 | val regex = Regex(pattern) 111 | if (root.isDirectory) { 112 | root.children.filter { 113 | var pathIgnore = false 114 | if (ignorePath.isNotEmpty()) { 115 | for (name in ignorePath) { 116 | if (it.path.contains(name, ignoreCase = true)) { 117 | pathIgnore = true 118 | println("${it.path} pathIgnore : $pathIgnore") 119 | break 120 | } 121 | } 122 | } 123 | !it.name.startsWith('.') && checkName(it.name) && !pathIgnore 124 | }.forEach { 125 | if (it.isDirectory) { 126 | generateFileMap(it, config, map, ignorePath) 127 | } else { 128 | config(it, regex, basePath, namedWithParent, map) 129 | } 130 | } 131 | } else { 132 | config(root, regex, basePath, namedWithParent, map) 133 | } 134 | } 135 | 136 | private fun config( 137 | it: VirtualFile, 138 | regex: Regex, 139 | basePath: String?, 140 | namedWithParent: Boolean, 141 | map: MutableMap 142 | ) { 143 | var key = it.nameWithoutExtension.replace(".", "_").toLowCamelCase(regex)///fileName style 144 | val value = it.path.removePrefix("$basePath/") 145 | if (namedWithParent) { 146 | it.parent?.let { parent -> 147 | key = "${parent.name.toLowCamelCase(regex)}${key.upperCaseFirst()}" 148 | if (map.containsKey(key)) { 149 | key = "${parent.parent.name.toLowCamelCase(regex)}${key.upperCaseFirst()}" 150 | } 151 | map[key] = value 152 | } 153 | } else { 154 | map[key] = value 155 | } 156 | } 157 | 158 | /** 159 | * 将所选择目录及子目录添加到yaml配置 160 | */ 161 | fun buildYaml(file: VirtualFile) { 162 | saveChanges() 163 | val modules = FileHelperNew.getAssets(project) 164 | var module: ModulePubSpecConfig? = null 165 | for (m in modules) { 166 | // println("file.path ${file.path} pubRoot path ${m.pubRoot.path}") 167 | if (file.path.startsWith(m.pubRoot.path)) { 168 | module = m 169 | break 170 | } 171 | } 172 | if (module != null && FlutterModuleUtils.isFlutterModule(module.module)) { 173 | // println("current module is ${module.module}") 174 | val paths = mutableListOf() 175 | val rootPath = "${module.pubRoot.path}/" 176 | if (file.isDirectory) { 177 | traversalDir(file, rootPath, paths) 178 | } else { 179 | paths.add(file.path.removePrefix(rootPath)) 180 | } 181 | // println(paths) 182 | val moduleAssets = FileHelperNew.tryGetAssetsList(module.map) 183 | if (moduleAssets != null) { 184 | val moduleDir = file.findModule(project)?.guessModuleDir() 185 | println(moduleDir?.path) 186 | moduleAssets.removeIf { 187 | var parentPath = moduleDir?.path 188 | var path = it as String 189 | path = path.removeSuffix(File.separator) 190 | if (path.contains(File.separator)) { 191 | val subIndex = path.lastIndexOf(File.separator) 192 | parentPath = "$parentPath${File.separator}${path.substring(0, subIndex + 1)}" 193 | path = path.substring(subIndex + 1, path.length) 194 | // println("parentPath:$parentPath path:$path") 195 | } 196 | val asset = File(parentPath, path) 197 | println("${asset.absolutePath} file exists : ${asset.exists()}") 198 | !asset.exists() 199 | } 200 | paths.removeIf { 201 | moduleAssets.contains(it) 202 | } 203 | } 204 | val yamlFile = module.pubRoot.pubspec.toPsiFile(project) as? YAMLFile 205 | yamlFile?.let { 206 | val psiElement = 207 | yamlFile.node.getChildren(null) 208 | .firstOrNull()?.psi?.children?.firstOrNull()?.children?.firstOrNull { it.text.startsWith("flutter:") } 209 | if (psiElement != null) { 210 | val yamlMapping = psiElement.children.first() as YAMLMapping 211 | WriteCommandAction.runWriteCommandAction(project) { 212 | var assetsValue = yamlMapping.keyValues.firstOrNull { it.keyText == "assets" } 213 | val stringBuilder = StringBuilder() 214 | moduleAssets?.forEach { 215 | stringBuilder.append(" - $it\n") 216 | } 217 | paths.forEach { 218 | stringBuilder.append(" - $it\n") 219 | } 220 | stringBuilder.removeSuffix("\n") 221 | if (assetsValue == null) { 222 | assetsValue = YAMLElementGenerator.getInstance(project) 223 | .createYamlKeyValue("assets", stringBuilder.toString()) 224 | yamlMapping.putKeyValue(assetsValue) 225 | } else { 226 | val yamlValue = PsiTreeUtil.collectElementsOfType( 227 | YAMLElementGenerator.getInstance(project) 228 | .createDummyYamlWithText(stringBuilder.toString()), YAMLSequence::class.java 229 | ).iterator().next() 230 | assetsValue.setValue(yamlValue) 231 | } 232 | } 233 | } 234 | } 235 | saveChanges() 236 | showNotify("Flutter: Configuration complete.") 237 | } else { 238 | showNotify("This module is not flutter module") 239 | } 240 | } 241 | 242 | private fun saveChanges() { 243 | ApplicationManager.getApplication().saveAll() 244 | PsiDocumentManager.getInstance(project).commitAllDocumentsUnderProgress() 245 | } 246 | 247 | private fun traversalDir(file: VirtualFile, rootPath: String, list: MutableList) { 248 | if (file.isDirectory) { 249 | list.add("${file.path.removePrefix(rootPath)}/") 250 | file.children.forEach { 251 | if (it.isDirectory) { 252 | traversalDir(it, rootPath, list) 253 | } 254 | } 255 | } 256 | } 257 | 258 | private fun checkName(name: String): Boolean { 259 | return !ignoreDir.contains(name) 260 | } 261 | 262 | } 263 | -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/utils/FileHelperNew.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.utils 2 | 3 | import com.crzsc.plugin.setting.PluginSetting 4 | import com.intellij.openapi.module.Module 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.openapi.project.guessModuleDir 7 | import com.intellij.openapi.vfs.VirtualFile 8 | import io.flutter.pub.PubRoot 9 | import io.flutter.utils.FlutterModuleUtils 10 | import org.jetbrains.kotlin.idea.util.projectStructure.allModules 11 | import org.jetbrains.kotlin.konan.file.File 12 | import org.yaml.snakeyaml.Yaml 13 | import java.io.FileInputStream 14 | import java.util.* 15 | import java.util.regex.Pattern 16 | 17 | /** 18 | * 基于Module来处理Assets 19 | */ 20 | object FileHelperNew { 21 | 22 | /** 23 | * 获取所有可用的Flutter Module的Asset配置 24 | */ 25 | @JvmStatic 26 | fun getAssets(project: Project): List { 27 | val modules = project.allModules() 28 | val folders = mutableListOf() 29 | for (module in modules) { 30 | if (FlutterModuleUtils.isFlutterModule(module)) { 31 | val moduleDir = module.guessModuleDir() 32 | if (moduleDir != null) { 33 | getPubSpecConfig(module)?.let { 34 | folders.add(it) 35 | } 36 | } 37 | } 38 | } 39 | return folders 40 | } 41 | 42 | @JvmStatic 43 | fun shouldActivateFor(project: Project): Boolean { 44 | return FlutterModuleUtils.hasFlutterModule(project) 45 | } 46 | 47 | fun tryGetAssetsList(map: Map<*, *>): MutableList<*>? { 48 | (map["flutter"] as? Map<*, *>)?.let { 49 | return it["assets"] as? MutableList<*> 50 | } 51 | return null 52 | } 53 | 54 | @JvmStatic 55 | fun getPubSpecConfig(module: Module): ModulePubSpecConfig? { 56 | try { 57 | val moduleDir = module.guessModuleDir() 58 | val pubRoot = PubRoot.forDirectory(moduleDir) 59 | if (moduleDir != null && pubRoot != null) { 60 | val fis = FileInputStream(pubRoot.pubspec.path) 61 | val pubConfigMap = Yaml().load(fis) as? Map 62 | if (pubConfigMap != null) { 63 | val assetVFiles = mutableListOf() 64 | (pubConfigMap["flutter"] as? Map<*, *>)?.let { configureMap -> 65 | (configureMap["assets"] as? ArrayList<*>)?.let { list -> 66 | for (path in list) { 67 | moduleDir.findFileByRelativePath(path as String)?.let { 68 | if (it.isDirectory) { 69 | val index = path.indexOf("/") 70 | val assetsPath = if (index == -1) { 71 | path 72 | } else { 73 | path.substring(0, index) 74 | } 75 | val assetVFile = moduleDir.findChild(assetsPath) 76 | ?: moduleDir.createChildDirectory(this, assetsPath) 77 | if (!assetVFiles.contains(assetVFile)) { 78 | assetVFiles.add(assetVFile) 79 | } 80 | } else { 81 | if (!assetVFiles.contains(it)) { 82 | assetVFiles.add(it) 83 | } 84 | } 85 | } 86 | } 87 | } 88 | } 89 | return ModulePubSpecConfig( 90 | module, 91 | pubRoot, 92 | assetVFiles, 93 | pubConfigMap, 94 | ) 95 | } 96 | } 97 | } catch (e: Exception) { 98 | e.printStackTrace() 99 | return null 100 | } 101 | return null 102 | } 103 | 104 | /** 105 | * 读取配置 106 | */ 107 | private fun readSetting(config: ModulePubSpecConfig, key: String): Any? { 108 | (config.map[Constants.KEY_CONFIGURATION_MAP] as? Map<*, *>)?.let { configureMap -> 109 | return configureMap[key] 110 | } 111 | return null 112 | } 113 | 114 | /** 115 | * 是否开启了自动检测 116 | */ 117 | fun isAutoDetectionEnable(config: ModulePubSpecConfig): Boolean { 118 | return readSetting(config, Constants.KEY_AUTO_DETECTION) as Boolean? ?: PluginSetting.instance.autoDetection 119 | } 120 | 121 | /** 122 | * 是否根据父文件夹命名 默认true 123 | */ 124 | fun isNamedWithParent(config: ModulePubSpecConfig): Boolean { 125 | return readSetting(config, Constants.KEY_NAMED_WITH_PARENT) as Boolean? 126 | ?: PluginSetting.instance.namedWithParent 127 | } 128 | 129 | fun isWithLeadingWithPackageName(config: ModulePubSpecConfig): Boolean { 130 | return readSetting(config, Constants.KEY_LEADING_WITH_PACKAGE_NAME) as Boolean? 131 | ?: PluginSetting.instance.leadingWithPackageName 132 | } 133 | 134 | /** 135 | * 读取生成的类名配置 136 | */ 137 | fun getGeneratedClassName(config: ModulePubSpecConfig): String { 138 | return readSetting(config, Constants.KEY_CLASS_NAME) as String? ?: PluginSetting.instance.className 139 | ?: Constants.DEFAULT_CLASS_NAME 140 | } 141 | 142 | /** 143 | * 读取文件分割配置 144 | */ 145 | fun getFilenameSplitPattern(config: ModulePubSpecConfig): String { 146 | return try { 147 | val pattern = 148 | readSetting(config, Constants.FILENAME_SPLIT_PATTERN) as String? 149 | ?: PluginSetting.instance.filenameSplitPattern ?: Constants.DEFAULT_FILENAME_SPLIT_PATTERN 150 | Pattern.compile(pattern) 151 | pattern 152 | } catch (e: Exception) { 153 | e.printStackTrace() 154 | Constants.DEFAULT_FILENAME_SPLIT_PATTERN 155 | } 156 | } 157 | 158 | /** 159 | * 读取忽略文件目录 160 | */ 161 | fun getPathIgnore(config: ModulePubSpecConfig): List { 162 | return try { 163 | val paths = 164 | readSetting(config, Constants.PATH_IGNORE) as List? 165 | ?: emptyList() 166 | paths 167 | } catch (e: Exception) { 168 | e.printStackTrace() 169 | emptyList() 170 | } 171 | } 172 | 173 | /** 174 | * 获取generated自动生成目录 175 | * 从yaml中读取 176 | */ 177 | private fun getGeneratedFilePath(config: ModulePubSpecConfig): VirtualFile { 178 | return config.pubRoot.lib?.let { lib -> 179 | // 没有配置则返回默认path 180 | val filePath: String = readSetting(config, Constants.KEY_OUTPUT_DIR) as String? 181 | ?: PluginSetting.instance.filePath ?: Constants.DEFAULT_OUTPUT_DIR 182 | println("getGeneratedFilePath $filePath") 183 | if (!filePath.contains(File.separator)) { 184 | return@let lib.findOrCreateChildDir(lib, filePath) 185 | } else { 186 | var file = lib 187 | filePath.split(File.separator).forEach { dir -> 188 | if (dir.isNotEmpty()) { 189 | file = file.findOrCreateChildDir(file, dir) 190 | } 191 | } 192 | return@let file 193 | } 194 | }!! 195 | } 196 | 197 | private fun VirtualFile.findOrCreateChildDir(requestor: Any, name: String): VirtualFile { 198 | val child = findChild(name) 199 | return child ?: createChildDirectory(requestor, name) 200 | } 201 | 202 | /** 203 | * 获取需要生成的文件 如果没有则会创建文件 204 | */ 205 | fun getGeneratedFile(config: ModulePubSpecConfig): VirtualFile { 206 | return getGeneratedFilePath(config).let { 207 | val configName = getGeneratedFileName(config) 208 | return@let it.findOrCreateChildData( 209 | it, 210 | "$configName.dart" 211 | ) 212 | } 213 | } 214 | 215 | fun getGeneratedFileName(config: ModulePubSpecConfig): String = 216 | readSetting(config, Constants.KEY_OUTPUT_FILENAME) as? String ?: PluginSetting.instance.fileName 217 | ?: Constants.DEFAULT_CLASS_NAME.lowercase() 218 | 219 | } 220 | 221 | /** 222 | * 模块Flutter配置信息 223 | */ 224 | data class ModulePubSpecConfig( 225 | val module: Module, 226 | val pubRoot: PubRoot, 227 | val assetVFiles: List, 228 | val map: Map, 229 | val isFlutterModule: Boolean = FlutterModuleUtils.isFlutterModule(module) 230 | ) { 231 | fun getLeadingWithPackageNameIfChecked(): String { 232 | if (FileHelperNew.isWithLeadingWithPackageName(this)) { 233 | return "packages/${map["name"]}/" 234 | } 235 | return ""; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/utils/Messages.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.utils 2 | 3 | import java.util.* 4 | 5 | 6 | fun message(key: String): String { 7 | val l = if (Locale.getDefault().language.equals("us") or Locale.getDefault().language.equals("en")) "" else "_" + Locale.getDefault().language 8 | return ResourceBundle.getBundle("messages.MessagesBundle$l").getString(key) 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/utils/PluginUtils.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.utils 2 | 3 | import com.intellij.notification.NotificationDisplayType 4 | import com.intellij.notification.NotificationGroup 5 | import com.intellij.notification.NotificationType 6 | import com.intellij.notification.Notifications 7 | import com.intellij.openapi.application.ApplicationManager 8 | import com.intellij.openapi.fileEditor.FileEditorManager 9 | import com.intellij.openapi.fileEditor.OpenFileDescriptor 10 | import com.intellij.openapi.vfs.VirtualFile 11 | import com.intellij.psi.PsiElement 12 | 13 | object PluginUtils { 14 | 15 | @JvmStatic 16 | fun showNotify(message: String?) { 17 | val notificationGroup = NotificationGroup("FlutterAssetsGenerator", NotificationDisplayType.BALLOON, true) 18 | ApplicationManager.getApplication().invokeLater { 19 | val notification = notificationGroup.createNotification(message!!, NotificationType.INFORMATION) 20 | Notifications.Bus.notify(notification) 21 | } 22 | } 23 | 24 | @JvmStatic 25 | fun showError(message: String?) { 26 | val notificationGroup = NotificationGroup("FlutterAssetsGenerator", NotificationDisplayType.BALLOON, true) 27 | ApplicationManager.getApplication().invokeLater { 28 | val notification = notificationGroup.createNotification(message!!, NotificationType.ERROR) 29 | Notifications.Bus.notify(notification) 30 | } 31 | } 32 | 33 | /** 34 | * 转换小写驼峰式 35 | */ 36 | fun String.toLowCamelCase(regex: Regex): String { 37 | return if (this.isEmpty()) { 38 | this 39 | } else { 40 | val newStr = this.replace(Regex("[@]"), "") 41 | val split = newStr.split(regex) 42 | val sb = StringBuilder() 43 | for (i in split.indices) { 44 | if (i == 0) { 45 | sb.append(split[i].lowerCaseFirst()) 46 | } else { 47 | sb.append(split[i].upperCaseFirst()) 48 | } 49 | } 50 | return sb.toString() 51 | } 52 | } 53 | 54 | /** 55 | * 新窗口打开文件 56 | */ 57 | fun PsiElement.openFile(vFile: VirtualFile) { 58 | FileEditorManager.getInstance(project) 59 | .openTextEditor(OpenFileDescriptor(project, vFile), true) 60 | } 61 | 62 | fun String.lowerCaseFirst(): String { 63 | return if (this.isEmpty()) { 64 | this 65 | } else { 66 | "${this[0].lowercase()}${this.subSequence(1, this.length)}" 67 | } 68 | } 69 | 70 | /** 71 | * 首字母大写 72 | */ 73 | fun String.upperCaseFirst(): String { 74 | return if (this.isEmpty()) { 75 | this 76 | } else { 77 | "${this[0].uppercase()}${this.subSequence(1, this.length)}" 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /src/main/java/com/crzsc/plugin/utils/StringExt.kt: -------------------------------------------------------------------------------- 1 | package com.crzsc.plugin.utils 2 | 3 | /** 4 | * 是svg拓展名 5 | */ 6 | val String.isSvgExtension: Boolean 7 | get() = endsWith(".svg", true) 8 | 9 | /** 10 | * 是Image拓展名 11 | */ 12 | val String.isImageExtension: Boolean 13 | get() = endsWith(".png", true) 14 | || endsWith(".jpg", true) 15 | || endsWith(".webp", true) 16 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | com.crzsc.FlutterAssetsGenerator 3 | FlutterAssetsGenerator 4 | Ray&Zsc 5 | 6 | 8 | com.intellij.modules.json 9 | com.intellij.modules.lang 10 | 11 | com.intellij.modules.all 12 | com.intellij.modules.platform 13 | Dart 14 | org.jetbrains.kotlin 15 | io.flutter 16 | 17 | 18 | 19 | 20 | 23 | 24 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 57 | 58 | 59 | 60 | 61 | 62 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 81 | 82 | 83 | 84 | 85 | 87 | 88 | 89 | 90 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/main/resources/messages/MessagesBun_zh.properties: -------------------------------------------------------------------------------- 1 | notFlutterProject=该项目不是flutter项目 2 | settingsAutoDetection=启用自动检测 3 | settingsNamed=继承命名 4 | settingsFilePath=输入生成的文件路径: 5 | settingsFileName=输入生成的文件名: 6 | settingsClassName=输入生成的类名: 7 | settingsLeadingWithPackageName=以包名开头 8 | settingsSplitPattern=输入文件名分割模式: -------------------------------------------------------------------------------- /src/main/resources/messages/MessagesBundle.properties: -------------------------------------------------------------------------------- 1 | notFlutterProject=This project is not the flutter project 2 | settingsAutoDetection=Enable auto-detection 3 | settingsNamed=Named with parent 4 | settingsFilePath=Enter generated file path: 5 | settingsFileName=Enter generated file name: 6 | settingsClassName=Enter generated class name: 7 | settingsLeadingWithPackageName=Enable leading with package name 8 | settingsSplitPattern=Enter filename split pattern: 9 | --------------------------------------------------------------------------------