├── .gitignore ├── .idea ├── .gitignore ├── artifacts │ ├── super_github_v1_0_0_alpha01.xml │ ├── super_github_v1_0_3.xml │ └── super_github_v1_0_4.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── jarRepositories.xml ├── libraries-with-intellij-classes.xml ├── markdown-navigator-enh.xml ├── misc.xml ├── modules │ ├── super-github.main.iml │ └── super-github.test.iml ├── runConfigurations.xml ├── super-github.iml └── vcs.xml ├── README.md ├── assets └── summary.svg ├── build.gradle ├── cover.jpeg ├── extras ├── auto_comment.gif └── profile-summary-demo.gif ├── gpm.json ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── logo.png ├── settings.gradle └── src ├── main ├── kotlin │ └── com │ │ └── theapache64 │ │ └── supergithub │ │ ├── data │ │ ├── remote │ │ │ └── repos │ │ │ │ └── GetRepoResponse.kt │ │ └── repositories │ │ │ └── GitHubRepo.kt │ │ ├── features │ │ ├── BaseFeature.kt │ │ ├── FoldableContent.kt │ │ ├── ProfileSummary.kt │ │ ├── RepoCreatedAt.kt │ │ └── ReviewComment.kt │ │ ├── main.kt │ │ └── utils │ │ ├── PathUtils.kt │ │ ├── StringUtils.kt │ │ ├── Svgs.kt │ │ └── TimeUtils.kt └── resources │ ├── icons │ ├── icon128.png │ ├── icon16.png │ └── icon48.png │ └── manifest.json └── test └── kotlin └── com └── theapache64 └── supergithub ├── data └── repositories │ └── GitHubRepoTest.kt └── utils ├── PathUtilsTest.kt ├── StringUtilsTest.kt └── TimeUtilsTest.kt /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/kotlin,intellij,gradle 3 | # Edit at https://www.gitignore.io/?templates=kotlin,intellij,gradle 4 | 5 | ### Intellij ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/modules.xml 37 | # .idea/*.iml 38 | # .idea/modules 39 | # *.iml 40 | # *.ipr 41 | 42 | # CMake 43 | cmake-build-*/ 44 | 45 | # Mongo Explorer plugin 46 | .idea/**/mongoSettings.xml 47 | 48 | # File-based project format 49 | *.iws 50 | 51 | # IntelliJ 52 | out/ 53 | 54 | # mpeltonen/sbt-idea plugin 55 | .idea_modules/ 56 | 57 | # JIRA plugin 58 | atlassian-ide-plugin.xml 59 | 60 | # Cursive Clojure plugin 61 | .idea/replstate.xml 62 | 63 | # Crashlytics plugin (for Android Studio and IntelliJ) 64 | com_crashlytics_export_strings.xml 65 | crashlytics.properties 66 | crashlytics-build.properties 67 | fabric.properties 68 | 69 | # Editor-based Rest Client 70 | .idea/httpRequests 71 | 72 | # Android studio 3.1+ serialized cache file 73 | .idea/caches/build_file_checksums.ser 74 | 75 | ### Intellij Patch ### 76 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 77 | 78 | # *.iml 79 | # modules.xml 80 | # .idea/misc.xml 81 | # *.ipr 82 | 83 | # Sonarlint plugin 84 | .idea/**/sonarlint/ 85 | 86 | # SonarQube Plugin 87 | .idea/**/sonarIssues.xml 88 | 89 | # Markdown Navigator plugin 90 | .idea/**/markdown-navigator.xml 91 | .idea/**/markdown-navigator/ 92 | 93 | ### Kotlin ### 94 | # Compiled class file 95 | *.class 96 | 97 | # Log file 98 | *.log 99 | 100 | # BlueJ files 101 | *.ctxt 102 | 103 | # Mobile Tools for Java (J2ME) 104 | .mtj.tmp/ 105 | 106 | # Package Files # 107 | *.jar 108 | *.war 109 | *.nar 110 | *.ear 111 | *.zip 112 | *.tar.gz 113 | *.rar 114 | 115 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 116 | hs_err_pid* 117 | 118 | ### Gradle ### 119 | .gradle 120 | build/ 121 | 122 | # Ignore Gradle GUI config 123 | gradle-app.setting 124 | 125 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 126 | !gradle-wrapper.jar 127 | 128 | # Cache of project 129 | .gradletasknamecache 130 | 131 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 132 | # gradle/wrapper/gradle-wrapper.properties 133 | 134 | ### Gradle Patch ### 135 | **/build/ 136 | 137 | # End of https://www.gitignore.io/api/kotlin,intellij,gradle 138 | SecretConstants.kt 139 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # Datasource local storage ignored files 5 | /dataSources/ 6 | /dataSources.local.xml 7 | # Editor-based HTTP Client requests 8 | /httpRequests/ 9 | -------------------------------------------------------------------------------- /.idea/artifacts/super_github_v1_0_0_alpha01.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/super_github_v1_0_3.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/artifacts/super_github_v1_0_4.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | $PROJECT_DIR$/build/libs 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/libraries-with-intellij-classes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 64 | 65 | -------------------------------------------------------------------------------- /.idea/markdown-navigator-enh.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules/super-github.main.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | COMPILATION_AND_SOURCE_SET_HOLDER 7 | 8 | 9 | 21 | 24 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.idea/modules/super-github.test.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | super-github.main 8 | 9 | COMPILATION_AND_SOURCE_SET_HOLDER 10 | 11 | browserTest|super-github:test|js 12 | 13 | 14 | 15 | 27 | 30 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/super-github.iml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![](cover.jpeg) 2 | 3 | # :rocket: super-github 4 | 5 | Enhance your GitHub experience 6 | 7 | 8 | ## Install 9 | 10 | Open an issue, and I'll publish it in Chrome store 😉 11 | 12 | ## :paintbrush: Features 13 | 14 | ### :baby_bottle: Repo Birth Date 15 | Ever wondered when's your favorite repo born ? 16 | 17 | ![](https://i.imgur.com/D0dVc4Z.png) 18 | 19 | The emoji specifies project started time. 20 | 21 | If the time 22 | 23 | - between 12AM and 5AM : 🌙 24 | - between 5AM and 10AM : 🌞 25 | - between 10AM and 3PM : ☀️ 26 | - between 3PM and 7PM : 🌥 27 | - after 7PM : 🌓 28 | 29 | ### :bar_chart: Profile Summary 30 | 31 | ![](extras/profile-summary-demo.gif) 32 | 33 | Click on the chart icon to view profile summary. 34 | 35 | ![](https://i.imgur.com/KfRyc7y.png) 36 | 37 | It also supports organizations too. 38 | 39 | ![](https://i.imgur.com/cpkP1OY.png) 40 | 41 | Thanks to [@tipsy](https://github.com/tipsy/profile-summary-for-github) for his hard work. :hugs: 42 | 43 | ### 👌 Auto Comment (PR Approval) 44 | 45 | ![](extras/auto_comment.gif) 46 | 47 | Every time you click on the `Approve` radio button, your comment box will be filled with a random LGTM message. 48 | 49 | ### 🙏 Foldable Code 50 | 51 | Helps you write foldable code blocks when you paste code/stacktrace into a comment body. 52 | Watch the demo [here](https://twitter.com/theapache64/status/1365004116003446793) 53 | 54 | ## :ballot_box_with_check: TODO 55 | 56 | - [x] Repo birth date 57 | - [x] Profile summary 58 | - [x] Random `LGTM` message for PR approval 59 | - [x] Foldable code 60 | - [ ] Do you have a feature in mind? Let's discuss it [here](https://github.com/theapache64/super-github/issues/new?labels=enhancement) 61 | 62 | ## :writing_hand: Author 63 | 64 | - theapache64 -------------------------------------------------------------------------------- /assets/summary.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 8 | 10 | 12 | 14 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.js' version '1.5.0' 3 | } 4 | 5 | group 'com.theapache64' 6 | version 'v1.0.4' 7 | 8 | repositories { 9 | mavenCentral() 10 | } 11 | 12 | dependencies { 13 | implementation "org.jetbrains.kotlin:kotlin-stdlib-js" 14 | testImplementation "org.jetbrains.kotlin:kotlin-test-js" 15 | 16 | // Kotlinx Coroutines Core:Coroutines support libraries for Kotlin 17 | implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.3.7' 18 | } 19 | 20 | kotlin.target.browser {} -------------------------------------------------------------------------------- /cover.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/cover.jpeg -------------------------------------------------------------------------------- /extras/auto_comment.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/extras/auto_comment.gif -------------------------------------------------------------------------------- /extras/profile-summary-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/extras/profile-summary-demo.gif -------------------------------------------------------------------------------- /gpm.json: -------------------------------------------------------------------------------- 1 | { 2 | "added": [ 3 | { 4 | "id": 1, 5 | "type": "testImplementation", 6 | "installed_name": "junit", 7 | "gpm_dep": { 8 | "artifact_id": "junit", 9 | "default_type": "implementation", 10 | "docs": "https://mvnrepository.com/artifact/junit/junit", 11 | "get_from": "Central", 12 | "group_id": "junit", 13 | "name": "JUnit", 14 | "description": "JUnit is a unit testing framework for Java, created by Erich Gamma and Kent Beck." 15 | } 16 | }, 17 | { 18 | "id": 2, 19 | "type": "implementation", 20 | "installed_name": "moshi", 21 | "gpm_dep": { 22 | "artifact_id": "moshi", 23 | "default_type": "implementation", 24 | "docs": "https://mvnrepository.com/artifact/com.squareup.moshi/moshi", 25 | "get_from": "Central", 26 | "group_id": "com.squareup.moshi", 27 | "name": "Moshi", 28 | "description": "Moshi" 29 | } 30 | }, 31 | { 32 | "id": 3, 33 | "type": "implementation", 34 | "installed_name": "coroutines", 35 | "gpm_dep": { 36 | "artifact_id": "kotlinx-coroutines-core-common", 37 | "default_type": "implementation", 38 | "docs": "https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-common", 39 | "get_from": "Central", 40 | "group_id": "org.jetbrains.kotlinx", 41 | "name": "Kotlinx Coroutines Core", 42 | "description": "Coroutines support libraries for Kotlin" 43 | } 44 | }, 45 | { 46 | "id": 4, 47 | "type": "implementation", 48 | "installed_name": "coroutines js", 49 | "gpm_dep": { 50 | "artifact_id": "kotlinx-coroutines-core-js", 51 | "default_type": "implementation", 52 | "docs": "https://mvnrepository.com/artifact/org.jetbrains.kotlinx/kotlinx-coroutines-core-js", 53 | "get_from": "Central", 54 | "group_id": "org.jetbrains.kotlinx", 55 | "name": "Kotlinx Coroutines Core", 56 | "description": "Coroutines support libraries for Kotlin" 57 | } 58 | } 59 | ] 60 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu May 21 17:27:54 IST 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.1-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | # Determine the Java command to use to start the JVM. 86 | if [ -n "$JAVA_HOME" ] ; then 87 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 88 | # IBM's JDK on AIX uses strange locations for the executables 89 | JAVACMD="$JAVA_HOME/jre/sh/java" 90 | else 91 | JAVACMD="$JAVA_HOME/bin/java" 92 | fi 93 | if [ ! -x "$JAVACMD" ] ; then 94 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 95 | 96 | Please set the JAVA_HOME variable in your environment to match the 97 | location of your Java installation." 98 | fi 99 | else 100 | JAVACMD="java" 101 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 102 | 103 | Please set the JAVA_HOME variable in your environment to match the 104 | location of your Java installation." 105 | fi 106 | 107 | # Increase the maximum file descriptors if we can. 108 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 109 | MAX_FD_LIMIT=`ulimit -H -n` 110 | if [ $? -eq 0 ] ; then 111 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 112 | MAX_FD="$MAX_FD_LIMIT" 113 | fi 114 | ulimit -n $MAX_FD 115 | if [ $? -ne 0 ] ; then 116 | warn "Could not set maximum file descriptor limit: $MAX_FD" 117 | fi 118 | else 119 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 120 | fi 121 | fi 122 | 123 | # For Darwin, add options to specify how the application appears in the dock 124 | if $darwin; then 125 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 126 | fi 127 | 128 | # For Cygwin or MSYS, switch paths to Windows format before running java 129 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 130 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 131 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 132 | JAVACMD=`cygpath --unix "$JAVACMD"` 133 | 134 | # We build the pattern for arguments to be converted via cygpath 135 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 136 | SEP="" 137 | for dir in $ROOTDIRSRAW ; do 138 | ROOTDIRS="$ROOTDIRS$SEP$dir" 139 | SEP="|" 140 | done 141 | OURCYGPATTERN="(^($ROOTDIRS))" 142 | # Add a user-defined pattern to the cygpath arguments 143 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 144 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 145 | fi 146 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 147 | i=0 148 | for arg in "$@" ; do 149 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 150 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 151 | 152 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 153 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 154 | else 155 | eval `echo args$i`="\"$arg\"" 156 | fi 157 | i=`expr $i + 1` 158 | done 159 | case $i in 160 | 0) set -- ;; 161 | 1) set -- "$args0" ;; 162 | 2) set -- "$args0" "$args1" ;; 163 | 3) set -- "$args0" "$args1" "$args2" ;; 164 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 165 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 166 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 167 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 168 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 169 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 170 | esac 171 | fi 172 | 173 | # Escape application args 174 | save () { 175 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 176 | echo " " 177 | } 178 | APP_ARGS=`save "$@"` 179 | 180 | # Collect all arguments for the java command, following the shell quoting and substitution rules 181 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 182 | 183 | exec "$JAVACMD" "$@" 184 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 33 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 34 | 35 | @rem Find java.exe 36 | if defined JAVA_HOME goto findJavaFromJavaHome 37 | 38 | set JAVA_EXE=java.exe 39 | %JAVA_EXE% -version >NUL 2>&1 40 | if "%ERRORLEVEL%" == "0" goto init 41 | 42 | echo. 43 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 44 | echo. 45 | echo Please set the JAVA_HOME variable in your environment to match the 46 | echo location of your Java installation. 47 | 48 | goto fail 49 | 50 | :findJavaFromJavaHome 51 | set JAVA_HOME=%JAVA_HOME:"=% 52 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 53 | 54 | if exist "%JAVA_EXE%" goto init 55 | 56 | echo. 57 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 58 | echo. 59 | echo Please set the JAVA_HOME variable in your environment to match the 60 | echo location of your Java installation. 61 | 62 | goto fail 63 | 64 | :init 65 | @rem Get command-line arguments, handling Windows variants 66 | 67 | if not "%OS%" == "Windows_NT" goto win9xME_args 68 | 69 | :win9xME_args 70 | @rem Slurp the command line arguments. 71 | set CMD_LINE_ARGS= 72 | set _SKIP=2 73 | 74 | :win9xME_args_slurp 75 | if "x%~1" == "x" goto execute 76 | 77 | set CMD_LINE_ARGS=%* 78 | 79 | :execute 80 | @rem Setup the command line 81 | 82 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 83 | 84 | @rem Execute Gradle 85 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 86 | 87 | :end 88 | @rem End local scope for the variables with windows NT shell 89 | if "%ERRORLEVEL%"=="0" goto mainEnd 90 | 91 | :fail 92 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 93 | rem the _cmd.exe /c_ return code! 94 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 95 | exit /b 1 96 | 97 | :mainEnd 98 | if "%OS%"=="Windows_NT" endlocal 99 | 100 | :omega 101 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/logo.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'super-github' 2 | 3 | -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/data/remote/repos/GetRepoResponse.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.data.remote.repos 2 | 3 | data class GetRepoResponse( 4 | val created_at: String? // 2019-02-11T04:03:06Z 5 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/data/repositories/GitHubRepo.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.data.repositories 2 | 3 | import com.theapache64.supergithub.data.remote.repos.GetRepoResponse 4 | import kotlinx.coroutines.await 5 | import kotlinx.browser.window 6 | 7 | object GitHubRepo { 8 | 9 | private const val BASE_URL = "https://api.github.com" 10 | 11 | suspend fun getRepo(repoPath: String): GetRepoResponse { 12 | val url = "$BASE_URL/repos/$repoPath" 13 | return window.fetch(url) 14 | .await() 15 | .json() 16 | .await() 17 | .unsafeCast() 18 | } 19 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/features/BaseFeature.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.features 2 | 3 | interface BaseFeature { 4 | suspend fun onGitHubPageLoaded() 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/features/FoldableContent.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.features 2 | 3 | import com.theapache64.supergithub.utils.foldableSvg 4 | import kotlinx.browser.document 5 | import kotlinx.browser.window 6 | import org.w3c.dom.HTMLTextAreaElement 7 | import org.w3c.dom.events.Event 8 | import org.w3c.dom.events.EventListener 9 | 10 | class FoldableContent : BaseFeature { 11 | companion object { 12 | private val COMMENTABLE_PAGE_URL_REGEX = 13 | "https:\\/\\/github\\.com\\/.+?\\/.+?\\/(issues|pull|issues)\\/(new|\\d+.*)".toRegex() 14 | 15 | private const val TARGET_CONTAINER_EXISTING_ISSUE_SELECTOR = 16 | "#issuecomment-new > div.border-0.border-md.timeline-comment.timeline-comment--caret > form > fieldset > tab-container > div.comment-form-head.tabnav.d-flex.flex-justify-between.mb-2.p-0.tabnav--responsive.flex-column.border-bottom-0.mb-0.mb-lg-2.flex-items-stretch.border-lg-bottom.color-border-primary.flex-lg-items-center.flex-lg-row > markdown-toolbar > div.flex-nowrap.d-none.d-md-inline-block.mr-3" 17 | 18 | private const val TARGET_CONTAINER_NEW_ISSUE_SELECTOR = 19 | "#new_issue > div > div > div.flex-shrink-0.col-12.col-md-9.mb-4.mb-md-0 > div > div.timeline-comment.color-bg-canvas.hx_comment-box--tip > div > tab-container > div.comment-form-head.tabnav.d-flex.flex-justify-between.mb-2.p-0.tabnav--responsive.flex-column.border-bottom-0.mb-0.mb-lg-2.flex-items-stretch.border-lg-bottom.color-border-primary.flex-lg-items-center.flex-lg-row > markdown-toolbar > div.flex-nowrap.d-none.d-md-inline-block.mr-3" 20 | private val MD_FOLDABLE by lazy { 21 | """ 22 | 23 | $foldableSvg 24 | 25 | """ 26 | } 27 | 28 | private val FOLDABLE_TEMPLATE = """ 29 |
30 | SUMMARY_GOES_HERE 31 | 32 | ``` 33 | PASTE_CONTENT_HERE 34 | ``` 35 |
36 | """.trimIndent() 37 | 38 | private const val TEXTAREA_EXISTING_SELECTOR = "#new_comment_field" 39 | private const val TEXTAREA_NEW_SELECTOR = "#issue_body" 40 | 41 | 42 | } 43 | 44 | override suspend fun onGitHubPageLoaded() { 45 | val url = window.location.href 46 | val isCommentablePage = COMMENTABLE_PAGE_URL_REGEX.matches(url) 47 | 48 | 49 | println("Is commentable ('$url') : $isCommentablePage") 50 | 51 | if (isCommentablePage) { 52 | document 53 | .querySelector(TARGET_CONTAINER_EXISTING_ISSUE_SELECTOR) 54 | .let { container -> 55 | container ?: document.querySelector(TARGET_CONTAINER_NEW_ISSUE_SELECTOR) 56 | } 57 | ?.let { container -> 58 | container.innerHTML = MD_FOLDABLE + container.innerHTML 59 | 60 | // Add click listener 61 | document.querySelector("md-foldable") 62 | ?.addEventListener( 63 | "click", 64 | object : EventListener { 65 | override fun handleEvent(event: Event) { 66 | // add content 67 | (document.querySelector(TEXTAREA_EXISTING_SELECTOR) ?: document.querySelector(TEXTAREA_NEW_SELECTOR)) 68 | ?.let { textArea -> 69 | textArea as HTMLTextAreaElement 70 | val startPos = textArea.selectionStart 71 | val endPos = textArea.selectionEnd 72 | require(startPos != null) { "startPos is null" } 73 | require(endPos != null) { "startPos is null" } 74 | val currentValue = textArea.value 75 | textArea.value = currentValue.substring( 76 | 0, 77 | startPos 78 | ) + FOLDABLE_TEMPLATE + currentValue.substring(endPos, currentValue.length) 79 | } 80 | } 81 | } 82 | ) ?: println("Couldn't find foldable") 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/features/ProfileSummary.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.features 2 | 3 | import com.theapache64.supergithub.utils.PathUtils 4 | import kotlinx.browser.document 5 | import kotlinx.browser.window 6 | 7 | class ProfileSummary : BaseFeature { 8 | companion object{ 9 | private const val PROFILE_NAME_SELECTOR = "#js-pjax-container > div.container-xl.px-3.px-md-4.px-lg-5 > div > div.Layout-sidebar > div > div.js-profile-editable-replace > div.clearfix.d-flex.d-md-block.flex-items-center.mb-4.mb-md-0 > div.vcard-names-container.float-left.js-profile-editable-names.col-12.py-3.js-sticky.js-user-profile-sticky-fields > h1 > span.p-name.vcard-fullname.d-block.overflow-hidden" 10 | private const val ORG_SELECTOR = "#org-profile-repositories > div > div.my-3 > div > form > div" 11 | } 12 | override suspend fun onGitHubPageLoaded() { 13 | val url = window.location.href 14 | if (PathUtils.isProfilePage(url)) { 15 | 16 | val profileNameElement = document.querySelector(PROFILE_NAME_SELECTOR) 17 | 18 | val username = PathUtils.getUsername(url) 19 | if (username == null) { 20 | println("Couldn't find username from $url") 21 | return 22 | } 23 | 24 | println("Username is $username") 25 | 26 | if (profileNameElement != null) { 27 | println("Okay") 28 | val summaryButton = getSummaryButton(username) 29 | profileNameElement.innerHTML += summaryButton 30 | 31 | } else { 32 | 33 | // Maybe organization 34 | val element = 35 | document.querySelector(ORG_SELECTOR) 36 | if (element != null) { 37 | val html = getSummaryButton(username, "margin-top: 4px; margin-right:10px;") 38 | element.innerHTML = "$html ${element.innerHTML}" 39 | } else { 40 | println("Query selector failed") 41 | } 42 | } 43 | 44 | } else { 45 | println("$url is not profile page") 46 | } 47 | } 48 | 49 | private fun getSummaryButton(username: String, imageStyle: String = ""): String { 50 | return """ 51 | 52 | 53 | 54 | """.trimIndent() 55 | } 56 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/features/RepoCreatedAt.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.features 2 | 3 | import com.theapache64.supergithub.data.repositories.GitHubRepo 4 | import com.theapache64.supergithub.utils.PathUtils 5 | import com.theapache64.supergithub.utils.TimeUtils 6 | import kotlinx.browser.document 7 | import kotlinx.browser.localStorage 8 | import kotlinx.browser.window 9 | import kotlin.js.Date 10 | 11 | class RepoCreatedAt : BaseFeature { 12 | 13 | override suspend fun onGitHubPageLoaded() { 14 | println("Finding repo created date") 15 | 16 | 17 | val repoPath = PathUtils.getRepoPath(window.location.href) 18 | if (repoPath != null) { 19 | val createdAtKey = "sg_$repoPath" 20 | val repoCreatedAt = localStorage.getItem(createdAtKey) ?: GitHubRepo.getRepo(repoPath).created_at 21 | 22 | if (repoCreatedAt != null) { 23 | 24 | localStorage.setItem(createdAtKey, repoCreatedAt) 25 | 26 | val repoCreatedDate = Date(repoCreatedAt) 27 | val timeEmoji = getTimeEmoji(repoCreatedDate) 28 | 29 | val timesAgo = TimeUtils.getRelativeTime(Date(), repoCreatedDate) 30 | if (timesAgo != null) { 31 | 32 | val div = 33 | document.querySelector("#repository-container-header > div.d-flex.mb-3.px-3.px-md-4.px-lg-5 > div") 34 | //document.querySelector("body > div.application-main > div > main > div.pagehead.repohead.hx_repohead.readability-menu.bg-gray-light.pb-0.pt-0.pt-lg-3 > div.d-flex.mb-4.p-responsive.d-none.d-lg-flex > div > h1") 35 | 36 | val hasBirthDate = div?.querySelector("span#sg_created_at") != null 37 | if (div != null && !hasBirthDate) { 38 | 39 | println("Added created date") 40 | 41 | div.innerHTML += """ 42 | $timeEmoji 43 | 44 | Born $timesAgo 45 | 46 | """.trimIndent() 47 | 48 | } else { 49 | println("Selector algorithm failed") 50 | } 51 | } else { 52 | println("Invalid creation date $repoCreatedDate") 53 | } 54 | } else { 55 | println("Invalid rep $repoPath") 56 | } 57 | } else { 58 | println("It's not a repo") 59 | } 60 | } 61 | 62 | private fun getTimeEmoji(repoCreatedAt: Date): String { 63 | 64 | val time: Pair = when (repoCreatedAt.getHours()) { 65 | in 0..5 -> Pair("Midnight", "1f319") // "🌙" 66 | in 5..10 -> Pair("Morning", "1f31e") // "🌞" 67 | in 10..15 -> Pair("Noon", "2600") // "☀️" 68 | in 15..19 -> Pair("Evening", "1f325") // "🌥" 69 | else -> Pair("Night", "1f313") // "🌓" 70 | } 71 | 72 | return """ 73 | 74 | """.trimIndent() 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/features/ReviewComment.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.features 2 | 3 | import kotlinx.browser.document 4 | import kotlinx.browser.window 5 | import org.w3c.dom.HTMLTextAreaElement 6 | import org.w3c.dom.events.Event 7 | import org.w3c.dom.events.EventListener 8 | 9 | class ReviewComment : BaseFeature { 10 | 11 | companion object { 12 | private val PR_REVIEW_PAGE_URL_REGEX = 13 | "https:\\/\\/github\\.com\\/.+?\\/.+?\\/pull\\/\\d+?\\/files.*".toRegex() 14 | 15 | private val reviewActions = mapOf( 16 | "approve" to listOf( 17 | "LGTM 👌. Feel free to merge", 18 | "Wow. Good job mate 👍. Feel free to merge", 19 | "Cool. LGTM. Please merge 🚀", 20 | "Hell of a job 👍. LGTM. Please merge" 21 | ), 22 | "comment" to listOf( 23 | "I am comment value 1", 24 | "I am comment value 2", 25 | ) 26 | ) 27 | 28 | private val allMessages = reviewActions.values.flatten() 29 | 30 | private const val RADIO_SELECTOR = 31 | "#review-changes-modal > div > div > div > form > div > label > input[type=radio]" 32 | } 33 | 34 | override suspend fun onGitHubPageLoaded() { 35 | val url = window.location.href 36 | val isPrReviewUrl = PR_REVIEW_PAGE_URL_REGEX.matches(url) 37 | 38 | if (isPrReviewUrl) { 39 | 40 | for (action in reviewActions.keys) { 41 | 42 | val messages = reviewActions[action] ?: error("TSH: Couldn't find key $action") 43 | 44 | document 45 | .querySelector("$RADIO_SELECTOR[value=$action]") 46 | ?.let { element -> 47 | element.addEventListener("click", object : EventListener { 48 | override fun handleEvent(event: Event) { 49 | document.querySelector("#pull_request_review_body")?.let { commentTextArea -> 50 | val currentComment = (commentTextArea as HTMLTextAreaElement).value 51 | println("Current comment is '$currentComment'") 52 | // Either comment should be empty or any of the messages should be the content 53 | if (currentComment.isBlank() || allMessages.contains(currentComment)) { 54 | val newMessage = messages.random() 55 | console.log("Changing review message to $newMessage") 56 | commentTextArea.value = newMessage 57 | } 58 | } 59 | } 60 | }) 61 | } ?: console.error("Uhh ho! failed to find the element") 62 | } 63 | 64 | 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/main.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub 2 | 3 | import com.theapache64.supergithub.features.FoldableContent 4 | import com.theapache64.supergithub.features.ReviewComment 5 | import com.theapache64.supergithub.features.ProfileSummary 6 | import com.theapache64.supergithub.features.RepoCreatedAt 7 | import com.theapache64.supergithub.utils.StringUtils 8 | import kotlinx.coroutines.GlobalScope 9 | import kotlinx.coroutines.delay 10 | import kotlinx.coroutines.launch 11 | import org.w3c.dom.HTMLSpanElement 12 | import kotlinx.browser.document 13 | import kotlinx.browser.window 14 | 15 | 16 | var prevUrl = window.location.toString() 17 | 18 | suspend fun main() { 19 | 20 | runSuperGithub() 21 | 22 | watchForUrlChange { 23 | println("URL changed, runSuperGithub") 24 | runSuperGithub() 25 | } 26 | } 27 | 28 | suspend fun runSuperGithub() { 29 | 30 | val features = listOf( 31 | RepoCreatedAt(), 32 | ProfileSummary(), 33 | ReviewComment(), 34 | FoldableContent() 35 | ) 36 | 37 | for (feature in features) { 38 | println("--------------------------") 39 | println("Executing feature -> ${feature::class.simpleName}...") 40 | feature.onGitHubPageLoaded() 41 | } 42 | } 43 | 44 | private fun watchForUrlChange(callback: suspend () -> Unit) { 45 | window.setInterval({ 46 | val newUrl = window.location.toString() 47 | if (newUrl != prevUrl) { 48 | prevUrl = newUrl 49 | println("URL changed!!") 50 | 51 | // Waiting for loading to be finished 52 | val progressBar = 53 | document.querySelector("body > div.position-relative.js-header-wrapper > span > span") as HTMLSpanElement 54 | 55 | GlobalScope.launch { 56 | var curProgress = 0 57 | while (curProgress < 100) { 58 | val progress = StringUtils.parseInt(progressBar.style.width) 59 | if (progress != null) { 60 | curProgress = progress 61 | } else { 62 | break 63 | } 64 | delay(200) 65 | } 66 | println("Hehe, progress is $curProgress") 67 | if (curProgress >= 100) { 68 | callback() 69 | } else { 70 | println("Page not loaded fully") 71 | } 72 | } 73 | } 74 | }, 500) 75 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/utils/PathUtils.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.utils 2 | 3 | object PathUtils { 4 | 5 | private val PROFILE_REGEX = "^https:\\/\\/github\\.com\\/[\\w-]+\\/?\$".toRegex() 6 | 7 | fun getRepoPath(fullUrl: String): String? { 8 | val slashSplit = fullUrl.split("/") 9 | if (slashSplit.size >= 5) { 10 | return "${slashSplit[3]}/${slashSplit[4]}".trim() 11 | } 12 | return null 13 | } 14 | 15 | fun isProfilePage(url: String): Boolean { 16 | return url.matches(PROFILE_REGEX) 17 | } 18 | 19 | fun getUsername(fullUrl: String): String? { 20 | val slashSplit = fullUrl.split("/") 21 | if (slashSplit.size >= 4) { 22 | return slashSplit[3].trim() 23 | } 24 | return null 25 | } 26 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/utils/StringUtils.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.utils 2 | 3 | object StringUtils { 4 | private val nonDigits = "\\D+".toRegex() 5 | fun parseInt(input: String): Int? { 6 | return try { 7 | input.replace(nonDigits, "").trim().toInt() 8 | }catch (e: NumberFormatException){ 9 | null 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/utils/Svgs.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.utils 2 | 3 | val foldableSvg by lazy { 4 | """ 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | """.trimIndent() 58 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/theapache64/supergithub/utils/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.utils 2 | 3 | import kotlin.js.Date 4 | import kotlin.math.floor 5 | import kotlin.math.round 6 | 7 | object TimeUtils { 8 | 9 | private const val SECOND_MILLIS = 1000L 10 | private const val MINUTE_MILLIS = 60 * SECOND_MILLIS 11 | private const val HOUR_MILLIS = 60 * MINUTE_MILLIS 12 | private const val DAY_MILLIS = 24 * HOUR_MILLIS 13 | private const val MONTH_MILLIS = 30 * DAY_MILLIS 14 | private const val YEAR_MILLIS = 12 * MONTH_MILLIS 15 | 16 | 17 | fun getRelativeTime(presentDate: Date, pastDate: Date): String? { 18 | 19 | val past = pastDate.getTime() 20 | val present = presentDate.getTime() 21 | val diff = present - past 22 | 23 | if (diff < 0) { 24 | return null 25 | } 26 | 27 | return when { 28 | 29 | diff < MINUTE_MILLIS -> { 30 | "just now"; 31 | } 32 | 33 | diff < 2 * MINUTE_MILLIS -> { 34 | "a minute ago"; 35 | } 36 | 37 | diff < 60 * MINUTE_MILLIS -> { 38 | "${floor(diff / MINUTE_MILLIS)} minutes ago"; 39 | } 40 | 41 | diff < 120 * MINUTE_MILLIS -> { 42 | "an hour ago"; 43 | } 44 | 45 | diff < 24 * HOUR_MILLIS -> { 46 | "${floor(diff / HOUR_MILLIS)} hours ago"; 47 | } 48 | 49 | diff < 48 * HOUR_MILLIS -> { 50 | "yesterday"; 51 | } 52 | 53 | diff < 30 * DAY_MILLIS -> { 54 | "${floor(diff / DAY_MILLIS)} days ago" 55 | } 56 | 57 | diff < 2 * MONTH_MILLIS -> { 58 | "a month ago" 59 | } 60 | 61 | 62 | diff < 12 * MONTH_MILLIS -> { 63 | "${floor(diff / MONTH_MILLIS)} months ago" 64 | } 65 | 66 | diff < 2 * YEAR_MILLIS -> { 67 | "a year ago" 68 | } 69 | 70 | else -> { 71 | "${round(diff / YEAR_MILLIS)} years ago"; 72 | } 73 | } 74 | } 75 | 76 | } -------------------------------------------------------------------------------- /src/main/resources/icons/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/src/main/resources/icons/icon128.png -------------------------------------------------------------------------------- /src/main/resources/icons/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/src/main/resources/icons/icon16.png -------------------------------------------------------------------------------- /src/main/resources/icons/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theapache64/super-github/3460987708952b27eb38df1aed829f65547b4824/src/main/resources/icons/icon48.png -------------------------------------------------------------------------------- /src/main/resources/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Super Github", 3 | "version": "1.0.4", 4 | "description": "Enhance your GitHub experience", 5 | "manifest_version": 2, 6 | "icons": { 7 | "16": "icons/icon16.png", 8 | "48": "icons/icon48.png", 9 | "128": "icons/icon128.png" 10 | }, 11 | "permissions": [ 12 | ], 13 | "content_scripts": [ 14 | { 15 | "matches": [ 16 | "https://github.com/*" 17 | ], 18 | "js": [ 19 | "super-github.js" 20 | ], 21 | "runat": "document_end" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/supergithub/data/repositories/GitHubRepoTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.data.repositories 2 | 3 | import kotlinx.coroutines.GlobalScope 4 | import kotlinx.coroutines.promise 5 | import kotlin.test.Test 6 | import kotlin.test.assertEquals 7 | import kotlin.test.assertNull 8 | 9 | fun runTest(block: suspend () -> Unit): dynamic = GlobalScope.promise { block() } 10 | 11 | class GitHubRepoTest { 12 | 13 | @Test 14 | fun getCreateDateForValidRepo() = runTest { 15 | val repoPath = "theapache64/faded" 16 | val repo = GitHubRepo.getRepo(repoPath) 17 | assertEquals("2019-02-11T04:03:06Z", repo.created_at) 18 | } 19 | 20 | @Test 21 | fun getCreateDateForInvalidRepo() = runTest { 22 | val repoPath = "" 23 | val repo = GitHubRepo.getRepo(repoPath) 24 | assertNull(repo.created_at) 25 | } 26 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/supergithub/utils/PathUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.utils 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | import kotlin.test.assertNull 6 | 7 | 8 | internal class PathUtilsTest { 9 | @Test 10 | fun getRepoPathValid() { 11 | val url = "https://github.com/theapache64/faded" 12 | val repoName = PathUtils.getRepoPath(url) 13 | assertEquals("theapache64/faded", repoName) 14 | } 15 | 16 | @Test 17 | fun getRepoPathInvalid() { 18 | val invalidUrl = "theapache64/faded" 19 | val repoName = PathUtils.getRepoPath(invalidUrl) 20 | assertEquals(null, repoName) 21 | } 22 | 23 | 24 | @Test 25 | fun getUsernameFromValidUrl() { 26 | val url = "https://github.com/theapache64" 27 | val userName = PathUtils.getUsername(url) 28 | assertEquals("theapache64", userName) 29 | } 30 | 31 | @Test 32 | fun getUsernameFromInvalidUrl() { 33 | val url = "https://github.com" 34 | val userName = PathUtils.getUsername(url) 35 | assertNull(userName) 36 | } 37 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/supergithub/utils/StringUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.utils 2 | 3 | import kotlin.test.Test 4 | import kotlin.test.assertEquals 5 | 6 | class StringUtilsTest { 7 | @Test 8 | fun parseInt() { 9 | assertEquals(100, StringUtils.parseInt("100%")) 10 | assertEquals(0, StringUtils.parseInt("0%")) 11 | assertEquals(null, StringUtils.parseInt("")) 12 | } 13 | } -------------------------------------------------------------------------------- /src/test/kotlin/com/theapache64/supergithub/utils/TimeUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.theapache64.supergithub.utils 2 | 3 | import kotlin.js.Date 4 | import kotlin.test.assertEquals 5 | 6 | class TimeUtilsTest { 7 | 8 | 9 | @kotlin.test.Test 10 | fun parseGood() { 11 | val today = Date("2020-01-20T16:00:00Z") // 04:00:00 PM, Jan 20, 2020 12 | TimeUtils.apply { 13 | 14 | val dateMap = mapOf( 15 | "2020-01-20T15:59:58Z" to "just now", // 03:59:58 PM, Jan 20, 2020 16 | "2020-01-20T15:59:00Z" to "a minute ago", // 03:59:00 PM, Jan 20, 2020 17 | "2020-01-20T15:30:00Z" to "30 minutes ago", // 03:30:00 PM, Jan 20, 2020 18 | "2020-01-20T15:01:00Z" to "59 minutes ago", // 03:01:00 PM, Jan 20, 2020 19 | "2020-01-20T15:00:00Z" to "an hour ago", // 03:00:00 PM, Jan 20, 2020 20 | "2020-01-20T14:00:00Z" to "2 hours ago", // 02:00:00 PM, Jan 20, 2020 21 | "2020-01-20T04:00:00Z" to "12 hours ago", // 04:00:00 AM, Jan 20, 2020 22 | "2020-01-20T01:00:00Z" to "15 hours ago", // 01:00:00 AM, Jan 20, 2020 23 | "2020-01-19T17:00:00Z" to "23 hours ago", // 05:00:00 PM, Jan 19, 2020 24 | "2020-01-19T16:01:00Z" to "23 hours ago", // 04:01:00 PM, Jan 19, 2020 25 | "2020-01-19T16:00:00Z" to "yesterday", // 04:00:00 PM, Jan 19, 2020 26 | "2020-01-18T00:00:00Z" to "2 days ago", // 00:00:00 PM, Jan 18, 2020 27 | "2020-01-01T00:00:00Z" to "19 days ago", // 00:00:00 PM, Jan 1, 2020 28 | "2019-12-20T00:00:00Z" to "a month ago", // 00:00:00 PM, Dec 20, 2019 29 | "2019-11-20T00:00:00Z" to "2 months ago", // 00:00:00 PM, Nov 20, 2019 30 | "2019-02-20T00:00:00Z" to "11 months ago", // 00:00:00 PM, Feb 20, 2019 31 | "2019-01-01T00:00:00Z" to "a year ago", // 00:00:00 PM, Jan 01, 2019 32 | "2018-01-01T00:00:00Z" to "2 years ago", // 00:00:00 PM, Jan 01, 2018 33 | "2010-01-01T00:00:00Z" to "10 years ago" // 00:00:00 PM, Jan 01, 2010 34 | ) 35 | 36 | for ((input, output) in dateMap) { 37 | val inputDate = Date(input) 38 | assertEquals(output, getRelativeTime(today, inputDate)) 39 | } 40 | 41 | } 42 | } 43 | } --------------------------------------------------------------------------------