From 79509aac0e0b4804c04efc439027c30cf83c0d37 Mon Sep 17 00:00:00 2001 From: Yevheniy Oliynyk Date: Thu, 1 Feb 2024 09:04:13 +0100 Subject: [PATCH] feat: Strings-based projects support (#119) --- .github/workflows/basic.yml | 9 +- .github/workflows/publish.yml | 9 +- README.md | 7 + build.gradle | 61 ++-- gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 43462 bytes gradle/wrapper/gradle-wrapper.properties | 7 +- gradlew | 301 +++++++++++------- gradlew.bat | 56 ++-- src/main/java/com/crowdin/Constants.java | 16 + .../com/crowdin/action/ActionContext.java | 26 ++ .../com/crowdin/action/BackgroundAction.java | 83 ++++- .../com/crowdin/action/DownloadAction.java | 127 ++++++-- .../DownloadSourceFromContextAction.java | 87 +++-- .../crowdin/action/DownloadSourcesAction.java | 138 +++++--- .../DownloadTranslationFromContextAction.java | 110 +++---- .../java/com/crowdin/action/UploadAction.java | 87 +++-- .../action/UploadFromContextAction.java | 77 +++-- .../action/UploadTranslationsAction.java | 139 +++++--- .../UploadTranslationsFromContextAction.java | 142 +++++---- .../activity/CrowdinStartupActivity.java | 44 ++- .../java/com/crowdin/client/BranchInfo.java | 15 +- src/main/java/com/crowdin/client/Crowdin.java | 241 +++++++++----- .../com/crowdin/client/CrowdinClient.java | 27 +- .../com/crowdin/client/CrowdinProperties.java | 100 +++++- .../client/CrowdinPropertiesLoader.java | 40 ++- .../java/com/crowdin/client/FileBean.java | 67 +++- .../com/crowdin/client/RequestBuilder.java | 17 + .../StringsCompletionContributor.java | 7 +- .../com/crowdin/event/FileChangeListener.java | 33 +- .../java/com/crowdin/logic/BranchLogic.java | 16 +- .../java/com/crowdin/logic/ContextLogic.java | 36 ++- .../crowdin/logic/DownloadBundleLogic.java | 63 ++++ .../logic/DownloadTranslationsLogic.java | 91 ++---- .../java/com/crowdin/logic/SourceLogic.java | 124 ++++++-- .../CrowdinProjectCacheProvider.java | 167 +++++++--- .../{logic => service}/CrowdinSettings.java | 12 +- .../com/crowdin/service/ProjectService.java | 53 +++ .../ui/TranslationProgressWindowFactory.java | 70 ---- .../RefreshTranslationProgressAction.java | 140 -------- .../ui/{ => dialog}/ConfirmActionDialog.java | 2 +- .../ui/{ => dialog}/ConfirmActionPanel.form | 2 +- .../ui/{ => dialog}/ConfirmActionPanel.java | 2 +- .../java/com/crowdin/ui/panel/ContentTab.java | 7 + .../ui/panel/CrowdinPanelWindowFactory.java | 157 +++++++++ .../ui/panel/download/DownloadWindow.form | 30 ++ .../ui/panel/download/DownloadWindow.java | 136 ++++++++ .../panel/download/action/CollapseAction.java | 64 ++++ .../panel/download/action/ExpandAction.java | 64 ++++ .../panel/download/action/RefreshAction.java | 113 +++++++ .../progress}/TranslationProgressWindow.form | 2 +- .../progress}/TranslationProgressWindow.java | 41 ++- .../progress}/TreeCellLanguage.form | 2 +- .../progress}/TreeCellLanguage.java | 2 +- .../action/GroupProgressByFiles.java | 24 +- .../RefreshTranslationProgressAction.java | 136 ++++++++ .../crowdin/ui/panel/upload/UploadWindow.form | 28 ++ .../crowdin/ui/panel/upload/UploadWindow.java | 64 ++++ .../panel/upload/action/CollapseAction.java | 64 ++++ .../ui/panel/upload/action/ExpandAction.java | 64 ++++ .../ui/panel/upload/action/RefreshAction.java | 92 ++++++ .../java/com/crowdin/ui/tree/CellData.java | 118 +++++++ .../com/crowdin/ui/tree/CellRenderer.java | 26 ++ .../java/com/crowdin/ui/tree/FileTree.java | 138 ++++++++ .../com/crowdin/ui/tree/FilesTreeItem.form | 39 +++ .../com/crowdin/ui/tree/FilesTreeItem.java | 21 ++ .../java/com/crowdin/util/ActionUtils.java | 19 -- .../com/crowdin/util/CrowdinFileUtil.java | 12 +- src/main/java/com/crowdin/util/FileUtil.java | 83 ++++- src/main/java/com/crowdin/util/GitUtil.java | 2 +- .../com/crowdin/util/LanguageMapping.java | 13 +- .../com/crowdin/util/NotificationUtil.java | 23 +- .../com/crowdin/util/PlaceholderUtil.java | 7 +- .../java/com/crowdin/util/PropertyUtil.java | 1 - .../java/com/crowdin/util/StringUtils.java | 41 +++ src/main/java/com/crowdin/util/UIUtil.java | 6 +- src/main/java/com/crowdin/util/Util.java | 13 +- src/main/resources/META-INF/plugin.xml | 48 ++- src/main/resources/icons/download-sources.svg | 4 + .../resources/icons/download-sources_dark.svg | 4 + src/main/resources/icons/download.svg | 3 + src/main/resources/icons/download_dark.svg | 3 + src/main/resources/icons/folder.svg | 13 + src/main/resources/icons/folder_dark.svg | 13 + src/main/resources/icons/upload-sources.svg | 3 + .../resources/icons/upload-sources_dark.svg | 3 + src/main/resources/icons/upload.svg | 4 + src/main/resources/icons/upload_dark.svg | 4 + .../resources/messages/messages.properties | 3 +- .../CrowdinProjectCacheProviderTest.java | 76 +++-- .../java/com/crowdin/client/MockCrowdin.java | 53 ++- .../crowdin/logic/CrowdinSettingsTest.java | 3 +- .../java/com/crowdin/ui/FileTreeTest.java | 130 ++++++++ .../com/crowdin/util/CrowdinFileUtilTest.java | 1 - 93 files changed, 3640 insertions(+), 1231 deletions(-) create mode 100644 src/main/java/com/crowdin/action/ActionContext.java create mode 100644 src/main/java/com/crowdin/logic/DownloadBundleLogic.java rename src/main/java/com/crowdin/{client => service}/CrowdinProjectCacheProvider.java (51%) rename src/main/java/com/crowdin/{logic => service}/CrowdinSettings.java (86%) create mode 100644 src/main/java/com/crowdin/service/ProjectService.java delete mode 100644 src/main/java/com/crowdin/ui/TranslationProgressWindowFactory.java delete mode 100644 src/main/java/com/crowdin/ui/action/RefreshTranslationProgressAction.java rename src/main/java/com/crowdin/ui/{ => dialog}/ConfirmActionDialog.java (96%) rename src/main/java/com/crowdin/ui/{ => dialog}/ConfirmActionPanel.form (97%) rename src/main/java/com/crowdin/ui/{ => dialog}/ConfirmActionPanel.java (94%) create mode 100644 src/main/java/com/crowdin/ui/panel/ContentTab.java create mode 100644 src/main/java/com/crowdin/ui/panel/CrowdinPanelWindowFactory.java create mode 100644 src/main/java/com/crowdin/ui/panel/download/DownloadWindow.form create mode 100644 src/main/java/com/crowdin/ui/panel/download/DownloadWindow.java create mode 100644 src/main/java/com/crowdin/ui/panel/download/action/CollapseAction.java create mode 100644 src/main/java/com/crowdin/ui/panel/download/action/ExpandAction.java create mode 100644 src/main/java/com/crowdin/ui/panel/download/action/RefreshAction.java rename src/main/java/com/crowdin/ui/{ => panel/progress}/TranslationProgressWindow.form (97%) rename src/main/java/com/crowdin/ui/{ => panel/progress}/TranslationProgressWindow.java (88%) rename src/main/java/com/crowdin/ui/{ => panel/progress}/TreeCellLanguage.form (97%) rename src/main/java/com/crowdin/ui/{ => panel/progress}/TreeCellLanguage.java (97%) rename src/main/java/com/crowdin/ui/{ => panel/progress}/action/GroupProgressByFiles.java (68%) create mode 100644 src/main/java/com/crowdin/ui/panel/progress/action/RefreshTranslationProgressAction.java create mode 100644 src/main/java/com/crowdin/ui/panel/upload/UploadWindow.form create mode 100644 src/main/java/com/crowdin/ui/panel/upload/UploadWindow.java create mode 100644 src/main/java/com/crowdin/ui/panel/upload/action/CollapseAction.java create mode 100644 src/main/java/com/crowdin/ui/panel/upload/action/ExpandAction.java create mode 100644 src/main/java/com/crowdin/ui/panel/upload/action/RefreshAction.java create mode 100644 src/main/java/com/crowdin/ui/tree/CellData.java create mode 100644 src/main/java/com/crowdin/ui/tree/CellRenderer.java create mode 100644 src/main/java/com/crowdin/ui/tree/FileTree.java create mode 100644 src/main/java/com/crowdin/ui/tree/FilesTreeItem.form create mode 100644 src/main/java/com/crowdin/ui/tree/FilesTreeItem.java delete mode 100644 src/main/java/com/crowdin/util/ActionUtils.java create mode 100644 src/main/java/com/crowdin/util/StringUtils.java create mode 100644 src/main/resources/icons/download-sources.svg create mode 100644 src/main/resources/icons/download-sources_dark.svg create mode 100644 src/main/resources/icons/download.svg create mode 100644 src/main/resources/icons/download_dark.svg create mode 100644 src/main/resources/icons/folder.svg create mode 100644 src/main/resources/icons/folder_dark.svg create mode 100644 src/main/resources/icons/upload-sources.svg create mode 100644 src/main/resources/icons/upload-sources_dark.svg create mode 100644 src/main/resources/icons/upload.svg create mode 100644 src/main/resources/icons/upload_dark.svg create mode 100644 src/test/java/com/crowdin/ui/FileTreeTest.java diff --git a/.github/workflows/basic.yml b/.github/workflows/basic.yml index 917ece6..fe599a1 100644 --- a/.github/workflows/basic.yml +++ b/.github/workflows/basic.yml @@ -17,17 +17,14 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 8 + java-version: 17 cache: 'gradle' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - - name: Execute Gradle run: ./gradlew build buildPlugin verifyPlugin diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index d42facb..b338835 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -8,17 +8,14 @@ jobs: package: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions/setup-java@v3 + - uses: actions/setup-java@v4 with: distribution: temurin - java-version: 8 + java-version: 17 cache: 'gradle' - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - - name: Execute Gradle run: ./gradlew build buildPlugin verifyPlugin diff --git a/README.md b/README.md index e084d34..65d33e7 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,13 @@ files.1.excluded-target-languages=uk # For a specific filegroup, high priority files.2.excluded-target-languages=fr # For a specific filegroup, high priority ``` +To specify cleanup mode or update strings flags for Strings-based projects use `cleanup-mode` and `update-strings`: + +```ini +files.1.cleanup-mode=true +files.1.update-strings=true +``` + ### Translations Upload Options The below properties can be used to configure the import options to the uploaded translations diff --git a/build.gradle b/build.gradle index f67e62f..9f091b7 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ plugins { id 'java' - id 'org.jetbrains.intellij' version '0.6.5' + id 'org.jetbrains.intellij' version '1.17.0' id 'jacoco' } group 'com.crowdin.crowdin-idea' version '1.6.3' -sourceCompatibility = 1.8 +sourceCompatibility = '17' repositories { mavenCentral() @@ -16,52 +16,59 @@ repositories { dependencies { - compile 'org.projectlombok:lombok:1.18.10' - annotationProcessor 'org.projectlombok:lombok:1.18.10' + implementation 'net.lingala.zip4j:zip4j:2.11.3' + implementation 'com.github.crowdin:crowdin-api-client-java:1.14.0' + implementation 'commons-io:commons-io:2.15.1' - testCompile group: 'junit', name: 'junit', version: '4.13.1' - compile group: 'net.lingala.zip4j', name: 'zip4j', version: '2.11.3' - compile 'com.github.crowdin:crowdin-api-client-java:1.12.0' - implementation 'org.apache.commons:commons-lang3:3.12.0' -// compile group: 'commons-io', name: 'commons-io', version: '2.6' //to run '2017.1.6' idea - - testCompile('org.junit.jupiter:junit-jupiter:5.5.2') - testCompile('org.hamcrest:hamcrest:2.2') - testCompile('org.mockito:mockito-core:2.1.0') + testImplementation 'junit:junit:4.13.1' + testImplementation 'org.junit.jupiter:junit-jupiter:5.5.2' + testImplementation 'org.hamcrest:hamcrest:2.2' + testImplementation 'org.mockito:mockito-core:2.1.0' } test { useJUnitPlatform() } -// See https://github.com/JetBrains/gradle-intellij-plugin/ intellij { -// version '2017.1.6' //current since-version //requires additional libraries - version '2019.2.3' -// version '2020.3.1' //current last version //requires 11 JDK to execute 'runIde' - plugins 'git4idea' - updateSinceUntilBuild false + version = '2022.3.3' + plugins = ['Git4Idea'] + updateSinceUntilBuild = false } patchPluginXml { - changeNotes """ + changeNotes = """ - Bugfixes""" } wrapper { - gradleVersion = '5.2.1' + gradleVersion = '8.5' } jacoco { - toolVersion = "0.8.5" - reportsDir = file("$buildDir/reports") + toolVersion = '0.8.10' + reportsDirectory = layout.buildDirectory.dir('reports') +} + +//https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin-faq.html#jacoco-reports-0-coverage +test { + jacoco { + includeNoLocationClasses = true + excludes = ["jdk.internal.*"] + } } jacocoTestReport { + classDirectories.setFrom(instrumentCode) + reports { - xml.enabled true - csv.enabled false - xml.destination file("${buildDir}/coverage.xml") + xml.required = true + csv.required = false + xml.outputLocation = layout.buildDirectory.file('coverage.xml') } - getExecutionData().setFrom("$buildDir/jacoco/test.exec") + getExecutionData().setFrom(layout.buildDirectory.file('jacoco/test.exec')) +} + +jacocoTestCoverageVerification { + classDirectories.setFrom(instrumentCode) } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738cbd051603d91cc39de6cb000dd98fe6b02..d64cd4917707c1f8861d8cb53dd15194d4248596 100644 GIT binary patch literal 43462 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 literal 55190 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 z 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the location of your Java installation." + fi fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 6d57edc..93e3f59 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,4 +1,20 @@ -@if "%DEBUG%" == "" @echo off +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -9,19 +25,23 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +55,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,38 +65,26 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal diff --git a/src/main/java/com/crowdin/Constants.java b/src/main/java/com/crowdin/Constants.java index 329385a..77028a5 100644 --- a/src/main/java/com/crowdin/Constants.java +++ b/src/main/java/com/crowdin/Constants.java @@ -27,6 +27,8 @@ public final class Constants { public static final String PROPERTY_FILES_TRANSLATIONS_PATTERN = "files.%stranslation"; public static final Pattern PROPERTY_FILES_TRANSLATIONS_REGEX = Pattern.compile("^files\\.(|\\d+\\.)translation$"); public static final String PROPERTY_FILES_EXCLUDED_TARGET_LANGUAGES_PATTERN = "files.%sexcluded-target-languages"; + public static final String PROPERTY_FILES_CLEANUP_MODE_PATTERN = "files.%scleanup-mode"; + public static final String PROPERTY_FILES_UPDATE_STRINGS_PATTERN = "files.%supdate-strings"; public static final String PROPERTY_FILES_LABELS_PATTERN = "files.%slabels"; public static final String PROPERTY_AUTO_UPLOAD = "auto-upload"; public static final String PROPERTY_DISABLE_BRANCHES = "disable-branches"; @@ -47,4 +49,18 @@ public final class Constants { public static final String PROPERTY_AUTO_APPROVE_IMPORTED = "auto-approve-imported"; public static final String PROPERTY_TRANSLATE_HIDDEN = "translate-hidden"; + + public static final String PROGRESS_TOOLBAR_ID = "Crowdin.TranslationProgressToolbar"; + + public static final String TOOLWINDOW_ID = "Crowdin"; + public static final String UPLOAD_TOOLBAR_ID = "Crowdin.UploadToolbar"; + public static final String DOWNLOAD_TOOLBAR_ID = "Crowdin.DownloadToolbar"; + + public static final String PROGRESS_REFRESH_ACTION = "Crowdin.RefreshTranslationProgressAction"; + public static final String UPLOAD_REFRESH_ACTION = "Crowdin.RefreshUploadAction"; + public static final String DOWNLOAD_REFRESH_ACTION = "Crowdin.RefreshDownloadAction"; + public static final String DOWNLOAD_TRANSLATIONS_ACTION = "Crowdin.DownloadTranslations"; + public static final String DOWNLOAD_SOURCES_ACTION = "Crowdin.DownloadSources"; + + public static final String PROGRESS_GROUP_FILES_BY_FILE_ACTION = "Crowdin.GroupByFiles"; } diff --git a/src/main/java/com/crowdin/action/ActionContext.java b/src/main/java/com/crowdin/action/ActionContext.java new file mode 100644 index 0000000..3c9830d --- /dev/null +++ b/src/main/java/com/crowdin/action/ActionContext.java @@ -0,0 +1,26 @@ +package com.crowdin.action; + +import com.crowdin.client.Crowdin; +import com.crowdin.client.CrowdinProperties; +import com.crowdin.client.sourcefiles.model.Branch; +import com.crowdin.service.CrowdinProjectCacheProvider; +import com.intellij.openapi.vfs.VirtualFile; + +public class ActionContext { + + public final String branchName; + public final Branch branch; + public final VirtualFile root; + public final CrowdinProperties properties; + public final Crowdin crowdin; + public final CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache; + + public ActionContext(String branchName, Branch branch, VirtualFile root, CrowdinProperties properties, Crowdin crowdin, CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache) { + this.branchName = branchName; + this.branch = branch; + this.root = root; + this.properties = properties; + this.crowdin = crowdin; + this.crowdinProjectCache = crowdinProjectCache; + } +} diff --git a/src/main/java/com/crowdin/action/BackgroundAction.java b/src/main/java/com/crowdin/action/BackgroundAction.java index 848edcd..631997a 100644 --- a/src/main/java/com/crowdin/action/BackgroundAction.java +++ b/src/main/java/com/crowdin/action/BackgroundAction.java @@ -1,27 +1,103 @@ package com.crowdin.action; +import com.crowdin.client.Crowdin; +import com.crowdin.client.CrowdinProperties; +import com.crowdin.client.CrowdinPropertiesLoader; +import com.crowdin.client.sourcefiles.model.Branch; +import com.crowdin.logic.BranchLogic; +import com.crowdin.service.CrowdinProjectCacheProvider; +import com.crowdin.service.CrowdinSettings; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; +import com.crowdin.util.StringUtils; +import com.crowdin.util.UIUtil; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.progress.ProgressManager; import com.intellij.openapi.progress.Task; -import lombok.NonNull; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; import javax.swing.*; +import java.util.Optional; + +import static com.crowdin.Constants.MESSAGES_BUNDLE; public abstract class BackgroundAction extends AnAction { - public BackgroundAction() { } + public BackgroundAction() { + } public BackgroundAction(String text, String description, Icon icon) { super(text, description, icon); } - protected abstract void performInBackground(@NonNull AnActionEvent e, @NonNull ProgressIndicator indicator); + protected abstract void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator); protected abstract String loadingText(AnActionEvent e); + protected Optional prepare( + Project project, + ProgressIndicator indicator, + boolean checkForManagerAccess, + boolean createBranchIfNotExists, + boolean realodCrowdinCache, + String question, + String okBtn + ) { + VirtualFile root = FileUtil.getProjectBaseDir(project); + + CrowdinSettings crowdinSettings = project.getService(CrowdinSettings.class); + + if (!StringUtils.isEmpty(question) && !StringUtils.isEmpty(okBtn)) { + boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString(question), okBtn); + if (!confirmation) { + return Optional.empty(); + } + if (indicator != null) { + indicator.checkCanceled(); + } + } + + CrowdinProperties properties; + try { + properties = CrowdinPropertiesLoader.load(project); + } catch (Exception e) { + NotificationUtil.showErrorMessage(project, e.getMessage()); + return Optional.empty(); + } + NotificationUtil.setLogDebugLevel(properties.isDebug()); + NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); + + Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); + + BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); + String branchName = branchLogic.acquireBranchName(); + if (indicator != null) { + indicator.checkCanceled(); + } + + CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = + project.getService(CrowdinProjectCacheProvider.class).getInstance(crowdin, branchName, realodCrowdinCache); + + if (checkForManagerAccess) { + if (!crowdinProjectCache.isManagerAccess()) { + NotificationUtil.showErrorMessage(project, "You need to have manager access to perform this action"); + return Optional.empty(); + } + } + + Branch branch = branchLogic.getBranch(crowdinProjectCache, createBranchIfNotExists); + + if (indicator != null) { + indicator.checkCanceled(); + } + + return Optional.of(new ActionContext(branchName, branch, root, properties, crowdin, crowdinProjectCache)); + } + @Override public void actionPerformed(@NotNull AnActionEvent e) { ProgressManager.getInstance().run(new Task.Backgroundable(e.getProject(), "Crowdin") { @@ -31,6 +107,5 @@ public void run(@NotNull ProgressIndicator indicator) { performInBackground(e, indicator); } }); - } } diff --git a/src/main/java/com/crowdin/action/DownloadAction.java b/src/main/java/com/crowdin/action/DownloadAction.java index 3080be5..5fe79e4 100644 --- a/src/main/java/com/crowdin/action/DownloadAction.java +++ b/src/main/java/com/crowdin/action/DownloadAction.java @@ -1,71 +1,128 @@ package com.crowdin.action; -import com.crowdin.client.Crowdin; -import com.crowdin.client.CrowdinProjectCacheProvider; -import com.crowdin.client.CrowdinProperties; -import com.crowdin.client.CrowdinPropertiesLoader; -import com.crowdin.client.sourcefiles.model.Branch; -import com.crowdin.logic.BranchLogic; -import com.crowdin.logic.CrowdinSettings; +import com.crowdin.client.bundles.model.Bundle; +import com.crowdin.logic.DownloadBundleLogic; import com.crowdin.logic.DownloadTranslationsLogic; -import com.crowdin.util.*; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.panel.download.DownloadWindow; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; import static com.crowdin.Constants.MESSAGES_BUNDLE; +import static com.crowdin.Constants.TOOLWINDOW_ID; public class DownloadAction extends BackgroundAction { + private boolean enabled = false; + private boolean visible = false; + private String text = ""; + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + @Override - public void performInBackground(AnActionEvent anActionEvent, ProgressIndicator indicator) { + public void update(@NotNull AnActionEvent e) { + if (e.getPlace().equals(TOOLWINDOW_ID)) { + this.enabled = e.getPresentation().isEnabled(); + this.visible = e.getPresentation().isVisible(); + this.text = e.getPresentation().getText(); + } + e.getPresentation().setEnabled(!isInProgress.get() && enabled); + e.getPresentation().setVisible(visible); + e.getPresentation().setText(text); + } + + @Override + public void performInBackground(AnActionEvent anActionEvent, @NotNull ProgressIndicator indicator) { Project project = anActionEvent.getProject(); - try { - VirtualFile root = FileUtil.getProjectBaseDir(project); - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); + if (project == null) { + return; + } - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.download"), "Download"); - if (!confirmation) { - return; - } - indicator.checkCanceled(); + isInProgress.set(true); + try { + Optional context = super.prepare( + project, + indicator, + true, + false, + true, + "messages.confirm.download", + "Download" + ); - CrowdinProperties properties; - try { - properties = CrowdinPropertiesLoader.load(project); - } catch (Exception e) { - NotificationUtil.showErrorMessage(project, e.getMessage()); + if (context.isEmpty()) { return; } - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); + if (context.get().crowdinProjectCache.isStringsBased()) { + if (context.get().branch == null) { + NotificationUtil.showErrorMessage(project, "Branch is missing"); + return; + } + + DownloadWindow window = project.getService(ProjectService.class).getDownloadWindow(); + if (window == null) { + return; + } - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - indicator.checkCanceled(); + Bundle bundle = window.getSelectedBundle(); + + if (bundle == null) { + NotificationUtil.showErrorMessage(project, "Bundle not selected"); + return; + } + + (new DownloadBundleLogic(project, context.get().crowdin, context.get().root, bundle)).process(); + return; + } - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); + List selectedFiles = Optional + .ofNullable(project.getService(ProjectService.class).getDownloadWindow()) + .map(DownloadWindow::getSelectedFiles) + .orElse(Collections.emptyList()) + .stream() + .map(str -> Paths.get(context.get().root.getPath(), str)) + .toList(); - if (!crowdinProjectCache.isManagerAccess()) { - NotificationUtil.showErrorMessage(project, "You need to have manager access to perform this action"); + if (!selectedFiles.isEmpty()) { + for (Path file : selectedFiles) { + try { + VirtualFile virtualFile = FileUtil.findVFileByPath(file); + DownloadTranslationFromContextAction.performDownload(this, context.get(), virtualFile); + } catch (Exception e) { + NotificationUtil.logErrorMessage(project, e); + NotificationUtil.showWarningMessage(project, e.getMessage()); + } + } return; } - Branch branch = branchLogic.getBranch(crowdinProjectCache, false); - (new DownloadTranslationsLogic(project, crowdin, properties, root, crowdinProjectCache, branch)).process(); + (new DownloadTranslationsLogic(project, context.get().crowdin, context.get().properties, context.get().root, context.get().crowdinProjectCache, context.get().branch)).process(); } catch (ProcessCanceledException e) { throw e; } catch (Exception e) { NotificationUtil.logErrorMessage(project, e); NotificationUtil.showErrorMessage(project, e.getMessage()); + } finally { + isInProgress.set(false); + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, false)); } } diff --git a/src/main/java/com/crowdin/action/DownloadSourceFromContextAction.java b/src/main/java/com/crowdin/action/DownloadSourceFromContextAction.java index 73f2f80..221bcbd 100644 --- a/src/main/java/com/crowdin/action/DownloadSourceFromContextAction.java +++ b/src/main/java/com/crowdin/action/DownloadSourceFromContextAction.java @@ -1,82 +1,71 @@ package com.crowdin.action; -import com.crowdin.client.Crowdin; -import com.crowdin.client.CrowdinProjectCacheProvider; -import com.crowdin.client.CrowdinProperties; -import com.crowdin.client.CrowdinPropertiesLoader; -import com.crowdin.client.sourcefiles.model.Branch; -import com.crowdin.logic.BranchLogic; import com.crowdin.logic.ContextLogic; -import com.crowdin.logic.CrowdinSettings; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; import com.crowdin.util.FileUtil; import com.crowdin.util.NotificationUtil; -import com.crowdin.util.UIUtil; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import lombok.NonNull; +import org.jetbrains.annotations.NotNull; import java.net.URL; import java.util.Objects; +import java.util.Optional; import static com.crowdin.Constants.MESSAGES_BUNDLE; public class DownloadSourceFromContextAction extends BackgroundAction { + @Override - protected void performInBackground(@NonNull AnActionEvent anActionEvent, @NonNull ProgressIndicator indicator) { + protected void performInBackground(@NotNull AnActionEvent anActionEvent, @NotNull ProgressIndicator indicator) { final VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(anActionEvent.getDataContext()); if (file == null) { return; } - Project project = anActionEvent.getProject(); - try { - VirtualFile root = FileUtil.getProjectBaseDir(project); - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); + Project project = anActionEvent.getProject(); + if (project == null) { + return; + } - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.download"), "Download"); - if (!confirmation) { - return; - } - indicator.checkCanceled(); + try { + Optional context = super.prepare( + project, + indicator, + false, + false, + true, + "messages.confirm.download_sources", + "Download" + ); - CrowdinProperties properties; - try { - properties = CrowdinPropertiesLoader.load(project); - } catch (Exception e) { - NotificationUtil.showErrorMessage(project, e.getMessage()); + if (context.isEmpty()) { return; } - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - indicator.checkCanceled(); - - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); - - Branch branch = branchLogic.getBranch(crowdinProjectCache, false); - - Long sourceId = ContextLogic.findSourceIdFromSourceFile(properties, crowdinProjectCache.getFileInfos(branch), file, root); - URL url = crowdin.downloadFile(sourceId); - FileUtil.downloadFile(this, file, url); + performDownload(this, file, context.get()); NotificationUtil.showInformationMessage(project, MESSAGES_BUNDLE.getString("messages.success.download_source")); } catch (ProcessCanceledException e) { throw e; } catch (Exception e) { NotificationUtil.logErrorMessage(project, e); NotificationUtil.showErrorMessage(project, e.getMessage()); + } finally { + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, false)); } } + public static void performDownload(Object requestor, VirtualFile file, ActionContext context) { + Long sourceId = ContextLogic.findSourceIdFromSourceFile(context.properties, context.crowdinProjectCache.getFileInfos(context.branch), file, context.root); + URL url = context.crowdin.downloadFile(sourceId); + FileUtil.downloadFile(requestor, file, url); + } + @Override public void update(AnActionEvent e) { Project project = e.getProject(); @@ -86,11 +75,17 @@ public void update(AnActionEvent e) { final VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(e.getDataContext()); boolean isSourceFile = false; try { - CrowdinProperties properties = CrowdinPropertiesLoader.load(project); - isSourceFile = properties.getFiles() - .stream() - .flatMap(fb -> FileUtil.getSourceFilesRec(FileUtil.getProjectBaseDir(project), fb.getSource()).stream()) - .anyMatch(f -> Objects.equals(file, f)); + Optional context = super.prepare(project, null, false, false, false, null, null); + + if (context.isEmpty()) { + return; + } + + //hide for SB + isSourceFile = !context.get().crowdinProjectCache.isStringsBased() && context.get().properties.getFiles() + .stream() + .flatMap(fb -> FileUtil.getSourceFilesRec(FileUtil.getProjectBaseDir(project), fb.getSource()).stream()) + .anyMatch(f -> Objects.equals(file, f)); } catch (Exception exception) { // do nothing } finally { diff --git a/src/main/java/com/crowdin/action/DownloadSourcesAction.java b/src/main/java/com/crowdin/action/DownloadSourcesAction.java index a22c0cb..f65b7be 100644 --- a/src/main/java/com/crowdin/action/DownloadSourcesAction.java +++ b/src/main/java/com/crowdin/action/DownloadSourcesAction.java @@ -1,110 +1,140 @@ package com.crowdin.action; import com.crowdin.client.Crowdin; -import com.crowdin.client.CrowdinProjectCacheProvider; -import com.crowdin.client.CrowdinProperties; -import com.crowdin.client.CrowdinPropertiesLoader; import com.crowdin.client.FileBean; -import com.crowdin.client.sourcefiles.model.Branch; import com.crowdin.client.sourcefiles.model.FileInfo; -import com.crowdin.logic.BranchLogic; -import com.crowdin.logic.CrowdinSettings; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.panel.download.DownloadWindow; import com.crowdin.util.FileUtil; import com.crowdin.util.NotificationUtil; -import com.crowdin.util.UIUtil; import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import lombok.NonNull; +import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.io.InputStream; import java.net.URL; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import static com.crowdin.Constants.MESSAGES_BUNDLE; +import static com.crowdin.Constants.TOOLWINDOW_ID; public class DownloadSourcesAction extends BackgroundAction { + private boolean enabled = false; + private boolean visible = false; + private String text = ""; + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + @Override + public void update(@NotNull AnActionEvent e) { + if (e.getPlace().equals(TOOLWINDOW_ID)) { + this.enabled = e.getPresentation().isEnabled(); + this.visible = e.getPresentation().isVisible(); + this.text = e.getPresentation().getText(); + } + e.getPresentation().setEnabled(!isInProgress.get() && enabled); + e.getPresentation().setVisible(visible); + e.getPresentation().setText(text); + } + @Override protected String loadingText(AnActionEvent e) { return MESSAGES_BUNDLE.getString("labels.loading_text_download_sources"); } @Override - protected void performInBackground(@NonNull AnActionEvent anActionEvent, @NonNull ProgressIndicator indicator) { + protected void performInBackground(@NotNull AnActionEvent anActionEvent, @NotNull ProgressIndicator indicator) { Project project = anActionEvent.getProject(); - try { - VirtualFile root = FileUtil.getProjectBaseDir(project); - - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); + if (project == null) { + return; + } - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.download"), "Download"); - if (!confirmation) { + isInProgress.set(true); + try { + Optional context = super.prepare( + project, + indicator, + false, + false, + true, + "messages.confirm.download_sources", + "Download" + ); + + if (context.isEmpty()) { return; } - indicator.checkCanceled(); - CrowdinProperties properties; - try { - properties = CrowdinPropertiesLoader.load(project); - } catch (Exception e) { - NotificationUtil.showErrorMessage(project, e.getMessage()); + List selectedFiles = Optional + .ofNullable(project.getService(ProjectService.class).getDownloadWindow()) + .map(DownloadWindow::getSelectedFiles) + .orElse(Collections.emptyList()) + .stream() + .map(str -> Paths.get(context.get().root.getPath(), str)) + .toList(); + + if (!selectedFiles.isEmpty()) { + for (Path file : selectedFiles) { + try { + VirtualFile virtualFile = FileUtil.findVFileByPath(file); + DownloadSourceFromContextAction.performDownload(this, virtualFile, context.get()); + NotificationUtil.logDebugMessage(project, String.format(MESSAGES_BUNDLE.getString("messages.debug.download_sources.file_downloaded"), file)); + } catch (Exception e) { + NotificationUtil.logErrorMessage(project, e); + NotificationUtil.showWarningMessage(project, e.getMessage()); + } + } return; } - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - indicator.checkCanceled(); - - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); - Branch branch = branchLogic.getBranch(crowdinProjectCache, false); - - Map filePaths = crowdinProjectCache.getFileInfos(branch); + Map filePaths = context.get().crowdinProjectCache.getFileInfos(context.get().branch); AtomicBoolean isAnyFileDownloaded = new AtomicBoolean(false); - for (FileBean fileBean : properties.getFiles()) { - Predicate sourcePredicate = FileUtil.filePathRegex(fileBean.getSource(), properties.isPreserveHierarchy()); - Map localSourceFiles = (properties.isPreserveHierarchy()) - ? Collections.emptyMap() - : FileUtil.getSourceFilesRec(root, fileBean.getSource()).stream() - .collect(Collectors.toMap(VirtualFile::getPath, Function.identity())); + for (FileBean fileBean : context.get().properties.getFiles()) { + Predicate sourcePredicate = FileUtil.filePathRegex(fileBean.getSource(), context.get().properties.isPreserveHierarchy()); + Map localSourceFiles = (context.get().properties.isPreserveHierarchy()) + ? Collections.emptyMap() + : FileUtil.getSourceFilesRec(context.get().root, fileBean.getSource()).stream() + .collect(Collectors.toMap(VirtualFile::getPath, Function.identity())); List foundSources = filePaths.keySet().stream() - .map(FileUtil::unixPath) - .filter(sourcePredicate) - .map(FileUtil::normalizePath) - .sorted() - .collect(Collectors.toList()); + .map(FileUtil::unixPath) + .filter(sourcePredicate) + .map(FileUtil::normalizePath) + .sorted() + .toList(); if (foundSources.isEmpty()) { NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.no_sources_for_pattern"), fileBean.getSource())); return; } + for (String foundSourceFilePath : foundSources) { - if (properties.isPreserveHierarchy()) { + if (context.get().properties.isPreserveHierarchy()) { Long fileId = filePaths.get(foundSourceFilePath).getId(); - this.downloadFile(crowdin, fileId, root, foundSourceFilePath); + this.downloadFile(context.get().crowdin, fileId, context.get().root, foundSourceFilePath); isAnyFileDownloaded.set(true); } else { List fittingSources = localSourceFiles.keySet().stream() - .filter(localSourceFilePath -> localSourceFilePath.endsWith(foundSourceFilePath)) - .collect(Collectors.toList()); + .filter(localSourceFilePath -> localSourceFilePath.endsWith(foundSourceFilePath)) + .toList(); + if (fittingSources.isEmpty()) { NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.file_no_representative"), foundSourceFilePath)); continue; @@ -112,9 +142,10 @@ protected void performInBackground(@NonNull AnActionEvent anActionEvent, @NonNul NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.file_not_one_representative"), foundSourceFilePath)); continue; } + Long fileId = filePaths.get(foundSourceFilePath).getId(); VirtualFile file = localSourceFiles.get(fittingSources.get(0)); - this.downloadFile(crowdin, fileId, file); + this.downloadFile(context.get().crowdin, fileId, file); isAnyFileDownloaded.set(true); } NotificationUtil.logDebugMessage(project, String.format(MESSAGES_BUNDLE.getString("messages.debug.download_sources.file_downloaded"), foundSourceFilePath)); @@ -132,6 +163,9 @@ protected void performInBackground(@NonNull AnActionEvent anActionEvent, @NonNul } catch (Exception e) { NotificationUtil.logErrorMessage(project, e); NotificationUtil.showErrorMessage(project, e.getMessage()); + } finally { + isInProgress.set(false); + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, false)); } } diff --git a/src/main/java/com/crowdin/action/DownloadTranslationFromContextAction.java b/src/main/java/com/crowdin/action/DownloadTranslationFromContextAction.java index b3fbe9a..e84af3a 100644 --- a/src/main/java/com/crowdin/action/DownloadTranslationFromContextAction.java +++ b/src/main/java/com/crowdin/action/DownloadTranslationFromContextAction.java @@ -1,120 +1,98 @@ package com.crowdin.action; -import com.crowdin.client.Crowdin; -import com.crowdin.client.CrowdinProjectCacheProvider; -import com.crowdin.client.CrowdinProperties; -import com.crowdin.client.CrowdinPropertiesLoader; import com.crowdin.client.RequestBuilder; import com.crowdin.client.languages.model.Language; -import com.crowdin.client.sourcefiles.model.Branch; -import com.crowdin.logic.BranchLogic; import com.crowdin.logic.ContextLogic; -import com.crowdin.logic.CrowdinSettings; -import com.crowdin.util.ActionUtils; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; import com.crowdin.util.FileUtil; import com.crowdin.util.NotificationUtil; -import com.crowdin.util.UIUtil; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import lombok.NonNull; -import org.apache.commons.lang3.tuple.Pair; +import org.jetbrains.annotations.NotNull; import java.net.URL; +import java.util.Map; +import java.util.Optional; import static com.crowdin.Constants.MESSAGES_BUNDLE; public class DownloadTranslationFromContextAction extends BackgroundAction { + @Override - protected void performInBackground(@NonNull AnActionEvent anActionEvent, @NonNull ProgressIndicator indicator) { + protected void performInBackground(@NotNull AnActionEvent anActionEvent, @NotNull ProgressIndicator indicator) { final VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(anActionEvent.getDataContext()); + if (file == null) { return; } - Project project = anActionEvent.getProject(); - try { - VirtualFile root = FileUtil.getProjectBaseDir(project); - - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); - - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.download"), "Download"); - if (!confirmation) { - return; - } - indicator.checkCanceled(); - CrowdinProperties properties; - try { - properties = CrowdinPropertiesLoader.load(project); - } catch (Exception e) { - NotificationUtil.showErrorMessage(project, e.getMessage()); - return; - } - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); + Project project = anActionEvent.getProject(); - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - indicator.checkCanceled(); + if (project == null) { + return; + } - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); + try { - if (!crowdinProjectCache.isManagerAccess()) { - NotificationUtil.showErrorMessage(project, "You need to have manager access to perform this action"); + Optional context = super.prepare( + project, + indicator, + true, + false, + true, + "messages.confirm.download", + "Download" + ); + + if (context.isEmpty()) { return; } - Branch branch = branchLogic.getBranch(crowdinProjectCache, false); - - Pair source = ContextLogic.findSourceFileFromTranslationFile(file, properties, root, crowdinProjectCache) - .orElseThrow(() -> new RuntimeException(MESSAGES_BUNDLE.getString("errors.file_no_representative_context"))); - - Long sourceId = ContextLogic.findSourceIdFromSourceFile(properties, crowdinProjectCache.getFileInfos(branch), source.getLeft(), root); - - URL url = crowdin.downloadFileTranslation(sourceId, RequestBuilder.buildProjectFileTranslation(source.getRight().getId())); - FileUtil.downloadFile(this, file, url); + performDownload(this, context.get(), file); } catch (ProcessCanceledException e) { throw e; } catch (Exception e) { NotificationUtil.logErrorMessage(project, e); NotificationUtil.showErrorMessage(project, e.getMessage()); + } finally { + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, false)); } } + public static void performDownload(Object requestor, ActionContext context, VirtualFile file) { + Map.Entry source = ContextLogic.findSourceFileFromTranslationFile(file, context.properties, context.root, context.crowdinProjectCache) + .orElseThrow(() -> new RuntimeException(MESSAGES_BUNDLE.getString("errors.file_no_representative_context"))); + + Long sourceId = ContextLogic.findSourceIdFromSourceFile(context.properties, context.crowdinProjectCache.getFileInfos(context.branch), source.getKey(), context.root); + + URL url = context.crowdin.downloadFileTranslation(sourceId, RequestBuilder.buildProjectFileTranslation(source.getValue().getId())); + FileUtil.downloadFile(requestor, file, url); + } + @Override public void update(AnActionEvent e) { Project project = e.getProject(); final VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(e.getDataContext()); boolean isTranslationFile = false; + try { if (file == null) { return; } - CrowdinProperties properties; - try { - properties = CrowdinPropertiesLoader.load(project); - } catch (Exception exception) { - return; - } - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - VirtualFile root = FileUtil.getProjectBaseDir(project); - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); + Optional context = super.prepare(project, null, false, false, false, null, null); - String branchName = ActionUtils.getBranchName(project, properties, false); - - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, false); + if (context.isEmpty()) { + return; + } - isTranslationFile = ContextLogic.findSourceFileFromTranslationFile(file, properties, root, crowdinProjectCache).isPresent(); + //hide for SB + isTranslationFile = !context.get().crowdinProjectCache.isStringsBased() && ContextLogic.findSourceFileFromTranslationFile(file, context.get().properties, context.get().root, context.get().crowdinProjectCache).isPresent(); } catch (Exception exception) { // do nothing } finally { diff --git a/src/main/java/com/crowdin/action/UploadAction.java b/src/main/java/com/crowdin/action/UploadAction.java index 2c9e7b4..249979a 100644 --- a/src/main/java/com/crowdin/action/UploadAction.java +++ b/src/main/java/com/crowdin/action/UploadAction.java @@ -1,22 +1,28 @@ package com.crowdin.action; -import com.crowdin.client.*; -import com.crowdin.client.sourcefiles.model.Branch; -import com.crowdin.logic.BranchLogic; -import com.crowdin.logic.CrowdinSettings; +import com.crowdin.client.FileBean; import com.crowdin.logic.SourceLogic; -import com.crowdin.util.*; +import com.crowdin.service.CrowdinProjectCacheProvider; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.panel.upload.UploadWindow; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; import org.jetbrains.annotations.NotNull; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.function.Function; +import java.util.Optional; import java.util.stream.Collectors; import static com.crowdin.Constants.MESSAGES_BUNDLE; @@ -29,47 +35,60 @@ public class UploadAction extends BackgroundAction { @Override public void performInBackground(@NotNull final AnActionEvent anActionEvent, ProgressIndicator indicator) { Project project = anActionEvent.getProject(); + if (project == null) { + return; + } + try { - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); + Optional context = this.prepare( + project, + indicator, + false, + true, + true, + "messages.confirm.upload_sources", + "Upload" + ); - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.upload_sources"), "Upload"); - if (!confirmation) { + if (context.isEmpty()) { return; } - indicator.checkCanceled(); - - VirtualFile root = FileUtil.getProjectBaseDir(project); - CrowdinProperties properties = CrowdinPropertiesLoader.load(project); - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); + List selectedFiles = Optional + .ofNullable(project.getService(ProjectService.class).getUploadWindow()) + .map(UploadWindow::getSelectedFiles) + .orElse(Collections.emptyList()) + .stream() + .map(str -> Paths.get(context.get().root.getPath(), str)) + .toList(); NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.upload_sources.list_of_patterns") - + properties.getFiles().stream() - .map(fileBean -> String.format(MESSAGES_BUNDLE.getString("messages.debug.upload_sources.list_of_patterns_item"), fileBean.getSource(), fileBean.getTranslation())) - .collect(Collectors.joining())); - - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); - indicator.checkCanceled(); + + context.get().properties.getFiles().stream() + .map(fileBean -> String.format(MESSAGES_BUNDLE.getString("messages.debug.upload_sources.list_of_patterns_item"), fileBean.getSource(), fileBean.getTranslation())) + .collect(Collectors.joining())); - Branch branch = branchLogic.getBranch(crowdinProjectCache, true); - indicator.checkCanceled(); + Map> sources = context.get().properties.getFiles() + .stream() + .map(fileBean -> new AbstractMap.SimpleEntry<>( + fileBean, + FileUtil.getSourceFilesRec(context.get().root, fileBean.getSource()) + .stream() + .filter(f -> selectedFiles.isEmpty() || selectedFiles.contains(Paths.get(f.getPath()))) + .toList() + ) + ) + .filter(e -> !e.getValue().isEmpty()) + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue())); + SourceLogic.processSources(project, context.get().root, context.get().crowdin, context.get().crowdinProjectCache, context.get().branch, context.get().properties.isPreserveHierarchy(), sources); - Map> sources = properties.getFiles().stream() - .collect(Collectors.toMap(Function.identity(), fileBean -> FileUtil.getSourceFilesRec(root, fileBean.getSource()))); - SourceLogic.processSources(project, root, crowdin, crowdinProjectCache, branch, properties.isPreserveHierarchy(), sources); - CrowdinProjectCacheProvider.outdateBranch(branchName); + project.getService(CrowdinProjectCacheProvider.class).outdateBranch(context.get().branchName); } catch (ProcessCanceledException e) { throw e; } catch (Exception e) { NotificationUtil.logErrorMessage(project, e); NotificationUtil.showErrorMessage(project, e.getMessage()); + } finally { + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, true)); } } diff --git a/src/main/java/com/crowdin/action/UploadFromContextAction.java b/src/main/java/com/crowdin/action/UploadFromContextAction.java index 1f39c85..d23f49e 100644 --- a/src/main/java/com/crowdin/action/UploadFromContextAction.java +++ b/src/main/java/com/crowdin/action/UploadFromContextAction.java @@ -1,23 +1,27 @@ package com.crowdin.action; -import com.crowdin.client.*; -import com.crowdin.client.sourcefiles.model.Branch; -import com.crowdin.logic.BranchLogic; -import com.crowdin.logic.CrowdinSettings; +import com.crowdin.client.CrowdinProperties; +import com.crowdin.client.CrowdinPropertiesLoader; +import com.crowdin.client.FileBean; import com.crowdin.logic.SourceLogic; -import com.crowdin.util.*; +import com.crowdin.service.CrowdinProjectCacheProvider; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import static com.crowdin.Constants.MESSAGES_BUNDLE; @@ -26,52 +30,47 @@ */ public class UploadFromContextAction extends BackgroundAction { @Override - public void performInBackground(AnActionEvent anActionEvent, ProgressIndicator indicator) { + public void performInBackground(AnActionEvent anActionEvent, @NotNull ProgressIndicator indicator) { final VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(anActionEvent.getDataContext()); Project project = anActionEvent.getProject(); - try { - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); + if (project == null) { + return; + } - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.upload_source_file"), "Upload"); - if (!confirmation) { + try { + Optional context = this.prepare( + project, + indicator, + false, + true, + true, + "messages.confirm.upload_source_file", + "Upload" + ); + + if (context.isEmpty()) { return; } - indicator.checkCanceled(); - - VirtualFile root = FileUtil.getProjectBaseDir(project); - - CrowdinProperties properties = CrowdinPropertiesLoader.load(project); - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - indicator.checkCanceled(); - - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); - - Branch branch = branchLogic.getBranch(crowdinProjectCache, true); - indicator.checkCanceled(); - - FileBean foundFileBean = properties.getFiles() - .stream() - .filter(fb -> FileUtil.getSourceFilesRec(root, fb.getSource()).contains(file)) - .findAny() - .orElseThrow(() -> new RuntimeException("Unexpected error: couldn't find suitable source pattern")); + FileBean foundFileBean = context.get().properties.getFiles() + .stream() + .filter(fb -> FileUtil.getSourceFilesRec(context.get().root, fb.getSource()).contains(file)) + .findAny() + .orElseThrow(() -> new RuntimeException("Unexpected error: couldn't find suitable source pattern")); indicator.checkCanceled(); Map> source = Collections.singletonMap(foundFileBean, Collections.singletonList(file)); - SourceLogic.processSources(project, root, crowdin, crowdinProjectCache, branch, properties.isPreserveHierarchy(), source); + SourceLogic.processSources(project, context.get().root, context.get().crowdin, context.get().crowdinProjectCache, context.get().branch, context.get().properties.isPreserveHierarchy(), source); - CrowdinProjectCacheProvider.outdateBranch(branchName); + project.getService(CrowdinProjectCacheProvider.class).outdateBranch(context.get().branchName); } catch (ProcessCanceledException e) { throw e; } catch (Exception e) { NotificationUtil.logErrorMessage(project, e); NotificationUtil.showErrorMessage(project, e.getMessage()); + } finally { + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, true)); } } @@ -84,9 +83,9 @@ public void update(AnActionEvent e) { CrowdinProperties properties; properties = CrowdinPropertiesLoader.load(project); isSourceFile = properties.getFiles() - .stream() - .flatMap(fb -> FileUtil.getSourceFilesRec(FileUtil.getProjectBaseDir(project), fb.getSource()).stream()) - .anyMatch(f -> Objects.equals(file, f)); + .stream() + .flatMap(fb -> FileUtil.getSourceFilesRec(FileUtil.getProjectBaseDir(project), fb.getSource()).stream()) + .anyMatch(f -> Objects.equals(file, f)); } catch (Exception exception) { // do nothing } finally { diff --git a/src/main/java/com/crowdin/action/UploadTranslationsAction.java b/src/main/java/com/crowdin/action/UploadTranslationsAction.java index 32ddb8f..6eccb64 100644 --- a/src/main/java/com/crowdin/action/UploadTranslationsAction.java +++ b/src/main/java/com/crowdin/action/UploadTranslationsAction.java @@ -1,110 +1,153 @@ package com.crowdin.action; -import com.crowdin.client.*; +import com.crowdin.client.FileBean; +import com.crowdin.client.RequestBuilder; import com.crowdin.client.languages.model.Language; -import com.crowdin.client.sourcefiles.model.Branch; import com.crowdin.client.sourcefiles.model.FileInfo; import com.crowdin.client.translations.model.UploadTranslationsRequest; -import com.crowdin.logic.BranchLogic; -import com.crowdin.logic.CrowdinSettings; -import com.crowdin.util.*; +import com.crowdin.client.translations.model.UploadTranslationsStringsRequest; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.panel.upload.UploadWindow; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; +import com.crowdin.util.PlaceholderUtil; +import com.crowdin.util.StringUtils; import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.NotNull; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.concurrent.atomic.AtomicInteger; import static com.crowdin.Constants.MESSAGES_BUNDLE; +import static com.crowdin.util.Util.isFileFormatNotAllowed; public class UploadTranslationsAction extends BackgroundAction { @Override - public void performInBackground(@NotNull AnActionEvent e, ProgressIndicator indicator) { + public void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { Project project = e.getProject(); - VirtualFile root = FileUtil.getProjectBaseDir(project); + if (project == null) { + return; + } - CrowdinProperties properties; try { - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); - - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.upload_translations"), "Upload"); - if (!confirmation) { + Optional context = this.prepare( + project, + indicator, + true, + true, + true, + "messages.confirm.upload_translations", + "Upload" + ); + + if (context.isEmpty()) { return; } - indicator.checkCanceled(); - - properties = CrowdinPropertiesLoader.load(project); - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - indicator.checkCanceled(); - - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); + List selectedFiles = Optional + .ofNullable(project.getService(ProjectService.class).getUploadWindow()) + .map(UploadWindow::getSelectedFiles) + .orElse(Collections.emptyList()) + .stream() + .map(str -> Paths.get(context.get().root.getPath(), str)) + .toList(); - if (!crowdinProjectCache.isManagerAccess()) { - NotificationUtil.showErrorMessage(project, "You need to have manager access to perform this action"); + if (context.get().crowdinProjectCache.isStringsBased() && context.get().branch == null) { + NotificationUtil.showErrorMessage(project, "Branch is missing"); return; } - Branch branch = branchLogic.getBranch(crowdinProjectCache, false); + Map filePaths = context.get().crowdinProjectCache.isStringsBased() + ? Collections.emptyMap() + : context.get().crowdinProjectCache.getFileInfos(context.get().branch); - Map filePaths = crowdinProjectCache.getFileInfos(branch); - - NotificationUtil.logDebugMessage(project, "Project files: " + filePaths.keySet()); + if (!context.get().crowdinProjectCache.isStringsBased()) { + NotificationUtil.logDebugMessage(project, "Project files: " + filePaths.keySet()); + } AtomicInteger uploadedFilesCounter = new AtomicInteger(0); - for (FileBean fileBean : properties.getFiles()) { - for (VirtualFile source : FileUtil.getSourceFilesRec(root, fileBean.getSource())) { + for (FileBean fileBean : context.get().properties.getFiles()) { + for (VirtualFile source : FileUtil.getSourceFilesRec(context.get().root, fileBean.getSource())) { VirtualFile pathToPattern = FileUtil.getBaseDir(source, fileBean.getSource()); - String sourceRelativePath = properties.isPreserveHierarchy() ? StringUtils.removeStart(source.getPath(), root.getPath()) : FileUtil.sepAtStart(source.getName()); + String sourceRelativePath = context.get().properties.isPreserveHierarchy() ? StringUtils.removeStart(source.getPath(), context.get().root.getPath()) : FileUtil.sepAtStart(source.getName()); Map translationPaths = - PlaceholderUtil.buildTranslationPatterns(sourceRelativePath, fileBean.getTranslation(), crowdinProjectCache.getProjectLanguages(), crowdinProjectCache.getLanguageMapping()); + PlaceholderUtil.buildTranslationPatterns(sourceRelativePath, fileBean.getTranslation(), context.get().crowdinProjectCache.getProjectLanguages(), context.get().crowdinProjectCache.getLanguageMapping()); FileInfo crowdinSource = filePaths.get(FileUtil.normalizePath(sourceRelativePath)); - if (crowdinSource == null) { - NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.missing_source"), FileUtil.normalizePath((branchName != null ? branchName + "/" : "") + sourceRelativePath))); - return; + if (!context.get().crowdinProjectCache.isStringsBased() && crowdinSource == null) { + if (selectedFiles.isEmpty()) { + NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.missing_source"), FileUtil.normalizePath((context.get().branchName != null ? context.get().branchName + "/" : "") + sourceRelativePath))); + return; + } else { + continue; + } } for (Map.Entry translationPath : translationPaths.entrySet()) { - java.io.File translationFile = Paths.get(pathToPattern.getPath(), translationPath.getValue()).toFile(); + Path translationFilePath = Paths.get(pathToPattern.getPath(), translationPath.getValue()); + java.io.File translationFile = translationFilePath.toFile(); + + if (!selectedFiles.isEmpty()) { + if (!selectedFiles.contains(translationFilePath)) { + continue; + } + } + if (!translationFile.exists()) { - NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.missing_translation"), FileUtil.noSepAtStart(StringUtils.removeStart(translationFile.getPath(), root.getPath())))); + NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.missing_translation"), FileUtil.noSepAtStart(StringUtils.removeStart(translationFile.getPath(), context.get().root.getPath())))); continue; } + Long storageId; try (InputStream translationFileStrem = new FileInputStream(translationFile)) { - storageId = crowdin.addStorage(translationFile.getName(), translationFileStrem); + storageId = context.get().crowdin.addStorage(translationFile.getName(), translationFileStrem); } catch (IOException exception) { throw new RuntimeException("Unhandled exception with File '" + translationFile + "'", exception); } - UploadTranslationsRequest request = RequestBuilder.uploadTranslation(crowdinSource.getId(), storageId, properties.isImportEqSuggestions(), properties.isAutoApproveImported(), properties.isTranslateHidden()); + try { - crowdin.uploadTranslation(translationPath.getKey().getId(), request); + + if (context.get().crowdinProjectCache.isStringsBased()) { + UploadTranslationsStringsRequest request = RequestBuilder.uploadStringsTranslation(context.get().branch.getId(), storageId, context.get().properties.isImportEqSuggestions(), context.get().properties.isAutoApproveImported(), context.get().properties.isTranslateHidden()); + context.get().crowdin.uploadStringsTranslation(translationPath.getKey().getId(), request); + } else { + UploadTranslationsRequest request = RequestBuilder.uploadTranslation(crowdinSource.getId(), storageId, context.get().properties.isImportEqSuggestions(), context.get().properties.isAutoApproveImported(), context.get().properties.isTranslateHidden()); + context.get().crowdin.uploadTranslation(translationPath.getKey().getId(), request); + } + uploadedFilesCounter.incrementAndGet(); } catch (Exception exception) { - NotificationUtil.logErrorMessage(project, exception); - NotificationUtil.showErrorMessage(project, "Couldn't upload translation file '" + translationFile + "': " + exception.getMessage()); + if (isFileFormatNotAllowed(exception)) { + String message = String.format( + "*.%s files are not allowed to upload in strings-based projects", + source.getExtension() + ); + NotificationUtil.showWarningMessage(project, message); + } else { + NotificationUtil.logErrorMessage(project, exception); + NotificationUtil.showErrorMessage(project, "Couldn't upload translation file '" + translationFile + "': " + exception.getMessage()); + } } } } } + if (uploadedFilesCounter.get() > 0) { NotificationUtil.showInformationMessage(project, String.format(MESSAGES_BUNDLE.getString("messages.success.upload_translations"), uploadedFilesCounter.get())); } else { @@ -115,6 +158,8 @@ public void performInBackground(@NotNull AnActionEvent e, ProgressIndicator indi } catch (Exception exception) { NotificationUtil.logErrorMessage(project, exception); NotificationUtil.showErrorMessage(project, exception.getMessage()); + } finally { + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, true)); } } diff --git a/src/main/java/com/crowdin/action/UploadTranslationsFromContextAction.java b/src/main/java/com/crowdin/action/UploadTranslationsFromContextAction.java index 554c214..bc06f6d 100644 --- a/src/main/java/com/crowdin/action/UploadTranslationsFromContextAction.java +++ b/src/main/java/com/crowdin/action/UploadTranslationsFromContextAction.java @@ -1,107 +1,131 @@ package com.crowdin.action; -import com.crowdin.client.*; +import com.crowdin.client.FileBean; +import com.crowdin.client.RequestBuilder; import com.crowdin.client.languages.model.Language; -import com.crowdin.client.sourcefiles.model.Branch; import com.crowdin.client.sourcefiles.model.File; import com.crowdin.client.translations.model.UploadTranslationsRequest; -import com.crowdin.logic.BranchLogic; +import com.crowdin.client.translations.model.UploadTranslationsStringsRequest; import com.crowdin.logic.ContextLogic; -import com.crowdin.logic.CrowdinSettings; -import com.crowdin.util.*; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; +import com.crowdin.util.PlaceholderUtil; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.CommonDataKeys; -import com.intellij.openapi.components.ServiceManager; +import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.progress.ProcessCanceledException; import com.intellij.openapi.progress.ProgressIndicator; import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.Collections; import java.util.Map; +import java.util.Optional; import static com.crowdin.Constants.MESSAGES_BUNDLE; +import static com.crowdin.util.Util.isFileFormatNotAllowed; /** * Created by ihor on 1/27/17. */ public class UploadTranslationsFromContextAction extends BackgroundAction { - @Override - public void performInBackground(AnActionEvent anActionEvent, ProgressIndicator indicator) { + public void performInBackground(AnActionEvent anActionEvent, @NotNull ProgressIndicator indicator) { final VirtualFile file = CommonDataKeys.VIRTUAL_FILE.getData(anActionEvent.getDataContext()); + if (file == null) { + return; + } + Project project = anActionEvent.getProject(); - try { - CrowdinSettings crowdinSettings = ServiceManager.getService(project, CrowdinSettings.class); + if (project == null) { + return; + } - boolean confirmation = UIUtil.confirmDialog(project, crowdinSettings, MESSAGES_BUNDLE.getString("messages.confirm.upload_translation_file"), "Upload"); - if (!confirmation) { + try { + Optional context = this.prepare( + project, + indicator, + true, + true, + true, + "messages.confirm.upload_translation_file", + "Upload" + ); + + if (context.isEmpty()) { return; } - indicator.checkCanceled(); - - VirtualFile root = FileUtil.getProjectBaseDir(project); - CrowdinProperties properties = CrowdinPropertiesLoader.load(project); - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - - BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); - String branchName = branchLogic.acquireBranchName(true); - indicator.checkCanceled(); - - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); - if (!crowdinProjectCache.isManagerAccess()) { - NotificationUtil.showErrorMessage(project, "You need to have manager access to perform this action"); + if (context.get().crowdinProjectCache.isStringsBased() && context.get().branch == null) { + NotificationUtil.showErrorMessage(project, "Branch is missing"); return; } - Branch branch = branchLogic.getBranch(crowdinProjectCache, true); - - Map filePaths = crowdinProjectCache.getFiles(branch); + Map filePaths = context.get().crowdinProjectCache.isStringsBased() + ? Collections.emptyMap() + : context.get().crowdinProjectCache.getFiles(context.get().branch); indicator.checkCanceled(); - for (FileBean fileBean : properties.getFiles()) { - for (VirtualFile source : FileUtil.getSourceFilesRec(root, fileBean.getSource())) { + for (FileBean fileBean : context.get().properties.getFiles()) { + for (VirtualFile source : FileUtil.getSourceFilesRec(context.get().root, fileBean.getSource())) { VirtualFile pathToPattern = FileUtil.getBaseDir(source, fileBean.getSource()); - String relativePathToPattern = (properties.isPreserveHierarchy()) - ? java.io.File.separator + FileUtil.findRelativePath(root, pathToPattern) - : ""; - String patternPathToFile = (properties.isPreserveHierarchy()) - ? java.io.File.separator + FileUtil.findRelativePath(pathToPattern, source.getParent()) - : ""; + String relativePathToPattern = (context.get().properties.isPreserveHierarchy()) + ? java.io.File.separator + FileUtil.findRelativePath(context.get().root, pathToPattern) + : ""; + String patternPathToFile = (context.get().properties.isPreserveHierarchy()) + ? java.io.File.separator + FileUtil.findRelativePath(pathToPattern, source.getParent()) + : ""; File crowdinSource = filePaths.get(FileUtil.normalizePath(FileUtil.joinPaths(relativePathToPattern, patternPathToFile, source.getName()))); - if (crowdinSource == null) { - NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.missing_source"), (branchName != null ? branchName : "") + FileUtil.sepAtStart(FileUtil.joinPaths(relativePathToPattern, patternPathToFile, source.getName())))); + if (!context.get().crowdinProjectCache.isStringsBased() && crowdinSource == null) { + NotificationUtil.showWarningMessage(project, String.format(MESSAGES_BUNDLE.getString("errors.missing_source"), (context.get().branchName != null ? context.get().branchName : "") + FileUtil.sepAtStart(FileUtil.joinPaths(relativePathToPattern, patternPathToFile, source.getName())))); return; } String basePattern = PlaceholderUtil.replaceFilePlaceholders(fileBean.getTranslation(), FileUtil.joinPaths(relativePathToPattern, patternPathToFile, source.getName())); - for (Language lang : crowdinProjectCache.getProjectLanguages()) { - String builtPattern = PlaceholderUtil.replaceLanguagePlaceholders(basePattern, lang, crowdinProjectCache.getLanguageMapping()); + for (Language lang : context.get().crowdinProjectCache.getProjectLanguages()) { + String builtPattern = PlaceholderUtil.replaceLanguagePlaceholders(basePattern, lang, context.get().crowdinProjectCache.getLanguageMapping()); Path translationFile = Paths.get(pathToPattern.getPath(), builtPattern); int compare = translationFile.compareTo(Paths.get(file.getPath())); if (compare == 0) { Long storageId; - try (InputStream translationFileStrem = new FileInputStream(translationFile.toFile())) { - storageId = crowdin.addStorage(translationFile.toFile().getName(), translationFileStrem); + try (InputStream translationFileStream = new FileInputStream(translationFile.toFile())) { + storageId = context.get().crowdin.addStorage(translationFile.toFile().getName(), translationFileStream); } catch (IOException exception) { throw new RuntimeException("Unhandled exception with File '" + translationFile + "'", exception); } - UploadTranslationsRequest request = RequestBuilder.uploadTranslation(crowdinSource.getId(), storageId, properties.isImportEqSuggestions(), properties.isAutoApproveImported(), properties.isTranslateHidden()); + try { - crowdin.uploadTranslation(lang.getId(), request); + + if (context.get().crowdinProjectCache.isStringsBased()) { + UploadTranslationsStringsRequest request = RequestBuilder.uploadStringsTranslation(context.get().branch.getId(), storageId, context.get().properties.isImportEqSuggestions(), context.get().properties.isAutoApproveImported(), context.get().properties.isTranslateHidden()); + context.get().crowdin.uploadStringsTranslation(lang.getId(), request); + } else { + UploadTranslationsRequest request = RequestBuilder.uploadTranslation(crowdinSource.getId(), storageId, context.get().properties.isImportEqSuggestions(), context.get().properties.isAutoApproveImported(), context.get().properties.isTranslateHidden()); + context.get().crowdin.uploadTranslation(lang.getId(), request); + } + NotificationUtil.showInformationMessage(project, - String.format(MESSAGES_BUNDLE.getString("messages.success.upload_translation"), - FileUtil.noSepAtStart(FileUtil.joinPaths(relativePathToPattern, builtPattern)))); + String.format(MESSAGES_BUNDLE.getString("messages.success.upload_translation"), + FileUtil.noSepAtStart(FileUtil.joinPaths(relativePathToPattern, builtPattern)))); } catch (Exception exception) { - NotificationUtil.showErrorMessage(project, "Couldn't upload translation file '" + translationFile + "': " + exception.getMessage()); + if (isFileFormatNotAllowed(exception)) { + String message = String.format( + "*.%s files are not allowed to upload in strings-based projects", + source.getExtension() + ); + NotificationUtil.showWarningMessage(project, message); + } else { + NotificationUtil.showErrorMessage(project, "Couldn't upload translation file '" + translationFile + "': " + exception.getMessage()); + } } } } @@ -111,6 +135,8 @@ public void performInBackground(AnActionEvent anActionEvent, ProgressIndicator i throw e; } catch (Exception e) { NotificationUtil.showErrorMessage(project, e.getMessage()); + } finally { + ApplicationManager.getApplication().invokeAndWait(() -> CrowdinPanelWindowFactory.reloadPanels(project, true)); } } @@ -123,24 +149,14 @@ public void update(AnActionEvent e) { if (file == null) { return; } - CrowdinProperties properties; - try { - properties = CrowdinPropertiesLoader.load(project); - } catch (Exception exception) { - return; - } - NotificationUtil.setLogDebugLevel(properties.isDebug()); - NotificationUtil.logDebugMessage(project, MESSAGES_BUNDLE.getString("messages.debug.started_action")); - - VirtualFile root = FileUtil.getProjectBaseDir(project); - Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - String branchName = ActionUtils.getBranchName(project, properties, false); + Optional context = super.prepare(project, null, false, false, false, null, null); - CrowdinProjectCacheProvider.CrowdinProjectCache crowdinProjectCache = - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, false); + if (context.isEmpty()) { + return; + } - isTranslationFile = ContextLogic.findSourceFileFromTranslationFile(file, properties, root, crowdinProjectCache).isPresent(); + isTranslationFile = ContextLogic.findSourceFileFromTranslationFile(file, context.get().properties, context.get().root, context.get().crowdinProjectCache).isPresent(); } catch (Exception exception) { // do nothing } finally { diff --git a/src/main/java/com/crowdin/activity/CrowdinStartupActivity.java b/src/main/java/com/crowdin/activity/CrowdinStartupActivity.java index 76cf33b..18313f9 100644 --- a/src/main/java/com/crowdin/activity/CrowdinStartupActivity.java +++ b/src/main/java/com/crowdin/activity/CrowdinStartupActivity.java @@ -1,11 +1,13 @@ package com.crowdin.activity; import com.crowdin.client.Crowdin; -import com.crowdin.client.CrowdinProjectCacheProvider; import com.crowdin.client.CrowdinProperties; import com.crowdin.client.CrowdinPropertiesLoader; import com.crowdin.event.FileChangeListener; -import com.crowdin.util.ActionUtils; +import com.crowdin.logic.BranchLogic; +import com.crowdin.service.CrowdinProjectCacheProvider; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; import com.crowdin.util.NotificationUtil; import com.crowdin.util.PropertyUtil; import com.intellij.openapi.progress.ProgressIndicator; @@ -15,6 +17,8 @@ import com.intellij.openapi.startup.StartupActivity; import org.jetbrains.annotations.NotNull; +import java.util.EnumSet; + public class CrowdinStartupActivity implements StartupActivity { @Override @@ -29,21 +33,31 @@ public void runActivity(@NotNull Project project) { properties = CrowdinPropertiesLoader.load(project); Crowdin crowdin = new Crowdin(properties.getProjectId(), properties.getApiToken(), properties.getBaseUrl()); - String branchName = ActionUtils.getBranchName(project, properties, false); - - ProgressManager.getInstance().run(new Task.Backgroundable(project, "Crowdin") { - @Override - public void run(@NotNull ProgressIndicator indicator) { - try { - indicator.setText("Updating Crowdin cache"); - CrowdinProjectCacheProvider.getInstance(crowdin, branchName, true); - } catch (Exception e) { - NotificationUtil.showErrorMessage(project, e.getMessage()); - } - } - }); + this.reloadPlugin(project, crowdin, properties); } catch (Exception e) { NotificationUtil.showErrorMessage(project, e.getMessage()); } } + + private void reloadPlugin(Project project, Crowdin crowdin, CrowdinProperties properties) { + BranchLogic branchLogic = new BranchLogic(crowdin, project, properties); + String branchName = branchLogic.acquireBranchName(); + + ProgressManager.getInstance().run(new Task.Backgroundable(project, "Crowdin") { + @Override + public void run(@NotNull ProgressIndicator indicator) { + try { + indicator.setText("Updating Crowdin cache"); + project.getService(CrowdinProjectCacheProvider.class).getInstance(crowdin, branchName, true); + ProjectService service = project.getService(ProjectService.class); + EnumSet loadedComponents = service.addAndGetLoadedComponents(ProjectService.InitializationItem.STARTUP_ACTIVITY); + if (loadedComponents.contains(ProjectService.InitializationItem.UI_PANELS)) { + CrowdinPanelWindowFactory.reloadPanels(project, true); + } + } catch (Exception e) { + NotificationUtil.showErrorMessage(project, e.getMessage()); + } + } + }); + } } diff --git a/src/main/java/com/crowdin/client/BranchInfo.java b/src/main/java/com/crowdin/client/BranchInfo.java index 492335d..9850881 100644 --- a/src/main/java/com/crowdin/client/BranchInfo.java +++ b/src/main/java/com/crowdin/client/BranchInfo.java @@ -1,11 +1,20 @@ package com.crowdin.client; -import lombok.Data; - -@Data public class BranchInfo { private final String name; private final String title; + public BranchInfo(String name, String title) { + this.name = name; + this.title = title; + } + + public String getName() { + return name; + } + + public String getTitle() { + return title; + } } diff --git a/src/main/java/com/crowdin/client/Crowdin.java b/src/main/java/com/crowdin/client/Crowdin.java index 5a86319..f213e8b 100644 --- a/src/main/java/com/crowdin/client/Crowdin.java +++ b/src/main/java/com/crowdin/client/Crowdin.java @@ -1,18 +1,24 @@ package com.crowdin.client; +import com.crowdin.client.bundles.model.Bundle; +import com.crowdin.client.bundles.model.BundleExport; import com.crowdin.client.core.http.exceptions.HttpBadRequestException; import com.crowdin.client.core.http.exceptions.HttpException; import com.crowdin.client.core.model.*; import com.crowdin.client.labels.model.AddLabelRequest; import com.crowdin.client.labels.model.Label; import com.crowdin.client.languages.model.Language; +import com.crowdin.client.projectsgroups.model.Project; import com.crowdin.client.sourcefiles.model.*; import com.crowdin.client.sourcestrings.model.SourceString; +import com.crowdin.client.sourcestrings.model.UploadStringsProgress; +import com.crowdin.client.sourcestrings.model.UploadStringsRequest; import com.crowdin.client.translations.model.BuildProjectFileTranslationRequest; import com.crowdin.client.translations.model.BuildProjectTranslationRequest; import com.crowdin.client.translations.model.ProjectBuild; import com.crowdin.client.translations.model.UploadTranslationsRequest; -import com.crowdin.client.translationstatus.model.FileProgress; +import com.crowdin.client.translations.model.UploadTranslationsStringsRequest; +import com.crowdin.client.translationstatus.model.FileBranchProgress; import com.crowdin.client.translationstatus.model.LanguageProgress; import com.crowdin.util.RetryUtil; import com.crowdin.util.Util; @@ -36,69 +42,82 @@ public class Crowdin implements CrowdinClient { private final Long projectId; + private final String baseUrl; private final com.crowdin.client.Client client; public Crowdin(@NotNull Long projectId, @NotNull String apiToken, String baseUrl) { this.projectId = projectId; + this.baseUrl = baseUrl; Credentials credentials = new Credentials(apiToken, null, baseUrl); ClientConfig clientConfig = ClientConfig.builder() - .userAgent(Util.getUserAgent()) - .build(); + .userAgent(Util.getUserAgent()) + .build(); this.client = new Client(credentials, clientConfig); } + @Override + public Long getProjectId() { + return this.projectId; + } + @Override public Long addStorage(String fileName, InputStream content) { return executeRequest(() -> this.client.getStorageApi() - .addStorage(fileName, content) - .getData() - .getId()); + .addStorage(fileName, content) + .getData() + .getId()); } @Override public void updateSource(Long sourceId, UpdateFileRequest request) { executeRequest(() -> this.client.getSourceFilesApi() - .updateOrRestoreFile(this.projectId, sourceId, request)); + .updateOrRestoreFile(this.projectId, sourceId, request)); } @Override public URL downloadFile(Long fileId) { return url(executeRequest(() -> this.client.getSourceFilesApi() - .downloadFile(this.projectId, fileId) - .getData())); + .downloadFile(this.projectId, fileId) + .getData())); } @Override public void addSource(AddFileRequest request) { executeRequest(() -> this.client.getSourceFilesApi() - .addFile(this.projectId, request)); + .addFile(this.projectId, request)); } @Override public void editSource(Long fileId, List request) { executeRequest(() -> this.client.getSourceFilesApi() - .editFile(this.projectId, fileId, request)); + .editFile(this.projectId, fileId, request)); } @Override public void uploadTranslation(String languageId, UploadTranslationsRequest request) { executeRequest(() -> this.client.getTranslationsApi() - .uploadTranslations(this.projectId, languageId, request)); + .uploadTranslations(this.projectId, languageId, request)); + } + + @Override + public void uploadStringsTranslation(String languageId, UploadTranslationsStringsRequest request) { + executeRequest(() -> this.client.getTranslationsApi() + .uploadTranslationStringsBased(this.projectId, languageId, request)); } @Override public Directory addDirectory(AddDirectoryRequest request) { return executeRequest(() -> this.client.getSourceFilesApi() - .addDirectory(this.projectId, request) - .getData()); + .addDirectory(this.projectId, request) + .getData()); } @Override public com.crowdin.client.projectsgroups.model.Project getProject() { return executeRequest(() -> this.client.getProjectsGroupsApi() - .getProject(this.projectId) - .getData()); + .getProject(this.projectId) + .getData()); } @Override @@ -106,67 +125,103 @@ public List extractProjectLanguages(com.crowdin.client.projectsgroups. return crowdinProject.getTargetLanguages(); } + @Override + public UploadStringsProgress uploadStrings(UploadStringsRequest request) { + return executeRequest(() -> this.client.getSourceStringsApi() + .uploadStrings(this.projectId, request) + .getData()); + } + + @Override + public UploadStringsProgress checkUploadStringsStatus(String id) { + return executeRequest(() -> this.client.getSourceStringsApi() + .uploadStringsStatus(projectId, id) + .getData()); + } + @Override public ProjectBuild startBuildingTranslation(BuildProjectTranslationRequest request) { return executeRequest(() -> this.client.getTranslationsApi() - .buildProjectTranslation(this.projectId, request) - .getData()); + .buildProjectTranslation(this.projectId, request) + .getData()); } @Override public ProjectBuild checkBuildingStatus(Long buildId) { return executeRequest(() -> this.client.getTranslationsApi() - .checkBuildStatus(projectId, buildId) - .getData()); + .checkBuildStatus(projectId, buildId) + .getData()); } @Override public URL downloadProjectTranslations(Long buildId) { return url(executeRequest(() -> this.client.getTranslationsApi() - .downloadProjectTranslations(this.projectId, buildId) - .getData())); + .downloadProjectTranslations(this.projectId, buildId) + .getData())); + } + + + @Override + public BundleExport startBuildingBundle(Long bundleId) { + return executeRequest(() -> this.client.getBundlesApi() + .exportBundle(this.projectId, bundleId) + .getData()); + } + + @Override + public BundleExport checkBundleBuildingStatus(Long buildId, String exportId) { + return executeRequest(() -> this.client.getBundlesApi() + .checkBundleExportStatus(projectId, buildId, exportId) + .getData()); + } + + @Override + public URL downloadBundle(Long buildId, String exportId) { + return url(executeRequest(() -> this.client.getBundlesApi() + .downloadBundle(this.projectId, buildId, exportId) + .getData())); } @Override public URL downloadFileTranslation(Long fileId, BuildProjectFileTranslationRequest request) { return url(executeRequest(() -> client.getTranslationsApi() - .buildProjectFileTranslation(this.projectId, fileId, null, request) - .getData())); + .buildProjectFileTranslation(this.projectId, fileId, null, request) + .getData())); } @Override public List getSupportedLanguages() { return executeRequest(() -> client.getLanguagesApi().listSupportedLanguages(500, 0) - .getData() - .stream() - .map(ResponseObject::getData) - .collect(Collectors.toList())); + .getData() + .stream() + .map(ResponseObject::getData) + .collect(Collectors.toList())); } @Override public Map getDirectories(Long branchId) { return executeRequestFullList((limit, offset) -> this.client.getSourceFilesApi() - .listDirectories(this.projectId, branchId, null, null, true, limit, offset) - .getData() - ) - .stream() - .map(ResponseObject::getData) - .filter(dir -> Objects.equals(dir.getBranchId(), branchId)) - .collect(Collectors.toMap(Directory::getId, Function.identity())); + .listDirectories(this.projectId, branchId, null, null, true, limit, offset) + .getData() + ) + .stream() + .map(ResponseObject::getData) + .filter(dir -> Objects.equals(dir.getBranchId(), branchId)) + .collect(Collectors.toMap(Directory::getId, Function.identity())); } @Override public List getFiles(Long branchId) { return executeRequestFullList((limit, offset) -> this.client.getSourceFilesApi() - .listFiles(this.projectId, branchId, null, null, true, 500, 0) - .getData() - ) - .stream() - .map(ResponseObject::getData) - .filter(file -> Objects.equals(file.getBranchId(), branchId)) - .collect(Collectors.toList()); + .listFiles(this.projectId, branchId, null, null, true, 500, 0) + .getData() + ) + .stream() + .map(ResponseObject::getData) + .filter(file -> Objects.equals(file.getBranchId(), branchId)) + .collect(Collectors.toList()); } @Override @@ -183,7 +238,7 @@ public List getStrings() { null, limit, offset).getData() - ) + ) .stream() .map(ResponseObject::getData) .collect(Collectors.toList()); @@ -191,7 +246,7 @@ public List getStrings() { /** * @param request represents function that downloads list of models and has two args (limit, offset) - * @param represents model + * @param represents model * @return list of models accumulated from request function */ private List executeRequestFullList(BiFunction> request) { @@ -210,8 +265,8 @@ private List executeRequestFullList(BiFunction> public Branch addBranch(AddBranchRequest request) { try { return executeRequest(() -> this.client.getSourceFilesApi() - .addBranch(this.projectId, request) - .getData()); + .addBranch(this.projectId, request) + .getData()); } catch (Exception e) { if (e.getMessage() != null && e.getMessage().contains("regexNotMatch File name can't contain")) { throw new RuntimeException(MESSAGES_BUNDLE.getString("errors.branch_contains_forbidden_symbols")); @@ -233,50 +288,70 @@ public Optional getBranch(String name) { @Override public Map getBranches() { return executeRequestFullList((limit, offset) -> - this.client.getSourceFilesApi() - .listBranches(this.projectId, null, limit, offset) - .getData() + this.client.getSourceFilesApi() + .listBranches(this.projectId, null, limit, offset) + .getData() ) - .stream() - .map(ResponseObject::getData) - .collect(Collectors.toMap(Branch::getName, Function.identity())); + .stream() + .map(ResponseObject::getData) + .collect(Collectors.toMap(Branch::getName, Function.identity())); } @Override public List getProjectProgress() { return executeRequestFullList((limit, offset) -> this.client.getTranslationStatusApi() - .getProjectProgress(this.projectId, limit, offset, null) - .getData() - .stream() - .map(ResponseObject::getData) - .collect(Collectors.toList())); + .getProjectProgress(this.projectId, limit, offset, null) + .getData() + .stream() + .map(ResponseObject::getData) + .collect(Collectors.toList())); } @Override - public List getLanguageProgress(String languageId) { + public List getLanguageProgress(String languageId) { return executeRequestFullList((limit, offset) -> this.client.getTranslationStatusApi() - .getLanguageProgress(this.projectId, languageId, limit, offset) - .getData() - .stream() - .map(ResponseObject::getData) - .collect(Collectors.toList())); + .getLanguageProgress(this.projectId, languageId, limit, offset) + .getData() + .stream() + .map(ResponseObject::getData) + .collect(Collectors.toList())); } @Override public List

+ diff --git a/src/main/java/com/crowdin/ui/ConfirmActionPanel.java b/src/main/java/com/crowdin/ui/dialog/ConfirmActionPanel.java similarity index 94% rename from src/main/java/com/crowdin/ui/ConfirmActionPanel.java rename to src/main/java/com/crowdin/ui/dialog/ConfirmActionPanel.java index 78bd2b2..b1f217a 100644 --- a/src/main/java/com/crowdin/ui/ConfirmActionPanel.java +++ b/src/main/java/com/crowdin/ui/dialog/ConfirmActionPanel.java @@ -1,4 +1,4 @@ -package com.crowdin.ui; +package com.crowdin.ui.dialog; import javax.swing.*; diff --git a/src/main/java/com/crowdin/ui/panel/ContentTab.java b/src/main/java/com/crowdin/ui/panel/ContentTab.java new file mode 100644 index 0000000..298fc00 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/ContentTab.java @@ -0,0 +1,7 @@ +package com.crowdin.ui.panel; + +import javax.swing.*; + +public interface ContentTab { + JPanel getContent(); +} diff --git a/src/main/java/com/crowdin/ui/panel/CrowdinPanelWindowFactory.java b/src/main/java/com/crowdin/ui/panel/CrowdinPanelWindowFactory.java new file mode 100644 index 0000000..a9855a5 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/CrowdinPanelWindowFactory.java @@ -0,0 +1,157 @@ +package com.crowdin.ui.panel; + +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.download.DownloadWindow; +import com.crowdin.ui.panel.progress.TranslationProgressWindow; +import com.crowdin.ui.panel.upload.UploadWindow; +import com.intellij.ide.ActivityTracker; +import com.intellij.openapi.actionSystem.ActionGroup; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.ActionPlaces; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl; +import com.intellij.openapi.project.DumbAware; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.ui.SimpleToolWindowPanel; +import com.intellij.openapi.wm.ToolWindow; +import com.intellij.openapi.wm.ToolWindowFactory; +import com.intellij.openapi.wm.ToolWindowManager; +import com.intellij.ui.content.Content; +import com.intellij.ui.content.ContentFactory; +import com.intellij.ui.content.ContentManager; +import org.jetbrains.annotations.NotNull; + +import java.util.EnumSet; +import java.util.Optional; +import java.util.function.Supplier; + +import static com.crowdin.Constants.DOWNLOAD_REFRESH_ACTION; +import static com.crowdin.Constants.DOWNLOAD_TOOLBAR_ID; +import static com.crowdin.Constants.PROGRESS_REFRESH_ACTION; +import static com.crowdin.Constants.PROGRESS_TOOLBAR_ID; +import static com.crowdin.Constants.TOOLWINDOW_ID; +import static com.crowdin.Constants.UPLOAD_REFRESH_ACTION; +import static com.crowdin.Constants.UPLOAD_TOOLBAR_ID; + +public class CrowdinPanelWindowFactory implements ToolWindowFactory, DumbAware { + + public static final String PLACE_ID = CrowdinPanelWindowFactory.class.getName(); + + @Override + public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) { + ContentFactory contentFactory = ContentFactory.getInstance(); + ContentManager contentManager = toolWindow.getContentManager(); + ActionManager actionManager = ActionManager.getInstance(); + ProjectService projectService = project.getService(ProjectService.class); + + Content progressPanel = this.setupPanel( + () -> { + TranslationProgressWindow translationProgressWindow = new TranslationProgressWindow(); + projectService.setTranslationProgressWindow(translationProgressWindow); + return translationProgressWindow; + }, + (ActionGroup) actionManager.getAction(PROGRESS_TOOLBAR_ID), + contentFactory, + "Progress" + ); + + Content uploadPanel = this.setupPanel( + () -> { + UploadWindow uploadWindow = new UploadWindow(); + projectService.setUploadWindow(uploadWindow); + return uploadWindow; + }, + (ActionGroup) actionManager.getAction(UPLOAD_TOOLBAR_ID), + contentFactory, + "Upload" + ); + + Content downloadPanel = this.setupPanel( + () -> { + DownloadWindow downloadWindow = new DownloadWindow(); + projectService.setDownloadWindow(downloadWindow); + return downloadWindow; + }, + (ActionGroup) actionManager.getAction(DOWNLOAD_TOOLBAR_ID), + contentFactory, + "Download" + ); + + contentManager.addContent(uploadPanel, 0); + contentManager.addContent(downloadPanel, 1); + contentManager.addContent(progressPanel, 2); + + EnumSet loadedComponents = projectService.addAndGetLoadedComponents(ProjectService.InitializationItem.UI_PANELS); + if (loadedComponents.contains(ProjectService.InitializationItem.STARTUP_ACTIVITY)) { + reloadPanels(project, true); + } + } + + private Content setupPanel( + Supplier tabSupplier, + ActionGroup group, + ContentFactory contentFactory, + String name + ) { + SimpleToolWindowPanel panel = new SimpleToolWindowPanel(true, true); + ActionToolbarImpl actionToolbar = new ActionToolbarImpl(ActionPlaces.TOOLBAR, group, true); + + actionToolbar.setTargetComponent(panel); + + panel.setToolbar(actionToolbar); + panel.setContent(tabSupplier.get().getContent()); + + return contentFactory.createContent(panel, name, false); + } + + public static void reloadPanels(Project project, boolean fullReload) { + Optional + .ofNullable(ToolWindowManager.getInstance(project)) + .map(toolWindowManager -> toolWindowManager.getToolWindow(TOOLWINDOW_ID)) + .map(ToolWindow::getContentManager) + .ifPresent(manager -> { + ActionManager actionManager = ActionManager.getInstance(); + ProjectService projectService = project.getService(ProjectService.class); + if (fullReload) { + runRefresh(project, actionManager, PROGRESS_REFRESH_ACTION, () -> projectService.getTranslationProgressWindow().setPlug("Loading...")); + } + runRefresh(project, actionManager, UPLOAD_REFRESH_ACTION); + runRefresh(project, actionManager, DOWNLOAD_REFRESH_ACTION); + }); + } + + public static void updateToolbar(String actionId, String text, boolean visible, boolean enabled) { + AnAction action = ActionManager.getInstance().getAction(actionId); + Presentation presentation = new Presentation(); + presentation.setVisible(visible); + presentation.setEnabled(enabled); + presentation.setText(text); + action.update(AnActionEvent.createFromDataContext(TOOLWINDOW_ID, presentation, DataContext.EMPTY_CONTEXT)); + ActivityTracker.getInstance().inc(); + } + + private static void runRefresh(Project project, ActionManager actionManager, String action) { + runRefresh(project, actionManager, action, null); + } + + private static void runRefresh(Project project, ActionManager actionManager, String action, Runnable onRefresh) { + AnAction refreshAction = actionManager.getAction(action); + DataContext context = dataId -> project; + AnActionEvent anActionEvent = new AnActionEvent( + null, + context, + PLACE_ID, + new Presentation(), + actionManager, + 0 + ); + refreshAction.actionPerformed(anActionEvent); + if (onRefresh != null) { + onRefresh.run(); + } + } + +} diff --git a/src/main/java/com/crowdin/ui/panel/download/DownloadWindow.form b/src/main/java/com/crowdin/ui/panel/download/DownloadWindow.form new file mode 100644 index 0000000..9aafffe --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/download/DownloadWindow.form @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/crowdin/ui/panel/download/DownloadWindow.java b/src/main/java/com/crowdin/ui/panel/download/DownloadWindow.java new file mode 100644 index 0000000..3229ef2 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/download/DownloadWindow.java @@ -0,0 +1,136 @@ +package com.crowdin.ui.panel.download; + +import com.crowdin.client.bundles.model.Bundle; +import com.crowdin.ui.panel.ContentTab; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.tree.CellData; +import com.crowdin.ui.tree.CellRenderer; +import com.crowdin.ui.tree.FileTree; +import com.intellij.ide.ActivityTracker; +import com.intellij.ide.BrowserUtil; +import com.intellij.openapi.actionSystem.ActionManager; +import com.intellij.openapi.actionSystem.AnAction; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.actionSystem.DataContext; +import com.intellij.openapi.actionSystem.Presentation; +import com.intellij.ui.JBColor; +import com.intellij.ui.treeStructure.Tree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.List; +import java.util.Optional; + +import static com.crowdin.Constants.DOWNLOAD_SOURCES_ACTION; +import static com.crowdin.Constants.DOWNLOAD_TRANSLATIONS_ACTION; + +public class DownloadWindow implements ContentTab { + + private JPanel panel1; + private Tree tree1; + private JScrollPane scrollPane; + private boolean isBundlesMode = false; + private DefaultMutableTreeNode selectedElement; + + public DownloadWindow() { + scrollPane.getViewport().setBackground(JBColor.WHITE); + tree1.setCellRenderer(new CellRenderer()); + tree1.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + this.setPlug("Refresh tree"); + + tree1.addMouseListener(new MouseAdapter() { + @Override + public void mousePressed(MouseEvent e) { + int selRow = tree1.getRowForLocation(e.getX(), e.getY()); + TreePath selPath = tree1.getPathForLocation(e.getX(), e.getY()); + if (selRow != -1) { + if (e.getClickCount() == 2) { + Optional + .ofNullable(selPath.getLastPathComponent()) + .filter(DefaultMutableTreeNode.class::isInstance) + .map(CellRenderer::getData) + .filter(CellData::isLink) + .ifPresent(cell -> BrowserUtil.browse(cell.getLink())); + } + } + } + }); + + tree1.addTreeSelectionListener(e -> { + Optional selectedNode = Optional.ofNullable(e.getNewLeadSelectionPath()) + .map(TreePath::getLastPathComponent) + .map(DefaultMutableTreeNode.class::cast); + + if (selectedNode.isEmpty()) { + return; + } + + this.selectedElement = selectedNode.get(); + + if (!this.isBundlesMode) { + return; + } + + CellData cell = CellRenderer.getData(this.selectedElement); + + if (cell.isBundle()) { + CrowdinPanelWindowFactory.updateToolbar(DOWNLOAD_TRANSLATIONS_ACTION, "Download bundle", true, true); + } else { + CrowdinPanelWindowFactory.updateToolbar(DOWNLOAD_TRANSLATIONS_ACTION, "Select bundle to download", true, false); + } + }); + } + + public void setPlug(String text) { + tree1.setModel(new DefaultTreeModel(new DefaultMutableTreeNode(CellData.root(text)))); + } + + @Override + public JPanel getContent() { + return panel1; + } + + public Bundle getSelectedBundle() { + return CellRenderer.getData(this.selectedElement).getBundle(); + } + + public List getSelectedFiles() { + return FileTree.getFiles(this.selectedElement); + } + + public void rebuildFileTree(String projectName, List files) { + isBundlesMode = false; + this.selectedElement = null; + CrowdinPanelWindowFactory.updateToolbar(DOWNLOAD_SOURCES_ACTION, "Download Sources", true, true); + CrowdinPanelWindowFactory.updateToolbar(DOWNLOAD_TRANSLATIONS_ACTION, "Download Translations", true, true); + tree1.setModel(new DefaultTreeModel(FileTree.buildTree(projectName, files))); + FileTree.expandAll(tree1); + } + + public void rebuildBundlesTree(String projectName, List bundles, String bundleInfoUrl) { + isBundlesMode = true; + this.selectedElement = null; + CrowdinPanelWindowFactory.updateToolbar(DOWNLOAD_SOURCES_ACTION, "", false, false); + CrowdinPanelWindowFactory.updateToolbar(DOWNLOAD_TRANSLATIONS_ACTION, "Select bundle to download", true, false); + DefaultMutableTreeNode root = new DefaultMutableTreeNode(CellData.root(projectName)); + bundles.forEach(bundle -> root.add(new DefaultMutableTreeNode(CellData.bundle(bundle)))); + if (bundles.isEmpty()) { + root.add(new DefaultMutableTreeNode(CellData.link("Check how to create bundle", bundleInfoUrl))); + } + tree1.setModel(new DefaultTreeModel(root)); + expandAll(); + } + + public void expandAll() { + FileTree.expandAll(tree1); + } + + public void collapseAll() { + FileTree.collapseAll(tree1); + } +} diff --git a/src/main/java/com/crowdin/ui/panel/download/action/CollapseAction.java b/src/main/java/com/crowdin/ui/panel/download/action/CollapseAction.java new file mode 100644 index 0000000..6da23ca --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/download/action/CollapseAction.java @@ -0,0 +1,64 @@ +package com.crowdin.ui.panel.download.action; + +import com.crowdin.action.BackgroundAction; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.download.DownloadWindow; +import com.crowdin.util.NotificationUtil; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class CollapseAction extends BackgroundAction { + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + public CollapseAction() { + super("Collapse tree", "Collapse tree", AllIcons.Actions.Collapseall); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(!isInProgress.get()); + } + + @Override + protected void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { + Project project = e.getProject(); + if (project == null) { + return; + } + + e.getPresentation().setEnabled(false); + isInProgress.set(true); + try { + DownloadWindow window = project.getService(ProjectService.class).getDownloadWindow(); + + if (window == null) { + return; + } + + ApplicationManager.getApplication().invokeAndWait(window::collapseAll); + } catch (ProcessCanceledException ex) { + throw ex; + } catch (Exception ex) { + NotificationUtil.logErrorMessage(project, ex); + NotificationUtil.showErrorMessage(project, ex.getMessage()); + } finally { + e.getPresentation().setEnabled(true); + isInProgress.set(false); + } + } + + @Override + protected String loadingText(AnActionEvent e) { + return "Collapse download tree"; + } + +} diff --git a/src/main/java/com/crowdin/ui/panel/download/action/ExpandAction.java b/src/main/java/com/crowdin/ui/panel/download/action/ExpandAction.java new file mode 100644 index 0000000..abb1bb7 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/download/action/ExpandAction.java @@ -0,0 +1,64 @@ +package com.crowdin.ui.panel.download.action; + +import com.crowdin.action.BackgroundAction; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.download.DownloadWindow; +import com.crowdin.util.NotificationUtil; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class ExpandAction extends BackgroundAction { + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + public ExpandAction() { + super("Expand tree", "Expand tree", AllIcons.Actions.Expandall); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(!isInProgress.get()); + } + + @Override + protected void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { + Project project = e.getProject(); + if (project == null) { + return; + } + + e.getPresentation().setEnabled(false); + isInProgress.set(true); + try { + DownloadWindow window = project.getService(ProjectService.class).getDownloadWindow(); + + if (window == null) { + return; + } + + ApplicationManager.getApplication().invokeAndWait(window::expandAll); + } catch (ProcessCanceledException ex) { + throw ex; + } catch (Exception ex) { + NotificationUtil.logErrorMessage(project, ex); + NotificationUtil.showErrorMessage(project, ex.getMessage()); + } finally { + e.getPresentation().setEnabled(true); + isInProgress.set(false); + } + } + + @Override + protected String loadingText(AnActionEvent e) { + return "Expand download tree"; + } + +} diff --git a/src/main/java/com/crowdin/ui/panel/download/action/RefreshAction.java b/src/main/java/com/crowdin/ui/panel/download/action/RefreshAction.java new file mode 100644 index 0000000..61cb663 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/download/action/RefreshAction.java @@ -0,0 +1,113 @@ +package com.crowdin.ui.panel.download.action; + +import com.crowdin.action.ActionContext; +import com.crowdin.action.BackgroundAction; +import com.crowdin.client.FileBean; +import com.crowdin.client.languages.model.Language; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.panel.download.DownloadWindow; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; +import com.crowdin.util.PlaceholderUtil; +import com.crowdin.util.StringUtils; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class RefreshAction extends BackgroundAction { + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + public RefreshAction() { + super("Refresh data", "Refresh data", AllIcons.Actions.Refresh); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(!isInProgress.get()); + } + + @Override + protected void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { + boolean forceRefresh = !CrowdinPanelWindowFactory.PLACE_ID.equals(e.getPlace()); + Project project = e.getProject(); + if (project == null) { + return; + } + + e.getPresentation().setEnabled(false); + isInProgress.set(true); + try { + DownloadWindow window = project.getService(ProjectService.class).getDownloadWindow(); + if (window == null) { + return; + } + + Optional context = super.prepare(project, indicator, false, false, forceRefresh, null, null); + + if (context.isEmpty()) { + return; + } + + if (context.get().crowdinProjectCache.isStringsBased()) { + String url = context.get().crowdin.getBundlesUrl(context.get().crowdinProjectCache.getProject()); + ApplicationManager.getApplication() + .invokeAndWait(() -> window.rebuildBundlesTree(context.get().crowdinProjectCache.getProject().getName(), context.get().crowdinProjectCache.getBundles(), url)); + return; + } + + List files = new ArrayList<>(); + + for (FileBean fileBean : context.get().properties.getFiles()) { + for (VirtualFile source : FileUtil.getSourceFilesRec(context.get().root, fileBean.getSource())) { + VirtualFile pathToPattern = FileUtil.getBaseDir(source, fileBean.getSource()); + String sourceRelativePath = context.get().properties.isPreserveHierarchy() ? StringUtils.removeStart(source.getPath(), context.get().root.getPath()) : FileUtil.sepAtStart(source.getName()); + + Map translationPaths = + PlaceholderUtil.buildTranslationPatterns(sourceRelativePath, fileBean.getTranslation(), context.get().crowdinProjectCache.getProjectLanguages(), context.get().crowdinProjectCache.getLanguageMapping()); + + for (Map.Entry translationPath : translationPaths.entrySet()) { + java.io.File translationFile = Paths.get(pathToPattern.getPath(), translationPath.getValue()).toFile(); + if (translationFile.exists()) { + String file = Paths.get(context.get().root.getPath()).relativize(Paths.get(translationFile.getPath())).toString(); + files.add(file); + } + } + } + } + + ApplicationManager.getApplication() + .invokeAndWait(() -> window.rebuildFileTree(context.get().crowdinProjectCache.getProject().getName(), files)); + } catch (ProcessCanceledException ex) { + throw ex; + } catch (Exception ex) { + if (forceRefresh) { + NotificationUtil.logErrorMessage(project, ex); + NotificationUtil.showErrorMessage(project, ex.getMessage()); + } + } finally { + e.getPresentation().setEnabled(true); + isInProgress.set(false); + } + } + + @Override + protected String loadingText(AnActionEvent e) { + return "Refresh download panel"; + } + +} diff --git a/src/main/java/com/crowdin/ui/TranslationProgressWindow.form b/src/main/java/com/crowdin/ui/panel/progress/TranslationProgressWindow.form similarity index 97% rename from src/main/java/com/crowdin/ui/TranslationProgressWindow.form rename to src/main/java/com/crowdin/ui/panel/progress/TranslationProgressWindow.form index 1458adb..dc8615b 100644 --- a/src/main/java/com/crowdin/ui/TranslationProgressWindow.form +++ b/src/main/java/com/crowdin/ui/panel/progress/TranslationProgressWindow.form @@ -1,5 +1,5 @@ -
+ diff --git a/src/main/java/com/crowdin/ui/TranslationProgressWindow.java b/src/main/java/com/crowdin/ui/panel/progress/TranslationProgressWindow.java similarity index 88% rename from src/main/java/com/crowdin/ui/TranslationProgressWindow.java rename to src/main/java/com/crowdin/ui/panel/progress/TranslationProgressWindow.java index 5691464..7a3c76b 100644 --- a/src/main/java/com/crowdin/ui/TranslationProgressWindow.java +++ b/src/main/java/com/crowdin/ui/panel/progress/TranslationProgressWindow.java @@ -1,13 +1,12 @@ -package com.crowdin.ui; +package com.crowdin.ui.panel.progress; -import com.crowdin.client.translationstatus.model.FileProgress; +import com.crowdin.client.translationstatus.model.FileBranchProgress; import com.crowdin.client.translationstatus.model.LanguageProgress; +import com.crowdin.ui.panel.ContentTab; import com.intellij.icons.AllIcons; import com.intellij.openapi.util.IconLoader; import com.intellij.ui.JBColor; import com.intellij.ui.treeStructure.Tree; -import lombok.Data; - import javax.swing.*; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeCellRenderer; @@ -19,7 +18,7 @@ import java.util.TreeMap; import java.util.stream.Collectors; -public class TranslationProgressWindow { +public class TranslationProgressWindow implements ContentTab { private JPanel panel1; private Tree tree1; private JLabel translatedTip; @@ -29,7 +28,7 @@ public class TranslationProgressWindow { private boolean groupByFiles = false; private String projectName; - private Map> progressData; + private Map> progressData; private Map fileNames; private Map languageNames; @@ -41,6 +40,7 @@ public TranslationProgressWindow() { tree1.setCellRenderer(new TranslationProgressCellRenderer()); } + @Override public JPanel getContent() { return panel1; } @@ -53,7 +53,7 @@ public void setGroupByFiles(boolean groupByFiles) { this.groupByFiles = groupByFiles; } - public void setData(String projectName, Map> progressData, Map fileNames, Map languageNames) { + public void setData(String projectName, Map> progressData, Map fileNames, Map languageNames) { this.projectName = projectName; this.progressData = progressData; this.fileNames = fileNames; @@ -80,7 +80,7 @@ private DefaultMutableTreeNode buildTree() { Map fileGroups = new TreeMap<>(); for (LanguageProgress langProgress : sortedLanguageProgresses) { String languageName = languageNames.get(langProgress.getLanguageId()); - for (FileProgress fileProgress : progressData.get(langProgress)) { + for (FileBranchProgress fileProgress : progressData.get(langProgress)) { String fileName = fileNames.get(fileProgress.getFileId()); if (fileName == null) { continue; @@ -91,7 +91,7 @@ private DefaultMutableTreeNode buildTree() { fileProgress.getTranslationProgress() + "%", fileProgress.getApprovalProgress() + "%"))); } } - System.out.println(fileGroups); + for (DefaultMutableTreeNode fileNode : fileGroups.values()) { root.add(fileNode); } @@ -115,17 +115,12 @@ private DefaultMutableTreeNode buildTree() { public static class TranslationProgressCellRenderer extends DefaultTreeCellRenderer { - @Data public static class CellData { private Icon icon; - private String text; + private final String text; private String translatedProgressText; private String approvedProgressText; - public CellData(String text) { - this.text = text; - } - public CellData(Icon icon, String text) { this.icon = icon; this.text = text; @@ -143,6 +138,22 @@ public CellData(Icon icon, String text, String translatedProgressText, String ap this.translatedProgressText = translatedProgressText; this.approvedProgressText = approvedProgressText; } + + public Icon getIcon() { + return icon; + } + + public String getText() { + return text; + } + + public String getTranslatedProgressText() { + return translatedProgressText; + } + + public String getApprovedProgressText() { + return approvedProgressText; + } } diff --git a/src/main/java/com/crowdin/ui/TreeCellLanguage.form b/src/main/java/com/crowdin/ui/panel/progress/TreeCellLanguage.form similarity index 97% rename from src/main/java/com/crowdin/ui/TreeCellLanguage.form rename to src/main/java/com/crowdin/ui/panel/progress/TreeCellLanguage.form index 52f0613..4dcbe2d 100644 --- a/src/main/java/com/crowdin/ui/TreeCellLanguage.form +++ b/src/main/java/com/crowdin/ui/panel/progress/TreeCellLanguage.form @@ -1,5 +1,5 @@ - + diff --git a/src/main/java/com/crowdin/ui/TreeCellLanguage.java b/src/main/java/com/crowdin/ui/panel/progress/TreeCellLanguage.java similarity index 97% rename from src/main/java/com/crowdin/ui/TreeCellLanguage.java rename to src/main/java/com/crowdin/ui/panel/progress/TreeCellLanguage.java index 4b9d940..c2e99a2 100644 --- a/src/main/java/com/crowdin/ui/TreeCellLanguage.java +++ b/src/main/java/com/crowdin/ui/panel/progress/TreeCellLanguage.java @@ -1,4 +1,4 @@ -package com.crowdin.ui; +package com.crowdin.ui.panel.progress; import com.intellij.openapi.util.IconLoader; diff --git a/src/main/java/com/crowdin/ui/action/GroupProgressByFiles.java b/src/main/java/com/crowdin/ui/panel/progress/action/GroupProgressByFiles.java similarity index 68% rename from src/main/java/com/crowdin/ui/action/GroupProgressByFiles.java rename to src/main/java/com/crowdin/ui/panel/progress/action/GroupProgressByFiles.java index 74fca5d..ee921ca 100644 --- a/src/main/java/com/crowdin/ui/action/GroupProgressByFiles.java +++ b/src/main/java/com/crowdin/ui/panel/progress/action/GroupProgressByFiles.java @@ -1,21 +1,32 @@ -package com.crowdin.ui.action; +package com.crowdin.ui.panel.progress.action; -import com.crowdin.ui.TranslationProgressWindow; -import com.crowdin.ui.TranslationProgressWindowFactory; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.progress.TranslationProgressWindow; import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.actionSystem.ToggleAction; -import com.intellij.openapi.components.ServiceManager; import com.intellij.openapi.project.DumbAware; import com.intellij.openapi.project.Project; import org.jetbrains.annotations.NotNull; +import static com.crowdin.Constants.TOOLWINDOW_ID; + public class GroupProgressByFiles extends ToggleAction implements DumbAware { + private boolean enabled = false; + public GroupProgressByFiles() { super("Group by files", "Really group by files", AllIcons.Actions.GroupByFile); } + @Override + public void update(@NotNull AnActionEvent e) { + if (e.getPlace().equals(TOOLWINDOW_ID)) { + this.enabled = e.getPresentation().isEnabled(); + } + e.getPresentation().setEnabled(enabled); + } + @Override public boolean isSelected(@NotNull AnActionEvent e) { TranslationProgressWindow window = getTranslationProgressWindowOrNull(e.getProject()); @@ -39,8 +50,7 @@ private TranslationProgressWindow getTranslationProgressWindowOrNull(Project pro if (project == null) { return null; } - TranslationProgressWindowFactory.ProjectService projectService = - ServiceManager.getService(project, TranslationProgressWindowFactory.ProjectService.class); - return projectService.getTranslationProgressWindow(); + + return project.getService(ProjectService.class).getTranslationProgressWindow(); } } diff --git a/src/main/java/com/crowdin/ui/panel/progress/action/RefreshTranslationProgressAction.java b/src/main/java/com/crowdin/ui/panel/progress/action/RefreshTranslationProgressAction.java new file mode 100644 index 0000000..6e8b22c --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/progress/action/RefreshTranslationProgressAction.java @@ -0,0 +1,136 @@ +package com.crowdin.ui.panel.progress.action; + +import com.crowdin.action.ActionContext; +import com.crowdin.action.BackgroundAction; +import com.crowdin.client.languages.model.Language; +import com.crowdin.client.sourcefiles.model.FileInfo; +import com.crowdin.client.translationstatus.model.FileBranchProgress; +import com.crowdin.client.translationstatus.model.LanguageProgress; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.panel.progress.TranslationProgressWindow; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; +import com.crowdin.util.StringUtils; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static com.crowdin.Constants.PROGRESS_GROUP_FILES_BY_FILE_ACTION; +import static com.crowdin.util.FileUtil.joinPaths; +import static com.crowdin.util.FileUtil.normalizePath; +import static com.crowdin.util.FileUtil.sepAtStart; +import static com.crowdin.util.FileUtil.unixPath; + +public class RefreshTranslationProgressAction extends BackgroundAction { + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + public RefreshTranslationProgressAction() { + super("Refresh data", "Refresh data", AllIcons.Actions.Refresh); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(!isInProgress.get()); + } + + @Override + protected void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { + boolean forceRefresh = !CrowdinPanelWindowFactory.PLACE_ID.equals(e.getPlace()); + Project project = e.getProject(); + if (project == null) { + return; + } + + e.getPresentation().setEnabled(false); + isInProgress.set(true); + try { + + TranslationProgressWindow window = project.getService(ProjectService.class).getTranslationProgressWindow(); + if (window == null) { + return; + } + + Optional context = super.prepare(project, indicator, false, false, forceRefresh, null, null); + + if (context.isEmpty()) { + return; + } + + Map> progress = context.get().crowdin.getProjectProgress() + .parallelStream() + .collect(Collectors.toMap(Function.identity(), langProgress -> context.get().crowdin.getLanguageProgress(langProgress.getLanguageId()))); + + List crowdinFilePaths = context.get().properties.getFiles().stream() + .flatMap((fileBean) -> { + List sourceFiles = FileUtil.getSourceFilesRec(context.get().root, fileBean.getSource()); + return sourceFiles.stream().map(sourceFile -> { + if (context.get().properties.isPreserveHierarchy()) { + VirtualFile pathToPattern = FileUtil.getBaseDir(sourceFile, fileBean.getSource()); + + String relativePathToPattern = FileUtil.findRelativePath(FileUtil.getProjectBaseDir(project), pathToPattern); + String patternPathToFile = FileUtil.findRelativePath(pathToPattern, sourceFile.getParent()); + + return unixPath(sepAtStart(normalizePath(joinPaths(relativePathToPattern, patternPathToFile, sourceFile.getName())))); + } else { + return unixPath(sepAtStart(sourceFile.getName())); + } + }); + }) + .collect(Collectors.toList()); + + + Map fileNames = context.get().crowdinProjectCache.getFileInfos(context.get().branch).values() + .stream() + .filter((fileInfo) -> crowdinFilePaths.contains(removeBranchNameInPath(fileInfo.getPath(), context.get().branchName))) + .collect(Collectors.toMap(FileInfo::getId, file -> removeBranchNameInPath(file.getPath(), context.get().branchName))); + Map languageNames = context.get().crowdinProjectCache.getProjectLanguages() + .stream() + .collect(Collectors.toMap(Language::getId, Language::getName)); + + ApplicationManager.getApplication().invokeAndWait(() -> { + window.setData(context.get().crowdinProjectCache.getProject().getName(), progress, fileNames, languageNames); + window.rebuildTree(); + CrowdinPanelWindowFactory.updateToolbar( + PROGRESS_GROUP_FILES_BY_FILE_ACTION, + null, + true, + !context.get().crowdinProjectCache.isStringsBased() + ); + }); + } catch (ProcessCanceledException ex) { + throw ex; + } catch (Exception ex) { + if (forceRefresh) { + NotificationUtil.logErrorMessage(project, ex); + NotificationUtil.showErrorMessage(project, ex.getMessage()); + } + } finally { + e.getPresentation().setEnabled(true); + isInProgress.set(false); + } + } + + @Override + protected String loadingText(AnActionEvent e) { + return "Refresh translation progress"; + } + + private String removeBranchNameInPath(String path, String branchName) { + return (!StringUtils.isEmpty(branchName)) ? path.replaceAll("^/" + branchName, "") : path; + } +} diff --git a/src/main/java/com/crowdin/ui/panel/upload/UploadWindow.form b/src/main/java/com/crowdin/ui/panel/upload/UploadWindow.form new file mode 100644 index 0000000..e8aecf3 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/upload/UploadWindow.form @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/java/com/crowdin/ui/panel/upload/UploadWindow.java b/src/main/java/com/crowdin/ui/panel/upload/UploadWindow.java new file mode 100644 index 0000000..321b985 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/upload/UploadWindow.java @@ -0,0 +1,64 @@ +package com.crowdin.ui.panel.upload; + +import com.crowdin.ui.panel.ContentTab; +import com.crowdin.ui.tree.CellData; +import com.crowdin.ui.tree.CellRenderer; +import com.crowdin.ui.tree.FileTree; +import com.intellij.ui.JBColor; +import com.intellij.ui.treeStructure.Tree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; +import java.util.List; +import java.util.Optional; + +public class UploadWindow implements ContentTab { + private JPanel panel1; + private JScrollPane scrollPane; + private Tree tree1; + + private DefaultMutableTreeNode selectedElement; + + public UploadWindow() { + scrollPane.getViewport().setBackground(JBColor.WHITE); + tree1.setCellRenderer(new CellRenderer()); + tree1.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); + this.setPlug("Refresh tree"); + tree1.addTreeSelectionListener(e -> + Optional.ofNullable(e.getNewLeadSelectionPath()) + .map(TreePath::getLastPathComponent) + .map(DefaultMutableTreeNode.class::cast) + .ifPresent(node -> this.selectedElement = node) + ); + } + + public void setPlug(String text) { + tree1.setModel(new DefaultTreeModel(new DefaultMutableTreeNode(CellData.root(text)))); + } + + @Override + public JPanel getContent() { + return panel1; + } + + public List getSelectedFiles() { + return FileTree.getFiles(this.selectedElement); + } + + public void rebuildTree(String projectName, List files) { + this.selectedElement = null; + tree1.setModel(new DefaultTreeModel(FileTree.buildTree(projectName, files))); + expandAll(); + } + + public void expandAll() { + FileTree.expandAll(tree1); + } + + public void collapseAll() { + FileTree.collapseAll(tree1); + } +} diff --git a/src/main/java/com/crowdin/ui/panel/upload/action/CollapseAction.java b/src/main/java/com/crowdin/ui/panel/upload/action/CollapseAction.java new file mode 100644 index 0000000..4b83e42 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/upload/action/CollapseAction.java @@ -0,0 +1,64 @@ +package com.crowdin.ui.panel.upload.action; + +import com.crowdin.action.BackgroundAction; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.upload.UploadWindow; +import com.crowdin.util.NotificationUtil; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class CollapseAction extends BackgroundAction { + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + public CollapseAction() { + super("Collapse tree", "Collapse tree", AllIcons.Actions.Collapseall); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(!isInProgress.get()); + } + + @Override + protected void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { + Project project = e.getProject(); + if (project == null) { + return; + } + + e.getPresentation().setEnabled(false); + isInProgress.set(true); + try { + UploadWindow window = project.getService(ProjectService.class).getUploadWindow(); + + if (window == null) { + return; + } + + ApplicationManager.getApplication().invokeAndWait(window::collapseAll); + } catch (ProcessCanceledException ex) { + throw ex; + } catch (Exception ex) { + NotificationUtil.logErrorMessage(project, ex); + NotificationUtil.showErrorMessage(project, ex.getMessage()); + } finally { + e.getPresentation().setEnabled(true); + isInProgress.set(false); + } + } + + @Override + protected String loadingText(AnActionEvent e) { + return "Collapse upload tree"; + } + +} diff --git a/src/main/java/com/crowdin/ui/panel/upload/action/ExpandAction.java b/src/main/java/com/crowdin/ui/panel/upload/action/ExpandAction.java new file mode 100644 index 0000000..46013aa --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/upload/action/ExpandAction.java @@ -0,0 +1,64 @@ +package com.crowdin.ui.panel.upload.action; + +import com.crowdin.action.BackgroundAction; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.upload.UploadWindow; +import com.crowdin.util.NotificationUtil; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import org.jetbrains.annotations.NotNull; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class ExpandAction extends BackgroundAction { + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + public ExpandAction() { + super("Expand tree", "Expand tree", AllIcons.Actions.Expandall); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(!isInProgress.get()); + } + + @Override + protected void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { + Project project = e.getProject(); + if (project == null) { + return; + } + + e.getPresentation().setEnabled(false); + isInProgress.set(true); + try { + UploadWindow window = project.getService(ProjectService.class).getUploadWindow(); + + if (window == null) { + return; + } + + ApplicationManager.getApplication().invokeAndWait(window::expandAll); + } catch (ProcessCanceledException ex) { + throw ex; + } catch (Exception ex) { + NotificationUtil.logErrorMessage(project, ex); + NotificationUtil.showErrorMessage(project, ex.getMessage()); + } finally { + e.getPresentation().setEnabled(true); + isInProgress.set(false); + } + } + + @Override + protected String loadingText(AnActionEvent e) { + return "Expand upload tree"; + } + +} diff --git a/src/main/java/com/crowdin/ui/panel/upload/action/RefreshAction.java b/src/main/java/com/crowdin/ui/panel/upload/action/RefreshAction.java new file mode 100644 index 0000000..e743721 --- /dev/null +++ b/src/main/java/com/crowdin/ui/panel/upload/action/RefreshAction.java @@ -0,0 +1,92 @@ +package com.crowdin.ui.panel.upload.action; + +import com.crowdin.action.ActionContext; +import com.crowdin.action.BackgroundAction; +import com.crowdin.client.FileBean; +import com.crowdin.service.ProjectService; +import com.crowdin.ui.panel.CrowdinPanelWindowFactory; +import com.crowdin.ui.panel.upload.UploadWindow; +import com.crowdin.util.FileUtil; +import com.crowdin.util.NotificationUtil; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.actionSystem.AnActionEvent; +import com.intellij.openapi.application.ApplicationManager; +import com.intellij.openapi.progress.ProcessCanceledException; +import com.intellij.openapi.progress.ProgressIndicator; +import com.intellij.openapi.project.Project; +import com.intellij.openapi.vfs.VirtualFile; +import org.jetbrains.annotations.NotNull; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; + +public class RefreshAction extends BackgroundAction { + + private final AtomicBoolean isInProgress = new AtomicBoolean(false); + + public RefreshAction() { + super("Refresh data", "Refresh data", AllIcons.Actions.Refresh); + } + + @Override + public void update(@NotNull AnActionEvent e) { + super.update(e); + e.getPresentation().setEnabled(!isInProgress.get()); + } + + @Override + protected void performInBackground(@NotNull AnActionEvent e, @NotNull ProgressIndicator indicator) { + boolean forceRefresh = !CrowdinPanelWindowFactory.PLACE_ID.equals(e.getPlace()); + Project project = e.getProject(); + if (project == null) { + return; + } + + e.getPresentation().setEnabled(false); + isInProgress.set(true); + try { + UploadWindow window = project.getService(ProjectService.class).getUploadWindow(); + + if (window == null) { + return; + } + + Optional context = super.prepare(project, indicator, false, false, forceRefresh, null, null); + + if (context.isEmpty()) { + return; + } + + List files = new ArrayList<>(); + + for (FileBean fileBean : context.get().properties.getFiles()) { + for (VirtualFile source : FileUtil.getSourceFilesRec(context.get().root, fileBean.getSource())) { + String file = Paths.get(context.get().root.getPath()).relativize(Paths.get(source.getPath())).toString(); + files.add(file); + } + } + + ApplicationManager.getApplication() + .invokeAndWait(() -> window.rebuildTree(context.get().crowdinProjectCache.getProject().getName(), files)); + } catch (ProcessCanceledException ex) { + throw ex; + } catch (Exception ex) { + if (forceRefresh) { + NotificationUtil.logErrorMessage(project, ex); + NotificationUtil.showErrorMessage(project, ex.getMessage()); + } + } finally { + e.getPresentation().setEnabled(true); + isInProgress.set(false); + } + } + + @Override + protected String loadingText(AnActionEvent e) { + return "Refresh upload panel"; + } + +} diff --git a/src/main/java/com/crowdin/ui/tree/CellData.java b/src/main/java/com/crowdin/ui/tree/CellData.java new file mode 100644 index 0000000..7b7ebf6 --- /dev/null +++ b/src/main/java/com/crowdin/ui/tree/CellData.java @@ -0,0 +1,118 @@ +package com.crowdin.ui.tree; + +import com.crowdin.client.bundles.model.Bundle; +import com.intellij.icons.AllIcons; +import com.intellij.openapi.util.IconLoader; +import org.apache.commons.io.FilenameUtils; + +import javax.swing.*; +import java.util.Map; +import java.util.Optional; + +public class CellData { + + private static final Icon LOGO = IconLoader.getIcon("/icons/icon.svg", CellData.class); + + private static final Map FILES_TYPES_ICONS = Map.of( + "xml", AllIcons.FileTypes.Xml, + "json", AllIcons.FileTypes.Json, + "html", AllIcons.FileTypes.Html, + "xsd", AllIcons.FileTypes.XsdFile, + "css", AllIcons.FileTypes.Css, + "yml", AllIcons.FileTypes.Yaml, + "yaml", AllIcons.FileTypes.Yaml + ); + + private final String text; + private final Icon icon; + private final String file; + private final Bundle bundle; + + private final boolean isRoot; + + private final String link; + + public static CellData root(String text) { + return new CellData(true, text, LOGO, null, null, null); + } + + public static CellData folder(String text) { + return new CellData(false, text, AllIcons.Nodes.Folder, null, null, null); + } + + public static CellData file(String text, String file) { + String extension = FilenameUtils.getExtension(file); + Icon icon = Optional.ofNullable(extension) + .filter(e -> !e.isEmpty()) + .map(String::toLowerCase) + .filter(FILES_TYPES_ICONS::containsKey) + .map(FILES_TYPES_ICONS::get) + .orElse(AllIcons.FileTypes.Text); + return new CellData(false, text, icon, file, null, null); + } + + public static CellData bundle(Bundle bundle) { + return new CellData(false, bundle.getName(), AllIcons.FileTypes.Archive, null, bundle, null); + } + + public static CellData link(String text, String link) { + return new CellData(false, text, AllIcons.Ide.Link, null, null, link); + } + + private CellData(boolean isRoot, String text, Icon icon, String file, Bundle bundle, String link) { + this.isRoot = isRoot; + this.text = text; + this.icon = icon; + this.file = file; + this.bundle = bundle; + this.link = link; + } + + public boolean isRoot() { + return isRoot; + } + + public String getText() { + return text; + } + + public Icon getIcon() { + return icon; + } + + public boolean isFile() { + return file != null; + } + + public String getFile() { + return file; + } + + public boolean isBundle() { + return bundle != null; + } + + public Bundle getBundle() { + return bundle; + } + + public boolean isLink() { + return link != null; + } + + public String getLink() { + return link; + } + + @Override + public String toString() { + return "CellData{" + + "text='" + text + '\'' + + ", icon=" + icon + + ", file='" + file + '\'' + + ", bundle=" + bundle + + ", isRoot=" + isRoot + + ", link='" + link + '\'' + + '}'; + } +} diff --git a/src/main/java/com/crowdin/ui/tree/CellRenderer.java b/src/main/java/com/crowdin/ui/tree/CellRenderer.java new file mode 100644 index 0000000..47c7571 --- /dev/null +++ b/src/main/java/com/crowdin/ui/tree/CellRenderer.java @@ -0,0 +1,26 @@ +package com.crowdin.ui.tree; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeCellRenderer; +import java.awt.*; + +public class CellRenderer extends DefaultTreeCellRenderer { + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { + super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus); + CellData cellData = CellRenderer.getData(value); + if (cellData == null) { + return null; + } + + FilesTreeItem filesTreeItem = new FilesTreeItem(cellData.getText(), cellData.getIcon()); + return filesTreeItem.getContent(); + } + + public static CellData getData(Object value) { + return CellData.class.cast(DefaultMutableTreeNode.class.cast(value).getUserObject()); + } + +} diff --git a/src/main/java/com/crowdin/ui/tree/FileTree.java b/src/main/java/com/crowdin/ui/tree/FileTree.java new file mode 100644 index 0000000..650d13f --- /dev/null +++ b/src/main/java/com/crowdin/ui/tree/FileTree.java @@ -0,0 +1,138 @@ +package com.crowdin.ui.tree; + +import com.crowdin.util.FileUtil; + +import javax.swing.*; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreeNode; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.List; +import java.util.stream.StreamSupport; + +public class FileTree { + + public static DefaultMutableTreeNode buildTree(String name, List files) { + DefaultMutableTreeNode root = new DefaultMutableTreeNode(CellData.root(name)); + + List>> parts = files + .stream() + .map(f -> f.startsWith(FileUtil.PATH_SEPARATOR) ? f.substring(1) : f) + .map(f -> { + List fileParts = StreamSupport.stream(Paths.get(f).spliterator(), false).map(Path::toString).toList(); + return new AbstractMap.SimpleEntry<>(f, fileParts); + }) + .toList(); + + for (AbstractMap.SimpleEntry> entry : parts) { + String filePath = entry.getKey(); + List subParts = entry.getValue(); + DefaultMutableTreeNode prev = root; + for (int j = 0; j < subParts.size(); j++) { + for (int k = 0; k < j; k++) { + String parent = subParts.get(k); + Enumeration children = prev.children(); + while (children.hasMoreElements()) { + DefaultMutableTreeNode child = (DefaultMutableTreeNode) children.nextElement(); + CellData data = CellRenderer.getData(child); + if (data.getText().equals(parent)) { + prev = child; + break; + } + } + } + + String part = subParts.get(j); + + if (j + 1 != subParts.size()) { + //check if folder already created + boolean alreadyCreated = false; + Enumeration children = prev.children(); + while (children.hasMoreElements()) { + DefaultMutableTreeNode child = (DefaultMutableTreeNode) children.nextElement(); + CellData data = CellRenderer.getData(child); + if (data.getText().equals(part)) { + alreadyCreated = true; + break; + } + } + if (alreadyCreated) { + continue; + } + } + + DefaultMutableTreeNode element = j + 1 == subParts.size() + ? new DefaultMutableTreeNode(CellData.file(part, filePath)) + : new DefaultMutableTreeNode(CellData.folder(part)); + prev.add(element); + + if (j + 1 == subParts.size()) { + //reset + prev = root; + } + } + } + + return root; + } + + public static List getFiles(DefaultMutableTreeNode selectedElement) { + if (selectedElement == null) { + return Collections.emptyList(); + } + + CellData cell = CellRenderer.getData(selectedElement); + + if (cell.isRoot()) { + //empty list to force bulk action + return Collections.emptyList(); + } + + if (cell.isFile()) { + return Collections.singletonList(cell.getFile()); + } + + return FileTree + .childNodes(selectedElement, true) + .stream() + .map(CellRenderer::getData) + .filter(CellData::isFile) + .map(CellData::getFile) + .toList(); + } + + public static List childNodes(DefaultMutableTreeNode parent, boolean recursive) { + List res = new ArrayList<>(); + Enumeration children = recursive ? parent.breadthFirstEnumeration() : parent.children(); + while (children.hasMoreElements()) { + res.add((DefaultMutableTreeNode) children.nextElement()); + } + return res; + } + + public static void expandAll(JTree tree) { + toggleTree(tree, 0, tree.getRowCount(), false); + } + + public static void collapseAll(JTree tree) { + toggleTree(tree, 0, tree.getRowCount(), true); + } + + private static void toggleTree(JTree tree, int startingIndex, int rowCount, boolean collapse) { + for (int i = startingIndex; i < rowCount; ++i) { + if (collapse) { + tree.collapseRow(i); + } else { + tree.expandRow(i); + } + } + + if (tree.getRowCount() != rowCount) { + toggleTree(tree, rowCount, tree.getRowCount(), collapse); + } + } +} diff --git a/src/main/java/com/crowdin/ui/tree/FilesTreeItem.form b/src/main/java/com/crowdin/ui/tree/FilesTreeItem.form new file mode 100644 index 0000000..3c91a50 --- /dev/null +++ b/src/main/java/com/crowdin/ui/tree/FilesTreeItem.form @@ -0,0 +1,39 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/crowdin/ui/tree/FilesTreeItem.java b/src/main/java/com/crowdin/ui/tree/FilesTreeItem.java new file mode 100644 index 0000000..5138015 --- /dev/null +++ b/src/main/java/com/crowdin/ui/tree/FilesTreeItem.java @@ -0,0 +1,21 @@ +package com.crowdin.ui.tree; + +import javax.swing.*; + +public class FilesTreeItem { + private JPanel content; + private JLabel label; + + public FilesTreeItem(String text, Icon icon) { + label.setText(text); + label.setIcon(icon); + } + + public void setIcon(Icon icon) { + label.setIcon(icon); + } + + public JPanel getContent() { + return content; + } +} diff --git a/src/main/java/com/crowdin/util/ActionUtils.java b/src/main/java/com/crowdin/util/ActionUtils.java deleted file mode 100644 index 19d785b..0000000 --- a/src/main/java/com/crowdin/util/ActionUtils.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.crowdin.util; - -import com.crowdin.client.CrowdinProperties; -import com.intellij.openapi.project.Project; - -import static com.crowdin.Constants.MESSAGES_BUNDLE; - -public class ActionUtils { - - public static String getBranchName(Project project, CrowdinProperties properties, boolean performCheck) { - String branchName = properties.isDisabledBranches() ? "" : GitUtil.getCurrentBranch(project).getName(); - if (performCheck) { - if (!CrowdinFileUtil.isValidBranchName(branchName)) { - throw new RuntimeException(MESSAGES_BUNDLE.getString("errors.branch_contains_forbidden_symbols")); - } - } - return branchName; - } -} diff --git a/src/main/java/com/crowdin/util/CrowdinFileUtil.java b/src/main/java/com/crowdin/util/CrowdinFileUtil.java index 998f6c2..8acd7c9 100644 --- a/src/main/java/com/crowdin/util/CrowdinFileUtil.java +++ b/src/main/java/com/crowdin/util/CrowdinFileUtil.java @@ -2,8 +2,6 @@ import com.crowdin.client.languages.model.Language; import com.crowdin.client.sourcefiles.model.*; -import lombok.NonNull; -import org.apache.commons.lang.StringUtils; import java.util.HashMap; import java.util.List; @@ -11,13 +9,11 @@ import java.util.function.Function; import java.util.stream.Collectors; -import static com.crowdin.Constants.MESSAGES_BUNDLE; - public class CrowdinFileUtil { private CrowdinFileUtil() {} - public static Map buildFilePaths(@NonNull List files, @NonNull Map dirs) { + public static Map buildFilePaths(List files, Map dirs) { Map filePaths = new HashMap<>(); for (F file : files) { StringBuilder sb = new StringBuilder(file.getName()); @@ -33,7 +29,7 @@ public static Map buildFilePaths(@NonNull List< return filePaths; } - public static Map buildDirPaths(@NonNull Map dirs) { + public static Map buildDirPaths(Map dirs) { Map dirPaths = new HashMap<>(); for (Directory dir : dirs.values()) { StringBuilder sb = new StringBuilder(dir.getName()); @@ -49,12 +45,12 @@ public static Map buildDirPaths(@NonNull Map return dirPaths; } - public static Map revDirPaths(@NonNull Map dirs) { + public static Map revDirPaths(Map dirs) { return dirs.keySet().stream() .collect(Collectors.toMap(path -> dirs.get(path).getId(), Function.identity())); } - public static Map buildAllProjectTranslationsWithSources(@NonNull List sources, @NonNull Map dirPaths, @NonNull List projLanguages, LanguageMapping languageMapping) { + public static Map buildAllProjectTranslationsWithSources(List sources, Map dirPaths, List projLanguages, LanguageMapping languageMapping) { Map result = new HashMap<>(); for (File source : sources) { String sourcePath = ((source.getDirectoryId() != null) ? dirPaths.get(source.getDirectoryId()) + java.io.File.separator : java.io.File.separator) + source.getName(); diff --git a/src/main/java/com/crowdin/util/FileUtil.java b/src/main/java/com/crowdin/util/FileUtil.java index 3dab852..1435cde 100644 --- a/src/main/java/com/crowdin/util/FileUtil.java +++ b/src/main/java/com/crowdin/util/FileUtil.java @@ -5,10 +5,8 @@ import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VirtualFile; -import lombok.NonNull; -import org.apache.commons.lang.RandomStringUtils; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.lang.SystemUtils; +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.exception.ZipException; import java.io.File; import java.io.FileInputStream; @@ -18,9 +16,12 @@ import java.io.OutputStream; import java.net.URL; import java.nio.file.FileSystems; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -33,6 +34,9 @@ public final class FileUtil { public static final String PATH_SEPARATOR = FileSystems.getDefault().getSeparator(); public static final String PATH_SEPARATOR_REGEX = "\\".equals(PATH_SEPARATOR) ? "\\\\" : PATH_SEPARATOR; + public static final String OS_NAME = System.getProperty("os.name"); + public static final boolean IS_WINDOWS = OS_NAME != null && OS_NAME.startsWith("Windows"); + private FileUtil() { throw new UnsupportedOperationException(); } @@ -46,9 +50,13 @@ public static VirtualFile findVFileByPath(String path) { return LocalFileSystem.getInstance().findFileByPath(path); } - public static String findRelativePath(@NonNull VirtualFile baseDir, @NonNull VirtualFile file) { + public static VirtualFile findVFileByPath(Path path) { + return LocalFileSystem.getInstance().findFileByNioFile(path); + } + + public static String findRelativePath(VirtualFile baseDir, VirtualFile file) { return StringUtils.removeStart(file.getCanonicalPath(), baseDir.getCanonicalPath()) - .replaceAll("^[\\\\/]+", ""); + .replaceAll("^[\\\\/]+", ""); // @AvailableSince("181.2784.17") // return VfsUtil.findRelativePath(baseDir, file, java.io.File.separatorChar); } @@ -83,14 +91,14 @@ public static List getSourceFilesRec(VirtualFile root, String sourc if (!child.isDirectory() && !isDir && child.getName().matches(searchableRegex)) { files.add(child); } else if (child.isDirectory() && isDir && child.getName().matches(searchableRegex)) { - files.addAll(getSourceFilesRec(child, source.substring(sepIndex+1))); + files.addAll(getSourceFilesRec(child, source.substring(sepIndex + 1))); } } } else { VirtualFile foundChild = root.findChild(searchable); if (foundChild != null) { if (foundChild.isDirectory() && isDir) { - files.addAll(getSourceFilesRec(foundChild, source.substring(sepIndex+1))); + files.addAll(getSourceFilesRec(foundChild, source.substring(sepIndex + 1))); } else if (!foundChild.isDirectory() && !isDir) { files.add(foundChild); } @@ -115,16 +123,16 @@ public static Predicate filePathRegex(String filePathPattern, boolean pr return Pattern.compile("^" + PlaceholderUtil.formatSourcePatternForRegex(noSepAtStart(filePathPattern)) + "$").asPredicate(); } else { List sourcePatternSplits = Arrays.stream(splitPath(noSepAtStart(filePathPattern))) - .map(PlaceholderUtil::formatSourcePatternForRegex) - .collect(Collectors.toList()); + .map(PlaceholderUtil::formatSourcePatternForRegex) + .collect(Collectors.toList()); StringBuilder sourcePatternRegex = new StringBuilder(); - for (int i = 0; i < sourcePatternSplits.size()-1; i++) { + for (int i = 0; i < sourcePatternSplits.size() - 1; i++) { sourcePatternRegex.insert(0, "(") - .append(sourcePatternSplits.get(i)).append(PATH_SEPARATOR_REGEX).append(")?"); + .append(sourcePatternSplits.get(i)).append(PATH_SEPARATOR_REGEX).append(")?"); } sourcePatternRegex.insert(0, FileUtil.PATH_SEPARATOR_REGEX).insert(0, "^") - .append(sourcePatternSplits.get(sourcePatternSplits.size()-1)).append("$"); + .append(sourcePatternSplits.get(sourcePatternSplits.size() - 1)).append("$"); return Pattern.compile(sourcePatternRegex.toString()).asPredicate(); } @@ -149,7 +157,7 @@ public static void downloadFile(Object requestor, VirtualFile file, InputStream } public static File downloadTempFile(InputStream data) throws IOException { - File tempFile = FileUtilRt.createTempFile(RandomStringUtils.randomAlphanumeric(9), ".crowdin.tmp", true); + File tempFile = FileUtilRt.createTempFile(String.valueOf(System.currentTimeMillis()), ".crowdin.tmp", true); try (OutputStream tempFileOutput = new FileOutputStream(tempFile)) { FileUtilRt.copy(data, tempFileOutput); } @@ -164,13 +172,13 @@ public static VirtualFile createIfNeededFilePath(Object requestor, VirtualFile r VirtualFile child = dir.findChild(splitFilePath[i]); dir = (child != null) ? child : dir.createChildDirectory(requestor, splitFilePath[i]); } - VirtualFile file = dir.findChild(splitFilePath[splitFilePath.length-1]); - return (file != null) ? file : dir.createChildData(requestor, splitFilePath[splitFilePath.length-1]); + VirtualFile file = dir.findChild(splitFilePath[splitFilePath.length - 1]); + return (file != null) ? file : dir.createChildData(requestor, splitFilePath[splitFilePath.length - 1]); }); } public static String normalizePath(String path) { - return path.replaceAll("[\\\\/]+", SystemUtils.IS_OS_WINDOWS ? "\\\\" : "/"); + return path.replaceAll("[\\\\/]+", IS_WINDOWS ? "\\\\" : "/"); } public static String unixPath(String path) { @@ -200,4 +208,45 @@ public static String noSepAtEnd(String path) { public static String sepAtEnd(String path) { return noSepAtEnd(path) + PATH_SEPARATOR; } + + public static void extractArchive(File archive, String dirPath) { + if (archive == null) { + return; + } + + ZipFile zipFile; + + try (ZipFile file = new ZipFile(archive)) { + zipFile = file; + } catch (IllegalArgumentException e) { + throw new RuntimeException("Unexpected error: couldn't find zip file", e); + } catch (IOException e) { + throw new RuntimeException("Unexpected error: couldn't read zip file", e); + } + + try { + zipFile.extractAll(dirPath); + } catch (ZipException e) { + throw new RuntimeException("Unexpected error: couldn't extract zip file", e); + } + } + + public static void clear(VirtualFile root, File archive, String tempDir) { + if (archive != null) { + archive.delete(); + } + if (tempDir != null) { + try (var dirStream = Files.walk(Paths.get(tempDir))) { + dirStream + .map(Path::toFile) + .sorted(Comparator.reverseOrder()) + .forEach(File::delete); + } catch (IOException e) { + throw new RuntimeException("Couldn't delete temporary directory", e); + } + } + if (root != null) { + root.refresh(true, true); + } + } } diff --git a/src/main/java/com/crowdin/util/GitUtil.java b/src/main/java/com/crowdin/util/GitUtil.java index 87fa1f6..340ab61 100644 --- a/src/main/java/com/crowdin/util/GitUtil.java +++ b/src/main/java/com/crowdin/util/GitUtil.java @@ -28,7 +28,7 @@ public static BranchInfo getCurrentBranch(@NotNull final Project project) { GitLocalBranch localBranch; String branchName = ""; try { - repository = GitBranchUtil.getCurrentRepository(project); + repository = GitBranchUtil.guessWidgetRepository(project, FileUtil.getProjectBaseDir(project)); if (repository == null) { throw new RuntimeException(MESSAGES_BUNDLE.getString("errors.not_found_git_branch")); } diff --git a/src/main/java/com/crowdin/util/LanguageMapping.java b/src/main/java/com/crowdin/util/LanguageMapping.java index ae73186..c30bdc3 100644 --- a/src/main/java/com/crowdin/util/LanguageMapping.java +++ b/src/main/java/com/crowdin/util/LanguageMapping.java @@ -1,12 +1,8 @@ package com.crowdin.util; -import lombok.NonNull; -import lombok.ToString; - import java.util.HashMap; import java.util.Map; -@ToString public class LanguageMapping { private final Map> languageMapping; @@ -60,4 +56,11 @@ private static Map> deepCopy(Map { - Notification notification = GROUP_DISPLAY_ID_INFO.createNotification(TITLE, message, type, null); + Notification notification = GROUP_DISPLAY_ID_INFO.createNotification(TITLE, message, type); Notifications.Bus.notify(notification, project); }); } @@ -60,15 +59,17 @@ public static void logDebugMessage(@NotNull Project project, @NotNull String mes } public static void logErrorMessage(@NotNull Project project, @NotNull Exception e) { - Throwable rootCause = ExceptionUtils.getRootCause(e); - logMessage(project, ExceptionUtils.getStackTrace((rootCause != null) ? rootCause : e), NotificationType.ERROR, "ERROR"); + StringWriter sw = new StringWriter(); + PrintWriter pw = new PrintWriter(sw, true); + e.printStackTrace(pw); + logMessage(project, sw.getBuffer().toString(), NotificationType.ERROR, "ERROR"); } private static void logMessage(@NotNull Project project, @NotNull String message, @NotNull NotificationType type, String level) { if (isDebug) { String formattedMessage = String.format("%s %s : %s", logDateFormat.format(new Date()), level, message); ApplicationManager.getApplication().invokeLater(() -> { - Notification notification = GROUP_DISPLAY_ID_INFO_LOG.createNotification(TITLE, formattedMessage, type, null); + Notification notification = GROUP_DISPLAY_ID_INFO_LOG.createNotification(TITLE, formattedMessage, type); Notifications.Bus.notify(notification, project); }); } diff --git a/src/main/java/com/crowdin/util/PlaceholderUtil.java b/src/main/java/com/crowdin/util/PlaceholderUtil.java index 548b632..b27ba3b 100644 --- a/src/main/java/com/crowdin/util/PlaceholderUtil.java +++ b/src/main/java/com/crowdin/util/PlaceholderUtil.java @@ -1,13 +1,10 @@ package com.crowdin.util; -import com.crowdin.Constants; import com.crowdin.client.languages.model.Language; -import lombok.NonNull; import org.apache.commons.io.FilenameUtils; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; import static java.util.stream.Collectors.toMap; @@ -77,7 +74,7 @@ public static Map buildTranslationPatterns( .collect(toMap(lang -> lang, lang -> PlaceholderUtil.replaceLanguagePlaceholders(basePattern, lang, languageMapping))); } - public static String replaceLanguagePlaceholders(@NonNull String pattern, @NonNull Language lang, LanguageMapping langMapping) { + public static String replaceLanguagePlaceholders(String pattern, Language lang, LanguageMapping langMapping) { return pattern .replaceAll(PLACEHOLDER_LANGUAGE_ID, langMapping.getValueOrDefault(lang.getId(), PLACEHOLDER_LANGUAGE_ID_NAME, lang.getId())) @@ -100,7 +97,7 @@ public static String replaceLanguagePlaceholders(@NonNull String pattern, @NonNu langMapping.getValueOrDefault(lang.getId(), PLACEHOLDER_OSX_CODE_NAME, lang.getOsxCode())); } - public static String replaceFilePlaceholders(@NonNull String toFormat, @NonNull String sourcePath) { + public static String replaceFilePlaceholders(String toFormat, String sourcePath) { return toFormat .replace(PLACEHOLDER_ORIGINAL_FILE_NAME, FilenameUtils.getName(sourcePath)) .replace(PLACEHOLDER_FILE_NAME, FilenameUtils.getBaseName(sourcePath)) diff --git a/src/main/java/com/crowdin/util/PropertyUtil.java b/src/main/java/com/crowdin/util/PropertyUtil.java index e5eec61..37a4ec4 100644 --- a/src/main/java/com/crowdin/util/PropertyUtil.java +++ b/src/main/java/com/crowdin/util/PropertyUtil.java @@ -3,7 +3,6 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.vfs.VirtualFile; -import java.io.FileInputStream; import java.io.InputStream; import java.util.Properties; diff --git a/src/main/java/com/crowdin/util/StringUtils.java b/src/main/java/com/crowdin/util/StringUtils.java new file mode 100644 index 0000000..38c7aaa --- /dev/null +++ b/src/main/java/com/crowdin/util/StringUtils.java @@ -0,0 +1,41 @@ +package com.crowdin.util; + +public final class StringUtils { + + private StringUtils() { + throw new UnsupportedOperationException(); + } + + public static String removeStart(String str, String remove) { + if (!isEmpty(str) && !isEmpty(remove)) { + return str.startsWith(remove) ? str.substring(remove.length()) : str; + } else { + return str; + } + } + + public static String removeEnd(String str, String remove) { + if (!isEmpty(str) && !isEmpty(remove)) { + return str.endsWith(remove) ? str.substring(0, str.length() - remove.length()) : str; + } else { + return str; + } + } + + public static boolean isEmpty(String str) { + return str == null || str.length() == 0; + } + + public static boolean containsNone(String str, String invalidChars) { + char[] sourceChars = str != null ? str.toCharArray() : new char[]{}; + for (char sourceChar : sourceChars) { + for (char invalidChar : invalidChars.toCharArray()) { + if (invalidChar == sourceChar) { + return false; + } + } + } + return true; + } + +} diff --git a/src/main/java/com/crowdin/util/UIUtil.java b/src/main/java/com/crowdin/util/UIUtil.java index b5ef41c..8e66e4d 100644 --- a/src/main/java/com/crowdin/util/UIUtil.java +++ b/src/main/java/com/crowdin/util/UIUtil.java @@ -1,7 +1,7 @@ package com.crowdin.util; -import com.crowdin.logic.CrowdinSettings; -import com.crowdin.ui.ConfirmActionDialog; +import com.crowdin.service.CrowdinSettings; +import com.crowdin.ui.dialog.ConfirmActionDialog; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.project.Project; @@ -27,4 +27,4 @@ public static boolean confirmDialog(Project project, CrowdinSettings settings, S } } -} \ No newline at end of file +} diff --git a/src/main/java/com/crowdin/util/Util.java b/src/main/java/com/crowdin/util/Util.java index 36042bb..f9ce031 100644 --- a/src/main/java/com/crowdin/util/Util.java +++ b/src/main/java/com/crowdin/util/Util.java @@ -8,8 +8,6 @@ import java.util.List; import java.util.stream.Collectors; -import static com.crowdin.Constants.MESSAGES_BUNDLE; - public class Util { private static final String PLUGIN_NAME = "crowdin-android-studio-plugin"; @@ -30,13 +28,18 @@ public static String getPluginVersion() { public static String getUserAgent() { ApplicationInfo appInfo = ApplicationInfo.getInstance(); return String.format("%s/%s %s/%s %s/%s", - PLUGIN_NAME, getPluginVersion(), - appInfo.getVersionName(), appInfo.getApiVersion(), - System.getProperty("os.name"), System.getProperty("os.version")); + PLUGIN_NAME, getPluginVersion(), + appInfo.getVersionName(), appInfo.getApiVersion(), + System.getProperty("os.name"), System.getProperty("os.version")); } public static String prepareListMessageText(String mainText, List items) { String itemsInOne = "
    " + items.stream().map(s -> "
  • " + s + "
  • \n").collect(Collectors.joining()) + "
"; return "

" + mainText + "

" + itemsInOne + ""; } + + public static boolean isFileFormatNotAllowed(Exception e) { + return e.getMessage().contains("files are not allowed to upload in string-based projects") || + e.getMessage().contains("files are not allowed to upload in strings-based projects"); + } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 56adea7..9126db1 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -2,7 +2,9 @@ com.crowdin.crowdin-idea Crowdin 1.6.3 - + + + Crowdin @@ -33,19 +35,43 @@ - - - - + + + + + + + + + + - + + + + + + + + - + + + + + + + + + + + + @@ -64,14 +90,6 @@ - - - - - - - - com.intellij.modules.vcs Git4Idea diff --git a/src/main/resources/icons/download-sources.svg b/src/main/resources/icons/download-sources.svg new file mode 100644 index 0000000..7dcc2f4 --- /dev/null +++ b/src/main/resources/icons/download-sources.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/icons/download-sources_dark.svg b/src/main/resources/icons/download-sources_dark.svg new file mode 100644 index 0000000..53f13f2 --- /dev/null +++ b/src/main/resources/icons/download-sources_dark.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/icons/download.svg b/src/main/resources/icons/download.svg new file mode 100644 index 0000000..f912b18 --- /dev/null +++ b/src/main/resources/icons/download.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/icons/download_dark.svg b/src/main/resources/icons/download_dark.svg new file mode 100644 index 0000000..d27c5fd --- /dev/null +++ b/src/main/resources/icons/download_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/icons/folder.svg b/src/main/resources/icons/folder.svg new file mode 100644 index 0000000..572b8cb --- /dev/null +++ b/src/main/resources/icons/folder.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/main/resources/icons/folder_dark.svg b/src/main/resources/icons/folder_dark.svg new file mode 100644 index 0000000..483a2d3 --- /dev/null +++ b/src/main/resources/icons/folder_dark.svg @@ -0,0 +1,13 @@ + + + + + + + diff --git a/src/main/resources/icons/upload-sources.svg b/src/main/resources/icons/upload-sources.svg new file mode 100644 index 0000000..40d5f0e --- /dev/null +++ b/src/main/resources/icons/upload-sources.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/icons/upload-sources_dark.svg b/src/main/resources/icons/upload-sources_dark.svg new file mode 100644 index 0000000..0b8121c --- /dev/null +++ b/src/main/resources/icons/upload-sources_dark.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/main/resources/icons/upload.svg b/src/main/resources/icons/upload.svg new file mode 100644 index 0000000..85ab07d --- /dev/null +++ b/src/main/resources/icons/upload.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/icons/upload_dark.svg b/src/main/resources/icons/upload_dark.svg new file mode 100644 index 0000000..d5cbbee --- /dev/null +++ b/src/main/resources/icons/upload_dark.svg @@ -0,0 +1,4 @@ + + + diff --git a/src/main/resources/messages/messages.properties b/src/main/resources/messages/messages.properties index fb3880d..e32a7d1 100644 --- a/src/main/resources/messages/messages.properties +++ b/src/main/resources/messages/messages.properties @@ -54,6 +54,7 @@ messages.success.download_source=Source downloaded successfully messages.failure.download_sources=Couldn't download any sources messages.confirm.download=Are you sure you want to download translations? +messages.confirm.download_sources=Are you sure you want to download sources? messages.confirm.upload_sources=Are you sure you want to upload sources? messages.confirm.upload_source_file=Are you sure you want to upload this source file? messages.confirm.upload_translations=Are you sure you want to upload translations? @@ -74,4 +75,4 @@ messages.debug.upload_sources.upload=Attempt to upload source file '%s'(source p messages.debug.upload_sources.upload_request=Request body to upload file: %s messages.debug.upload_sources.list_of_patterns=List of source patterns: messages.debug.upload_sources.list_of_patterns_item=\n(source: '%s', translation: %s) -messages.debug.download_sources.file_downloaded=Source file '%s' downloaded \ No newline at end of file +messages.debug.download_sources.file_downloaded=Source file '%s' downloaded diff --git a/src/test/java/com/crowdin/client/CrowdinProjectCacheProviderTest.java b/src/test/java/com/crowdin/client/CrowdinProjectCacheProviderTest.java index 9535d79..c6f6ee3 100644 --- a/src/test/java/com/crowdin/client/CrowdinProjectCacheProviderTest.java +++ b/src/test/java/com/crowdin/client/CrowdinProjectCacheProviderTest.java @@ -1,12 +1,13 @@ package com.crowdin.client; -import com.crowdin.client.CrowdinProjectCacheProvider.CrowdinProjectCache; import com.crowdin.client.projectsgroups.model.Project; import com.crowdin.client.projectsgroups.model.ProjectSettings; import com.crowdin.client.sourcefiles.model.Branch; import com.crowdin.client.sourcefiles.model.Directory; import com.crowdin.client.sourcefiles.model.File; import com.crowdin.client.sourcefiles.model.FileInfo; +import com.crowdin.service.CrowdinProjectCacheProvider; +import com.crowdin.service.CrowdinProjectCacheProvider.CrowdinProjectCache; import com.crowdin.util.LanguageMapping; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,9 +35,11 @@ public class CrowdinProjectCacheProviderTest { + private CrowdinProjectCacheProvider crowdinProjectCacheProvider; + @BeforeEach void setUp() { - CrowdinProjectCacheProvider.reset(); + crowdinProjectCacheProvider = new CrowdinProjectCacheProvider(); } @ParameterizedTest @@ -59,7 +62,7 @@ private static Stream testGetFileInfos() { Map map = new HashMap<>(); map.put("fileinfo", fileInfo); Map map2 = new HashMap<>(); - Map> map1 = new HashMap<>(); + Map> map1 = new HashMap<>(); map1.put(branch, map); return Stream.of(arguments(map1, branch, map), arguments(map1, branch1, map2)); } @@ -67,7 +70,7 @@ private static Stream testGetFileInfos() { @ParameterizedTest @MethodSource public void testGetFiles(final Map> fileInfo, - final Branch branch, final Map expected) { + final Branch branch, final Map expected) { CrowdinProjectCache cache = new CrowdinProjectCache(); cache.setFileInfos(fileInfo); cache.setManagerAccess(true); @@ -85,7 +88,7 @@ private static Stream testGetFiles() { Map map = new HashMap<>(); map.put("fileinfo", file); Map map2 = new HashMap<>(); - Map> map1 = new HashMap<>(); + Map> map1 = new HashMap<>(); map1.put(branch, map); return Stream.of(arguments(map1, branch, map), arguments(map1, branch1, map2)); } @@ -122,7 +125,7 @@ private static Stream testRunTimeException() { @ParameterizedTest @MethodSource public void testAddOutdatedBranch(final String branchName) { - assertDoesNotThrow(() -> CrowdinProjectCacheProvider.outdateBranch(branchName)); + assertDoesNotThrow(() -> crowdinProjectCacheProvider.outdateBranch(branchName)); } private static Stream testAddOutdatedBranch() { @@ -135,7 +138,7 @@ private static Stream testAddOutdatedBranch() { void testSetsValuesForNotYetConfiguredCacheProperties() { CrowdinClient crowdin = new MockCrowdin(1L); //Using a different branch name than what's in MockCrowdin, so that fileinfos doesn't get updated - CrowdinProjectCache cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branchname", false); + CrowdinProjectCache cache = crowdinProjectCacheProvider.getInstance(crowdin, "branchname", false); assertEquals(crowdin.getProject(), cache.getProject()); assertFalse(cache.isManagerAccess()); @@ -155,7 +158,7 @@ void testSetsValuesForNotYetConfiguredCacheProperties() { void testDoesntSetConfigurationForExistingValues() { //Set the initial state of the underlying project cache. Using empty collections to have simpler test data. CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); Project project = new Project(); cache.setProject(project); @@ -175,7 +178,7 @@ void testDoesntSetConfigurationForExistingValues() { cache.setDirs(dirs); CrowdinClient crowdin = new MockCrowdin(1L); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); //Validating 0 collection sizes is feasible because we presume // that the used MockCrowdin instance returns non-empty collections @@ -192,7 +195,7 @@ void testDoesntSetConfigurationForExistingValues() { @Test void testSetsLanguageMappingForManagerAccess() { CrowdinClient crowdin = new MockCrowdin(2L); - CrowdinProjectCache cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); + CrowdinProjectCache cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); assertTrue(cache.isManagerAccess()); assertTrue(cache.getLanguageMapping().containsValue("hu", "placeholder")); @@ -201,12 +204,12 @@ void testSetsLanguageMappingForManagerAccess() { @Test void testSetsBranchesWhenOutdated() { CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); cache.setBranches(new HashMap<>()); - CrowdinProjectCacheProvider.outdateBranch("branch1"); + crowdinProjectCacheProvider.outdateBranch("branch1"); CrowdinClient crowdin = new MockCrowdin(1L); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); assertEquals(crowdin.getBranches(), cache.getBranches()); } @@ -215,7 +218,7 @@ void testSetsBranchesWhenOutdated() { void testDoesntSetConfigurationForNoUpdate() { //Set the initial state of the underlying project cache. Using empty collections to have simpler test data. CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); //Using ProjectSettings because that would set manager access to true, // thus we can validate if it remains false @@ -225,7 +228,7 @@ void testDoesntSetConfigurationForNoUpdate() { cache.setBranches(new HashMap<>()); CrowdinClient crowdin = new MockCrowdin(1L); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch1", false); //Test that no configuration was overwritten with data from the mock Crowdin client assertSame(project, cache.getProject()); @@ -238,7 +241,10 @@ void testDoesntSetConfigurationForNoUpdate() { void testSetsConfigurationForUpdate() { //Set the initial state of the underlying project cache. Using empty collections to have simpler test data. CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + Project prj = new Project(); + prj.setId(1L); + cache.setProject(prj); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); //Using ProjectSettings because that would set manager access to true, // thus we can validate if it remains false. @@ -247,8 +253,8 @@ void testSetsConfigurationForUpdate() { cache.setProjectLanguages(new ArrayList<>()); cache.setBranches(new HashMap<>()); - CrowdinClient crowdin = new MockCrowdin(1L); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch1", true); + CrowdinClient crowdin = new MockCrowdin(prj.getId()); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch1", true); //Test that no configuration was overwritten with data from the mock Crowdin client assertSame(crowdin.getProject(), cache.getProject()); @@ -262,26 +268,26 @@ void testSetsConfigurationForUpdate() { @Test void testSavesBranchWhenFileInfosDoesntContainBranch() { CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); CrowdinClient crowdin = new MockCrowdin(1L); cache.setBranches(crowdin.getBranches()); //FileInfos doesn't contain branch2 cache.setFileInfos(createFileInfos(STANDARD_BRANCH_1, new HashMap<>())); - CrowdinProjectCacheProvider.outdateBranch("branch2"); + crowdinProjectCacheProvider.outdateBranch("branch2"); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); assertTrue(cache.getFileInfos().containsKey(STANDARD_BRANCH_2)); assertTrue(cache.getDirs().containsKey(STANDARD_BRANCH_2)); - assertFalse(CrowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); + assertFalse(crowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); } @Test void testSavesBranchWhenDirsDoesntContainBranch() { CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); CrowdinClient crowdin = new MockCrowdin(1L); cache.setBranches(crowdin.getBranches()); @@ -290,20 +296,20 @@ void testSavesBranchWhenDirsDoesntContainBranch() { cache.setFileInfos(createFileInfos(STANDARD_BRANCH_2, fileInfoValue)); cache.setDirs(createDirs(STANDARD_BRANCH_1, new HashMap<>())); - CrowdinProjectCacheProvider.outdateBranch("branch2"); + crowdinProjectCacheProvider.outdateBranch("branch2"); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); //Validate if fileinfos branch entry value is updated assertNotSame(fileInfoValue, cache.getFileInfos().get(STANDARD_BRANCH_2)); assertTrue(cache.getDirs().containsKey(STANDARD_BRANCH_2)); - assertFalse(CrowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); + assertFalse(crowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); } @Test void testSavesBranchWhenBranchIsOutdated() { CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); CrowdinClient crowdin = new MockCrowdin(1L); cache.setBranches(crowdin.getBranches()); @@ -313,20 +319,20 @@ void testSavesBranchWhenBranchIsOutdated() { HashMap dirsValue = new HashMap<>(); cache.setDirs(createDirs(STANDARD_BRANCH_2, dirsValue)); - CrowdinProjectCacheProvider.outdateBranch("branch2"); + crowdinProjectCacheProvider.outdateBranch("branch2"); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); //Validate if fileinfos and dirs branch entry values are updated assertNotSame(fileInfoValue, cache.getFileInfos().get(STANDARD_BRANCH_2)); assertNotSame(dirsValue, cache.getDirs().get(STANDARD_BRANCH_2)); - assertFalse(CrowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); + assertFalse(crowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); } @Test void testSavesBranchWhenMarkedForUpdate() { CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); CrowdinClient crowdin = new MockCrowdin(1L); cache.setBranches(crowdin.getBranches()); @@ -336,18 +342,18 @@ void testSavesBranchWhenMarkedForUpdate() { HashMap dirsValue = new HashMap<>(); cache.setDirs(createDirs(STANDARD_BRANCH_2, dirsValue)); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch2", true); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch2", true); //Validate if fileinfos and dirs branch entry values are updated assertNotSame(fileInfoValue, cache.getFileInfos().get(STANDARD_BRANCH_2)); assertNotSame(dirsValue, cache.getDirs().get(STANDARD_BRANCH_2)); - assertFalse(CrowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); + assertFalse(crowdinProjectCacheProvider.getOutdatedBranches().contains("branch2")); } @Test void testDoesntSaveBranch() { CrowdinProjectCache cache = new CrowdinProjectCache(); - CrowdinProjectCacheProvider.setCrowdinProjectCache(cache); + crowdinProjectCacheProvider.setCrowdinProjectCache(cache); CrowdinClient crowdin = new MockCrowdin(1L); cache.setBranches(crowdin.getBranches()); @@ -357,7 +363,7 @@ void testDoesntSaveBranch() { HashMap dirsValue = new HashMap<>(); cache.setDirs(createDirs(STANDARD_BRANCH_2, dirsValue)); - cache = CrowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); + cache = crowdinProjectCacheProvider.getInstance(crowdin, "branch2", false); //Validate if fileinfos and dirs branch entry values are updated assertSame(fileInfoValue, cache.getFileInfos().get(STANDARD_BRANCH_2)); diff --git a/src/test/java/com/crowdin/client/MockCrowdin.java b/src/test/java/com/crowdin/client/MockCrowdin.java index 725ff8b..36a73c1 100644 --- a/src/test/java/com/crowdin/client/MockCrowdin.java +++ b/src/test/java/com/crowdin/client/MockCrowdin.java @@ -4,6 +4,8 @@ import com.crowdin.api.model.BranchBuilder; import com.crowdin.api.model.LanguageBuilder; +import com.crowdin.client.bundles.model.Bundle; +import com.crowdin.client.bundles.model.BundleExport; import com.crowdin.client.core.model.PatchRequest; import com.crowdin.client.labels.model.AddLabelRequest; import com.crowdin.client.labels.model.Label; @@ -18,11 +20,14 @@ import com.crowdin.client.sourcefiles.model.FileInfo; import com.crowdin.client.sourcefiles.model.UpdateFileRequest; import com.crowdin.client.sourcestrings.model.SourceString; +import com.crowdin.client.sourcestrings.model.UploadStringsProgress; +import com.crowdin.client.sourcestrings.model.UploadStringsRequest; import com.crowdin.client.translations.model.BuildProjectFileTranslationRequest; import com.crowdin.client.translations.model.BuildProjectTranslationRequest; import com.crowdin.client.translations.model.ProjectBuild; import com.crowdin.client.translations.model.UploadTranslationsRequest; -import com.crowdin.client.translationstatus.model.FileProgress; +import com.crowdin.client.translations.model.UploadTranslationsStringsRequest; +import com.crowdin.client.translationstatus.model.FileBranchProgress; import com.crowdin.client.translationstatus.model.LanguageProgress; import org.jetbrains.annotations.NotNull; @@ -54,6 +59,11 @@ public MockCrowdin(@NotNull Long projectId) { } } + @Override + public Long getProjectId() { + return project != null ? project.getId() : null; + } + @Override public Long addStorage(String fileName, InputStream content) { return null; @@ -80,6 +90,10 @@ public void editSource(Long fileId, List request) { public void uploadTranslation(String languageId, UploadTranslationsRequest request) { } + @Override + public void uploadStringsTranslation(String languageId, UploadTranslationsStringsRequest request) { + } + @Override public Directory addDirectory(AddDirectoryRequest request) { return null; @@ -95,6 +109,16 @@ public List extractProjectLanguages(Project crowdinProject) { return Collections.singletonList(LanguageBuilder.DEU.build()); } + @Override + public UploadStringsProgress uploadStrings(UploadStringsRequest request) { + return null; + } + + @Override + public UploadStringsProgress checkUploadStringsStatus(String id) { + return null; + } + @Override public ProjectBuild startBuildingTranslation(BuildProjectTranslationRequest request) { return null; @@ -110,6 +134,21 @@ public URL downloadProjectTranslations(Long buildId) { return null; } + @Override + public BundleExport startBuildingBundle(Long bundleId) { + return null; + } + + @Override + public BundleExport checkBundleBuildingStatus(Long buildId, String exportId) { + return null; + } + + @Override + public URL downloadBundle(Long buildId, String exportId) { + return null; + } + @Override public URL downloadFileTranslation(Long fileId, BuildProjectFileTranslationRequest request) { return null; @@ -163,7 +202,7 @@ public List getProjectProgress() { } @Override - public List getLanguageProgress(String languageId) { + public List getLanguageProgress(String languageId) { return null; } @@ -176,4 +215,14 @@ public List