├── .gitignore ├── .idea ├── .gitignore ├── compiler.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── README.md ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img.png ├── img_1.png ├── jetpackcompose ├── .gitignore ├── build.gradle ├── consumer-rules.pro ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── io │ │ └── github │ │ └── afalabarce │ │ └── jetpackcompose │ │ ├── AmountVisualTransformation.kt │ │ ├── BottomAppBarComposable.kt │ │ ├── BottomSheetDialogMaterial3.kt │ │ ├── CalendarComposables.kt │ │ ├── CardPager.kt │ │ ├── CircularProgressIndicator.kt │ │ ├── ConnectionAvailableContent.kt │ │ ├── DataGrid.kt │ │ ├── DrawCanvas.kt │ │ ├── FlipCardComposable.kt │ │ ├── LabelledSwitch.kt │ │ ├── LocalCompositionElements.kt │ │ ├── ModifierExtensions.kt │ │ ├── NoPaddingAlertDialog.kt │ │ ├── OrbitalMenu.kt │ │ ├── PasswordTextField.kt │ │ ├── PermissionManager.kt │ │ ├── PolygonalProgressBar.kt │ │ ├── RadioGroup.kt │ │ ├── ScaffoldWizard.kt │ │ ├── SetUiContent.kt │ │ ├── SpinnerSelector.kt │ │ ├── SvgPickerSelector.kt │ │ ├── SwipeableCard.kt │ │ ├── ViewModelService.kt │ │ ├── authmanager │ │ ├── Authenticator.kt │ │ ├── entities │ │ │ ├── BiometricCapabilities.kt │ │ │ └── IUser.kt │ │ ├── enums │ │ │ ├── LoginType.kt │ │ │ └── Operation.kt │ │ └── exceptions │ │ │ ├── AuthenticatorException.kt │ │ │ ├── UnsupportedAccountTypeException.kt │ │ │ ├── UnsupportedAuthTokenTypeException.kt │ │ │ └── UnsupportedFeaturesException.kt │ │ ├── networking │ │ ├── NetworkStatus.kt │ │ └── NetworkStatusTracker.kt │ │ ├── svg │ │ ├── AndroidResourceParser.kt │ │ ├── ResourceCollector.kt │ │ ├── Vector2SvgConverter.kt │ │ └── XmlUtilities.kt │ │ └── utilities │ │ └── Extensions.kt │ └── res │ └── values │ └── strings.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 41 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | dependencies { 5 | classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' 6 | // NOTE: Do not place your application dependencies here; they belong 7 | // in the individual module build.gradle files 8 | } 9 | } 10 | 11 | plugins { 12 | id 'com.android.application' version '7.4.2' apply false 13 | id 'com.android.library' version '7.4.2' apply false 14 | id 'org.jetbrains.kotlin.android' version '1.8.21' apply false 15 | } 16 | 17 | ext { 18 | compose_version = '1.4.3' 19 | material3_version='1.1.0' 20 | compose_compiler_version = '1.4.7' 21 | accompanist_version = '0.31.3-beta' 22 | coil_version = '2.3.0' 23 | } 24 | 25 | task clean(type: Delete) { 26 | delete rootProject.buildDir 27 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | # necessary to solve: 25 | # Software Components will not be created automatically for Maven publishing from Android Gradle Plugin 8.0. To opt-in to the future behavior, set the Gradle property android.disableAutomaticComponentCreation=true in the `gradle.properties` file or use the new publishing DSL. 26 | android.disableAutomaticComponentCreation=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afalabarce/jetpackcompose/d9fee93054cbf298eb99c85b88ee1067ce280454/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sun May 22 17:00:24 CEST 2022 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 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 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afalabarce/jetpackcompose/d9fee93054cbf298eb99c85b88ee1067ce280454/img.png -------------------------------------------------------------------------------- /img_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afalabarce/jetpackcompose/d9fee93054cbf298eb99c85b88ee1067ce280454/img_1.png -------------------------------------------------------------------------------- /jetpackcompose/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /jetpackcompose/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.android.library' 3 | id 'org.jetbrains.kotlin.android' 4 | id 'maven-publish' // Support for maven publishing artifacts 5 | id 'signing' // Support for signing artifacts 6 | } 7 | 8 | android { 9 | compileSdk 33 10 | 11 | defaultConfig { 12 | minSdk 24 13 | targetSdk 33 14 | versionCode 176 15 | versionName "1.7.6" 16 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 17 | consumerProguardFiles "consumer-rules.pro" 18 | vectorDrawables { 19 | useSupportLibrary true 20 | } 21 | } 22 | 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility JavaVersion.VERSION_11 31 | targetCompatibility JavaVersion.VERSION_11 32 | } 33 | kotlinOptions { 34 | jvmTarget = '11' 35 | } 36 | buildFeatures { 37 | compose true 38 | } 39 | composeOptions { 40 | kotlinCompilerExtensionVersion compose_compiler_version 41 | } 42 | packagingOptions { 43 | resources { 44 | excludes += '/META-INF/{AL2.0,LGPL2.1}' 45 | } 46 | } 47 | buildToolsVersion '30.0.3' 48 | } 49 | 50 | dependencies { 51 | implementation 'androidx.core:core-ktx:1.10.1' 52 | implementation 'androidx.appcompat:appcompat:1.6.1' 53 | implementation 'com.google.android.material:material:1.9.0' 54 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' 55 | implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.1" 56 | implementation 'androidx.activity:activity-compose:1.7.2' 57 | implementation 'androidx.preference:preference-ktx:1.2.0' 58 | implementation "androidx.constraintlayout:constraintlayout-compose:1.0.1" 59 | implementation "org.jetbrains.kotlin:kotlin-reflect:1.8.21" 60 | implementation "androidx.compose.ui:ui:$compose_version" 61 | implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" 62 | implementation "androidx.compose.material:material-icons-extended:1.4.3" 63 | implementation "androidx.compose.ui:ui-tooling:$compose_version" 64 | implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" 65 | implementation "androidx.compose.material3:material3:$material3_version" 66 | implementation "androidx.compose.animation:animation-graphics:$compose_version" 67 | implementation "androidx.compose.animation:animation:$compose_version" 68 | implementation "androidx.compose.animation:animation-core:$compose_version" 69 | implementation "com.google.accompanist:accompanist-navigation-animation:$accompanist_version" 70 | implementation "com.google.accompanist:accompanist-pager:$accompanist_version" 71 | implementation "com.google.accompanist:accompanist-pager-indicators:$accompanist_version" 72 | implementation "com.google.accompanist:accompanist-swiperefresh:$accompanist_version" 73 | implementation "com.google.accompanist:accompanist-permissions:$accompanist_version" 74 | implementation 'androidx.lifecycle:lifecycle-service:2.6.1' 75 | implementation 'com.google.code.gson:gson:2.10.1' 76 | 77 | // coil, like glide provides pictures asynchronously, but coil has better integration with compose 78 | implementation "io.coil-kt:coil-compose:$coil_version" 79 | implementation "io.coil-kt:coil-svg:$coil_version" 80 | implementation "io.coil-kt:coil-gif:$coil_version" 81 | implementation "io.coil-kt:coil-compose:$coil_version" 82 | 83 | } 84 | 85 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { 86 | kotlinOptions { 87 | freeCompilerArgs += "-opt-in=kotlin.RequiresOptIn" 88 | } 89 | } 90 | 91 | // Settings for publishing at mavenCentral 92 | 93 | ext{ 94 | publishedGroupId = "io.github.afalabarce" 95 | libraryName = "jetpackcompose" 96 | artifact = "jetpackcompose" 97 | libraryDescription = "Another Project for Jetpack Compose Composable Library" 98 | siteUrl = "https://github.com/afalabarce/jetpackcompose" 99 | gitUrl = "https://github.com/afalabarce/jetpackcompose.git" 100 | libraryVersionId = android.defaultConfig.versionCode 101 | libraryVersionCode = android.defaultConfig.versionName 102 | developerId = "afalabarce" 103 | developerName = "Antonio Fdez. Alabarce" 104 | developerEmail = "afalabarce@gmail.com" 105 | licenseName = "The Apache Software License, Version 2.0" 106 | licenseUrl = "http://www.apache.org/licenses/LICENSE-2.0.txt" 107 | allLicenses = ["Apache-2.0"] 108 | } 109 | 110 | task androidSourcesJar(type: Jar) { 111 | archiveClassifier = 'sources' 112 | from android.sourceSets.main.java.source 113 | } 114 | 115 | artifacts { 116 | archives androidSourcesJar 117 | } 118 | 119 | group = publishedGroupId 120 | version = libraryVersionCode 121 | 122 | ext["signing.keyId"] = '' 123 | ext["signing.password"] = '' 124 | ext["signing.secretKeyRingFile"] = '' 125 | ext["ossrhUsername"] = '' 126 | ext["ossrhPassword"] = '' 127 | 128 | File secretPropsFile = project.rootProject.file('local.properties') 129 | if (secretPropsFile.exists()) { 130 | println "Found secret props file, loading props" 131 | Properties p = new Properties() 132 | p.load(new FileInputStream(secretPropsFile)) 133 | p.each { name, value -> 134 | println "Prop: $name -> $value" 135 | ext[name] = value 136 | } 137 | } 138 | 139 | signing { 140 | sign publishing.publications 141 | } 142 | 143 | publishing { 144 | publications { 145 | release(MavenPublication) { 146 | // The coordinates of the library, being set from variables that 147 | // we'll set up in a moment 148 | groupId publishedGroupId 149 | artifactId artifact 150 | version libraryVersionCode 151 | 152 | println "groupId: $publishedGroupId" 153 | println "Artifact: $artifact" 154 | println "Version: $libraryVersionCode" 155 | 156 | // Two artifacts, the `aar` and the sources 157 | artifact("$buildDir/outputs/aar/${project.getName()}-release.aar") 158 | artifact androidSourcesJar 159 | 160 | // Self-explanatory metadata for the most part 161 | pom { 162 | name = artifact 163 | description = libraryDescription 164 | // If your project has a dedicated site, use its URL here 165 | url = gitUrl 166 | licenses { 167 | license { 168 | name = licenseName 169 | url = licenseUrl 170 | } 171 | } 172 | developers { 173 | developer { 174 | id = developerId 175 | name = developerName 176 | email = developerEmail 177 | } 178 | } 179 | // Version control info, if you're using GitHub, follow the format as seen here 180 | scm { 181 | connection = 'scm:git:github.com/afalabarce/jetpackcompose.git' 182 | developerConnection = 'scm:git:ssh://github.com/afalabarce/jetpackcompose.git' 183 | url = 'https://github.com/afalabarce/jetpackcompose/tree/master' 184 | } 185 | // A slightly hacky fix so that your POM will include any transitive dependencies 186 | // that your library builds upon 187 | withXml { 188 | def dependenciesNode = asNode().appendNode('dependencies') 189 | 190 | project.configurations.implementation.allDependencies.each { 191 | def dependencyNode = dependenciesNode.appendNode('dependency') 192 | dependencyNode.appendNode('groupId', it.group) 193 | dependencyNode.appendNode('artifactId', it.name) 194 | dependencyNode.appendNode('version', it.version) 195 | } 196 | } 197 | } 198 | } 199 | } 200 | repositories { 201 | // The repository to publish to, Sonatype/MavenCentral 202 | maven { 203 | // This is an arbitrary name, you may also use "mavencentral" or 204 | // any other name that's descriptive for you 205 | name = "sonatype" 206 | // these urls depend on the configuration provided to the user by sonatype 207 | def releasesRepoUrl = "https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/" 208 | def snapshotsRepoUrl = "https://s01.oss.sonatype.org/content/repositories/snapshots/" 209 | // You only need this if you want to publish snapshots, otherwise just set the URL 210 | // to the release repo directly 211 | url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 212 | 213 | // The username and password we've fetched earlier 214 | credentials { 215 | username ossrhUsername 216 | password ossrhPassword 217 | } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /jetpackcompose/consumer-rules.pro: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afalabarce/jetpackcompose/d9fee93054cbf298eb99c85b88ee1067ce280454/jetpackcompose/consumer-rules.pro -------------------------------------------------------------------------------- /jetpackcompose/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /jetpackcompose/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/AmountVisualTransformation.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.runtime.Stable 4 | import androidx.compose.ui.text.AnnotatedString 5 | import androidx.compose.ui.text.input.OffsetMapping 6 | import androidx.compose.ui.text.input.TransformedText 7 | import androidx.compose.ui.text.input.VisualTransformation 8 | import java.math.BigDecimal 9 | import java.text.DecimalFormatSymbols 10 | import java.util.Locale 11 | 12 | @Stable 13 | class AmountVisualTransformation( 14 | private val locale: Locale = Locale.getDefault(), 15 | private val hasMovedCursor: Boolean = true 16 | ) : VisualTransformation { 17 | 18 | private val symbols = DecimalFormatSymbols(locale) 19 | private val decimalSeparator = symbols.decimalSeparator 20 | private val thousandSeparator = symbols.groupingSeparator 21 | 22 | override fun filter(text: AnnotatedString): TransformedText { 23 | val input = text.text 24 | if (input.isValid()) { 25 | val inputWithSeparator = input.withCorrectSeparator() 26 | val numberOfDecimals = inputWithSeparator.getDecimalsNumber() 27 | val forNumber = if (numberOfDecimals > Zero) inputWithSeparator.getPart(Zero) else input 28 | var numberText = forNumber.withCorrectSeparator().formatThousands() 29 | 30 | if (input.last() == decimalSeparator) { 31 | numberText = numberText.dropLast(One) + decimalSeparator.toString() 32 | } 33 | 34 | if (numberOfDecimals > Zero) { 35 | numberText += decimalSeparator.toString() 36 | numberText += inputWithSeparator.getPart(One) 37 | } 38 | 39 | val offsetMapping = if (hasMovedCursor) { 40 | MovedCursorOffsetMapping( 41 | originalInput = input, 42 | inputTransformed = numberText, 43 | thousandSeparator = thousandSeparator 44 | ) 45 | } else { 46 | FixedCursorOffsetMapping( 47 | textLength = input.length, 48 | contentLength = numberText.length.safeNull() 49 | ) 50 | } 51 | 52 | val annotatedString = AnnotatedString( 53 | text = numberText 54 | ) 55 | 56 | return TransformedText(annotatedString, offsetMapping) 57 | } else { 58 | return TransformedText(text, OffsetMapping.Identity) 59 | } 60 | } 61 | 62 | private class FixedCursorOffsetMapping( 63 | private val textLength: Int, 64 | private val contentLength: Int 65 | ) : OffsetMapping { 66 | override fun originalToTransformed(offset: Int): Int { 67 | return contentLength 68 | } 69 | 70 | override fun transformedToOriginal(offset: Int): Int { 71 | return textLength 72 | } 73 | } 74 | 75 | private class MovedCursorOffsetMapping( 76 | private val originalInput: String, 77 | private val inputTransformed: String, 78 | private val thousandSeparator: Char 79 | ) : OffsetMapping { 80 | override fun originalToTransformed(offset: Int): Int { 81 | return offset + getOffsetCount(offset) 82 | } 83 | 84 | override fun transformedToOriginal(offset: Int): Int { 85 | return offset - getOffsetCount(offset) 86 | } 87 | 88 | private fun getOffsetCount(offset: Int): Int { 89 | val lastCursor = originalInput.length == offset 90 | val inputOffset = if (lastCursor) { 91 | inputTransformed 92 | } else 93 | inputTransformed.substring(Zero, offset) 94 | return inputOffset.count { it == thousandSeparator } 95 | } 96 | } 97 | 98 | private fun String.withCorrectSeparator(): String { 99 | val text = if (this.containsAny()) { 100 | var separatorText = this.replace(Comma, decimalSeparator) 101 | separatorText = separatorText.replace(Dot, decimalSeparator) 102 | separatorText 103 | } else { 104 | this 105 | } 106 | return text 107 | } 108 | 109 | private fun String.containsAny(): Boolean { 110 | return AllowedChars.any { this.contains(it) } 111 | } 112 | 113 | private fun Int?.safeNull() = this ?: Zero 114 | 115 | private fun String.isValid(): Boolean = (isNotEmpty() && startCorrect() && this.isValidDecimal()) 116 | 117 | private fun String.isValidDecimal(): Boolean = try { 118 | val commaReplaced = this.replace(Comma, Dot) 119 | val correctDecimal = commaReplaced.replace(decimalSeparator, Dot) 120 | BigDecimal(correctDecimal) 121 | true 122 | } catch (exception: NumberFormatException) { 123 | false 124 | } 125 | 126 | private fun String.startCorrect(): Boolean { 127 | val regex = Regex(StartRegex) 128 | return !regex.matches(this) && this.first().isDigit() 129 | } 130 | 131 | private fun String.getDecimalsNumber(): Int { 132 | val decimalParts = this.split(decimalSeparator) 133 | return if (decimalParts.size == MaxSize) { 134 | decimalParts[One].length 135 | } else { 136 | Zero 137 | } 138 | } 139 | 140 | private fun String.getPart(index: Int): String { 141 | val decimalParts = this.split(decimalSeparator) 142 | return if (decimalParts.size == MaxSize) { 143 | decimalParts[index] 144 | } else { 145 | EmptyString 146 | } 147 | } 148 | 149 | private fun String.formatThousands(): String { 150 | val addDecimal = !last().isDigit() 151 | val reversed = this.reversed().replace(decimalSeparator.toString(), EmptyString) 152 | val formatted = StringBuilder() 153 | 154 | for ((index, char) in reversed.withIndex()) { 155 | if (index > Zero && index % Thousands == Zero && char.isDigit()) { 156 | formatted.append(thousandSeparator) 157 | } 158 | formatted.append(char) 159 | } 160 | val formatThousands = formatted.reverse().toString() 161 | return if (addDecimal) formatThousands + decimalSeparator else formatThousands 162 | } 163 | } 164 | 165 | private const val Zero = 0 166 | private const val One = 1 167 | private const val MaxSize = 2 168 | private const val Thousands = 3 169 | private const val Comma = ',' 170 | private const val Dot = '.' 171 | private const val EmptyString = "" 172 | private const val StartRegex = "^0\\d+" 173 | private val AllowedChars = setOf(Comma, Dot) 174 | -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/BottomAppBarComposable.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape 6 | import androidx.compose.material.Button 7 | import androidx.compose.material.ButtonDefaults 8 | import androidx.compose.material.Icon 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Home 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.draw.clip 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.dp 19 | 20 | @Preview 21 | @Composable 22 | fun ToolButton(){ 23 | Box(modifier = Modifier.size(48.dp)) { 24 | Button( 25 | onClick = { /*TODO*/ }, 26 | modifier = Modifier.fillMaxSize(), 27 | shape = MaterialTheme.shapes.large , 28 | colors = ButtonDefaults.buttonColors(backgroundColor = Color.Transparent, contentColor = Color.Transparent), 29 | border = null, 30 | elevation = null, 31 | contentPadding = PaddingValues(0.dp) 32 | ) { 33 | Box( 34 | modifier = Modifier 35 | .fillMaxSize(), 36 | contentAlignment = Alignment.TopCenter 37 | ) { 38 | Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(0.5f) 39 | .clip( 40 | AbsoluteRoundedCornerShape( 41 | topLeftPercent = 0, 42 | topRightPercent = 0, 43 | bottomLeftPercent = 100, 44 | bottomRightPercent = 100 45 | ) 46 | ) 47 | .background(Color.Blue) 48 | ) 49 | Box( 50 | modifier = Modifier 51 | .fillMaxSize(), 52 | contentAlignment = Alignment.Center 53 | ){ 54 | Icon(Icons.Filled.Home, contentDescription = null, tint = Color.White) 55 | } 56 | } 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/BottomSheetDialogMaterial3.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.activity.compose.BackHandler 5 | import androidx.compose.animation.AnimatedVisibility 6 | import androidx.compose.animation.core.* 7 | import androidx.compose.animation.slideInVertically 8 | import androidx.compose.animation.slideOutVertically 9 | import androidx.compose.animation.splineBasedDecay 10 | import androidx.compose.foundation.BorderStroke 11 | import androidx.compose.foundation.background 12 | import androidx.compose.foundation.clickable 13 | import androidx.compose.foundation.gestures.awaitFirstDown 14 | import androidx.compose.foundation.gestures.verticalDrag 15 | import androidx.compose.foundation.layout.* 16 | import androidx.compose.foundation.shape.CornerSize 17 | import androidx.compose.material3.* 18 | import androidx.compose.runtime.* 19 | import androidx.compose.ui.Alignment 20 | import androidx.compose.ui.ExperimentalComposeUiApi 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.composed 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.graphics.Shape 25 | import androidx.compose.ui.input.pointer.pointerInput 26 | import androidx.compose.ui.input.pointer.positionChange 27 | import androidx.compose.ui.input.pointer.util.VelocityTracker 28 | import androidx.compose.ui.platform.LocalContext 29 | import androidx.compose.ui.unit.IntOffset 30 | import androidx.compose.ui.unit.dp 31 | import androidx.compose.ui.window.Dialog 32 | import androidx.compose.ui.window.DialogProperties 33 | import kotlinx.coroutines.coroutineScope 34 | import kotlinx.coroutines.launch 35 | import kotlin.math.absoluteValue 36 | import kotlin.math.roundToInt 37 | 38 | @SuppressLint("ReturnFromAwaitPointerEventScope") 39 | private fun Modifier.swipeToDismiss( 40 | onDismissed: () -> Unit 41 | ): Modifier = composed { 42 | val offsetY = remember { Animatable(0f) } 43 | pointerInput(Unit) { 44 | // Used to calculate fling decay. 45 | val decay = splineBasedDecay(this) 46 | // Use suspend functions for touch events and the Animatable. 47 | coroutineScope { 48 | while (true) { 49 | // Detect a touch down event. 50 | val pointerId = awaitPointerEventScope { awaitFirstDown().id } 51 | val velocityTracker = VelocityTracker() 52 | // Stop any ongoing animation. 53 | offsetY.stop() 54 | awaitPointerEventScope { 55 | verticalDrag(pointerId) { change -> 56 | // Update the animation value with touch events. 57 | launch { 58 | offsetY.snapTo( 59 | offsetY.value + change.positionChange().y 60 | ) 61 | } 62 | velocityTracker.addPosition( 63 | change.uptimeMillis, 64 | change.position 65 | ) 66 | } 67 | } 68 | // No longer receiving touch events. Prepare the animation. 69 | val velocity = velocityTracker.calculateVelocity().x 70 | val targetOffsetY = decay.calculateTargetValue( 71 | offsetY.value, 72 | velocity 73 | ) 74 | // The animation stops when it reaches the bounds. 75 | offsetY.updateBounds( 76 | lowerBound = -size.height.toFloat(), 77 | upperBound = size.height.toFloat() 78 | ) 79 | launch { 80 | if (targetOffsetY.absoluteValue <= size.height) { 81 | // Not enough velocity; Slide back. 82 | offsetY.animateTo( 83 | targetValue = 0f, 84 | initialVelocity = velocity 85 | ) 86 | } else { 87 | // The element was swiped away. 88 | offsetY.animateDecay(velocity, decay) 89 | onDismissed() 90 | } 91 | } 92 | } 93 | } 94 | }.offset { IntOffset(0, offsetY.value.roundToInt()) } 95 | } 96 | 97 | @OptIn(ExperimentalComposeUiApi::class) 98 | @Composable 99 | fun BottomSheetDialogMaterial3( 100 | isVisible: Boolean, 101 | slideTimeInMillis: Int = 800, 102 | backDropColor: Color = Color(0x44444444), 103 | dialogShape: Shape = MaterialTheme.shapes.medium.copy( 104 | bottomEnd = CornerSize(0), 105 | bottomStart = CornerSize(0) 106 | ), 107 | dialogElevation: CardElevation = CardDefaults.cardElevation(defaultElevation = 4.dp), 108 | dialogBorderStroke: BorderStroke? = BorderStroke(2.dp, MaterialTheme.colorScheme.onBackground), 109 | cardColors: CardColors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.background), 110 | onDismissRequest: () -> Unit = {}, 111 | content: @Composable () -> Unit 112 | ) { 113 | val context = LocalContext.current 114 | val transitionState = remember { MutableTransitionState(initialState = false) } 115 | val bottomPadding by remember { mutableStateOf(0.dp) } 116 | val resources = context.resources 117 | val withNavBar = 118 | resources.getBoolean(resources.getIdentifier("config_showNavigationBar", "bool", "android")) 119 | if (withNavBar) { 120 | //bottomPadding = 24.dp 121 | } 122 | 123 | if (!isVisible) 124 | transitionState.targetState = false 125 | else { 126 | Dialog( 127 | onDismissRequest = { 128 | transitionState.targetState = false 129 | onDismissRequest() 130 | }, 131 | properties = DialogProperties(usePlatformDefaultWidth = false) 132 | ) { 133 | Box( 134 | modifier = Modifier 135 | .fillMaxSize() 136 | .background(backDropColor) 137 | .padding(bottom = bottomPadding) 138 | .clickable { 139 | transitionState.targetState = false 140 | onDismissRequest() 141 | }, 142 | contentAlignment = Alignment.BottomCenter, 143 | ) { 144 | AnimatedVisibility( 145 | visibleState = transitionState, 146 | enter = slideInVertically( 147 | initialOffsetY = { it }, 148 | animationSpec = TweenSpec( 149 | durationMillis = slideTimeInMillis, 150 | delay = 0, 151 | easing = LinearEasing 152 | ) 153 | ), 154 | exit = slideOutVertically( 155 | targetOffsetY = { it }, 156 | animationSpec = TweenSpec( 157 | durationMillis = slideTimeInMillis, 158 | delay = 0, 159 | easing = LinearEasing 160 | ) 161 | ) 162 | ) { 163 | Card( 164 | modifier = Modifier 165 | .fillMaxWidth() 166 | .clickable { }.swipeToDismiss { 167 | transitionState.targetState = false 168 | onDismissRequest() 169 | }, 170 | shape = dialogShape, 171 | elevation = dialogElevation, 172 | border = dialogBorderStroke, 173 | colors = cardColors, 174 | ) { 175 | content() 176 | } 177 | } 178 | 179 | BackHandler { 180 | transitionState.targetState = false 181 | onDismissRequest() 182 | } 183 | } 184 | } 185 | transitionState.targetState = true 186 | } 187 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/CardPager.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.height 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.material.Card 12 | import androidx.compose.material.MaterialTheme 13 | import androidx.compose.material.Surface 14 | import androidx.compose.material.Text 15 | import androidx.compose.material.contentColorFor 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.getValue 18 | import androidx.compose.runtime.mutableStateOf 19 | import androidx.compose.runtime.remember 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.graphics.Shape 25 | import androidx.compose.ui.tooling.preview.Preview 26 | import androidx.compose.ui.unit.Dp 27 | import androidx.compose.ui.unit.dp 28 | import com.google.accompanist.pager.HorizontalPager 29 | import com.google.accompanist.pager.HorizontalPagerIndicator 30 | import com.google.accompanist.pager.rememberPagerState 31 | 32 | 33 | @Composable 34 | fun CardPager( 35 | modifier: Modifier, 36 | shape: Shape = MaterialTheme.shapes.medium, 37 | backgroundColor: Color = MaterialTheme.colors.surface, 38 | contentColor: Color = contentColorFor(backgroundColor), 39 | showPagerIndicator: Boolean = true, 40 | pagerIndicatorActiveColor: Color = Color.Black, 41 | pagerIndicatorInactiveColor: Color = Color.Gray, 42 | border: BorderStroke? = null, 43 | elevation: Dp = 1.dp, 44 | pageComposables: Array<@Composable () -> Unit> 45 | ){ 46 | var selectedPage by remember { mutableStateOf(0) } 47 | Card( 48 | modifier = modifier, 49 | shape = shape, 50 | backgroundColor = backgroundColor, 51 | contentColor = contentColor, 52 | border = border, 53 | elevation = elevation, 54 | 55 | ){ 56 | Box( 57 | modifier = Modifier.fillMaxSize(), 58 | contentAlignment = Alignment.BottomCenter, 59 | ) { 60 | val pagerState = rememberPagerState(selectedPage) 61 | 62 | HorizontalPager( 63 | modifier = Modifier.fillMaxSize(), 64 | count = pageComposables.size, 65 | state = pagerState, 66 | 67 | ) { pagerScope -> 68 | if (pagerScope in pageComposables.indices){ 69 | pageComposables[pagerScope]() 70 | } 71 | } 72 | if (showPagerIndicator){ 73 | HorizontalPagerIndicator( 74 | pagerState = pagerState, 75 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), 76 | pageCount = pageComposables.size, 77 | activeColor = pagerIndicatorActiveColor, 78 | inactiveColor = pagerIndicatorInactiveColor, 79 | ) 80 | } 81 | } 82 | } 83 | } 84 | 85 | @Preview(showSystemUi = true) 86 | @Composable 87 | private fun CardPagerPreview(){ 88 | MaterialTheme{ 89 | Surface{ 90 | Column(modifier = Modifier 91 | .fillMaxSize() 92 | .padding(horizontal = 8.dp), 93 | verticalArrangement = Arrangement.Top) { 94 | CardPager( 95 | modifier = Modifier 96 | .fillMaxWidth() 97 | .height(96.dp) 98 | .padding(vertical = 4.dp), 99 | border = BorderStroke(1.dp, Color.Black), 100 | pageComposables = arrayOf( 101 | { 102 | Text(text = "Page 1.1") 103 | }, 104 | { 105 | Text(text = "Page 1.2") 106 | }, 107 | { 108 | Text(text = "Page 1.3") 109 | } 110 | ) 111 | ) 112 | 113 | CardPager( 114 | modifier = Modifier 115 | .fillMaxWidth() 116 | .height(96.dp) 117 | .padding(vertical = 4.dp), 118 | border = BorderStroke(1.dp, Color.Black), 119 | pagerIndicatorActiveColor = Color.Red, 120 | pagerIndicatorInactiveColor = Color.Cyan, 121 | pageComposables = arrayOf( 122 | { 123 | Text(text = "Page 2.1") 124 | }, 125 | { 126 | Text(text = "Page 2.2") 127 | }, 128 | { 129 | Text(text = "Page 2.3") 130 | } 131 | ) 132 | ) 133 | } 134 | 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/CircularProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.animation.core.animateFloatAsState 4 | import androidx.compose.foundation.Canvas 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material.CircularProgressIndicator 7 | import androidx.compose.material.MaterialTheme 8 | import androidx.compose.material.ProgressIndicatorDefaults 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.geometry.Offset 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.drawscope.Stroke 15 | import androidx.compose.ui.unit.Dp 16 | import androidx.compose.ui.unit.dp 17 | 18 | @Composable 19 | fun CircularProgressIndicatorWithBackground(progress: Float, 20 | modifier: Modifier = Modifier, 21 | progressColor: Color = MaterialTheme.colors.primary, 22 | backgroundColor: Color = MaterialTheme.colors.onPrimary, 23 | strokeWidth: Dp = ProgressIndicatorDefaults.StrokeWidth, 24 | content: @Composable () -> Unit 25 | ){ 26 | Box(modifier = modifier, 27 | contentAlignment = Alignment.Center 28 | ) { 29 | Canvas( 30 | modifier = Modifier.fillMaxSize() 31 | .padding(16.dp) 32 | ) { 33 | drawCircle( 34 | color = backgroundColor, 35 | center = Offset(size.width / 2f, 24 + size.height / 2f), 36 | style = Stroke(width = strokeWidth.toPx()), 37 | 38 | ) 39 | } 40 | val animatedProgress = animateFloatAsState( 41 | targetValue = progress, 42 | animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec 43 | ).value 44 | 45 | CircularProgressIndicator( 46 | animatedProgress, 47 | modifier = Modifier 48 | .fillMaxSize() 49 | .padding(16.dp), 50 | color = progressColor, 51 | strokeWidth = strokeWidth 52 | ) 53 | Column(modifier = Modifier 54 | .fillMaxWidth() 55 | .padding(top = 16.dp), 56 | horizontalAlignment = Alignment.CenterHorizontally 57 | ) { 58 | content() 59 | } 60 | } 61 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/ConnectionAvailableContent.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.activity.compose.setContent 5 | import androidx.compose.runtime.* 6 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 7 | import io.github.afalabarce.jetpackcompose.networking.NetworkStatus 8 | import io.github.afalabarce.jetpackcompose.networking.NetworkStatusTracker 9 | 10 | fun ComponentActivity.setNetworkingContent(content: @Composable () -> Unit){ 11 | setContent { 12 | val currentNetworkStatus by NetworkStatusTracker(this@setNetworkingContent) 13 | .networkStatus.collectAsStateWithLifecycle(initialValue = NetworkStatus.Available) 14 | 15 | CompositionLocalProvider ( 16 | LocalNetworkStatus provides currentNetworkStatus, 17 | ){ 18 | content() 19 | } 20 | } 21 | } 22 | 23 | fun ComponentActivity.setConnectionAvailableContent(content: @Composable (NetworkStatus) -> Unit){ 24 | setContent { 25 | val networkStatusTracker by remember { mutableStateOf(NetworkStatusTracker(this)) } 26 | val connectionStatus by networkStatusTracker.networkStatus.collectAsStateWithLifecycle(NetworkStatus.Available) 27 | 28 | content(connectionStatus) 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/DataGrid.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.horizontalScroll 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.material.Button 9 | import androidx.compose.material.ButtonDefaults 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.text.style.TextAlign 17 | import androidx.compose.ui.tooling.preview.Preview 18 | import androidx.compose.ui.unit.Dp 19 | import androidx.compose.ui.unit.TextUnit 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import io.github.afalabarce.jetpackcompose.utilities.format 23 | import java.util.* 24 | import kotlin.reflect.KProperty 25 | 26 | enum class DataType{ 27 | Long, 28 | Decimal, 29 | Date, 30 | Boolean, 31 | Image, 32 | List, 33 | String 34 | } 35 | 36 | data class GridColumn( 37 | val visibleIndex: Int, 38 | val title: String, 39 | val dataType: DataType, 40 | val fieldName: String = "", 41 | val width: Dp, 42 | val height: Dp, 43 | val textAlign: TextAlign = TextAlign.Center, 44 | val textSize: TextUnit = 12.sp, 45 | val fontWeight: FontWeight = FontWeight.Normal, 46 | val backgroundColor: Color = Color.Gray, 47 | val textColor: Color = Color.Black, 48 | val dataFormat: String = "", 49 | val visible: Boolean = true 50 | ) 51 | @Composable 52 | private fun HeaderCell(column: GridColumn, onClick: (GridColumn) -> Unit){ 53 | Button( 54 | onClick = { onClick(column) }, 55 | modifier = Modifier 56 | .size(column.width, column.height) 57 | .padding(0.dp), 58 | colors = ButtonDefaults.buttonColors(backgroundColor = column.backgroundColor) 59 | ) { 60 | Text( 61 | modifier = Modifier 62 | .fillMaxSize() 63 | .padding(0.dp), 64 | text = column.title, 65 | fontSize = column.textSize, 66 | textAlign = column.textAlign, 67 | fontWeight = column.fontWeight 68 | ) 69 | } 70 | } 71 | 72 | @Composable 73 | private fun DataRow(row: T, columns: List){ 74 | val classType = row!!::class 75 | 76 | Row(modifier = Modifier 77 | .fillMaxWidth(), 78 | verticalAlignment = Alignment.Top) { 79 | columns.filter { c -> c.visible }.sortedBy { c -> c.visibleIndex }.forEach { column -> 80 | val columnField = classType.members.firstOrNull { p -> p.name == column.fieldName} as? KProperty 81 | 82 | if (columnField != null){ 83 | val cellValue = columnField.getter.call(row).toString() 84 | /*try { 85 | when (column.dataType) { 86 | DataType.Long -> (columnField.get(row) as Long).format(column.dataFormat) 87 | DataType.Decimal -> (columnField.get(row) as Float).format(column.dataFormat) 88 | DataType.Boolean -> (columnField.get(row) as Boolean).toString() 89 | DataType.Date -> (columnField.get(row) as? Date 90 | ?: Calendar.getInstance().time).format(column.dataFormat) 91 | else -> columnField.get(row).toString() 92 | } 93 | } catch (ex: java.lang.Exception) { 94 | columnField.name 95 | } 96 | */ 97 | Text( 98 | text = cellValue, 99 | fontSize = column.textSize, 100 | modifier = Modifier.size(column.width, column.height), 101 | color = column.textColor 102 | ) 103 | } 104 | } 105 | } 106 | } 107 | 108 | @Composable 109 | fun DataGrid( 110 | modifier: Modifier, 111 | columns: List, 112 | items: List, 113 | onColumnClick: (GridColumn) -> Unit 114 | ){ 115 | Column(modifier = modifier 116 | .horizontalScroll(rememberScrollState())) { 117 | Row(modifier = Modifier 118 | .fillMaxWidth(), 119 | verticalAlignment = Alignment.Top 120 | ) { 121 | columns.filter { c -> c.visible } 122 | .sortedBy { x -> x.visibleIndex } 123 | .forEach { column -> HeaderCell(column = column, onClick = onColumnClick) } 124 | } 125 | LazyColumn(modifier = Modifier.fillMaxHeight()){ 126 | items(items){row -> DataRow(row, columns = columns) } 127 | } 128 | } 129 | } 130 | 131 | data class TestEntity(val column1: String, val column2: Long, val column3: Float, val column4: Date) 132 | 133 | @Preview(showSystemUi = true) 134 | @Composable 135 | fun DataGridPreview(){ 136 | 137 | val columns = listOf( 138 | GridColumn( 139 | visibleIndex = 0, 140 | title = "Header 1", 141 | dataType = DataType.String, 142 | fieldName = "column1", 143 | width = 110.dp, 144 | height = 32.dp 145 | ), 146 | GridColumn( 147 | visibleIndex = 1, 148 | title = "Header 2", 149 | dataType = DataType.Long, 150 | fieldName = "column2", 151 | width = 110.dp, 152 | height = 32.dp 153 | ), 154 | GridColumn( 155 | visibleIndex = 3, 156 | title = "Header 3", 157 | dataType = DataType.Decimal, 158 | fieldName = "column3", 159 | width = 110.dp, 160 | height = 32.dp 161 | ), 162 | GridColumn( 163 | visibleIndex = 2, 164 | title = "Header 4", 165 | dataType = DataType.Date, 166 | fieldName = "column4", 167 | width = 110.dp, 168 | height = 32.dp 169 | ) 170 | ) 171 | 172 | DataGrid( 173 | modifier = Modifier.fillMaxSize(), 174 | columns = columns, 175 | items = listOf( 176 | TestEntity("Data 1", 1000L,2.5f, Calendar.getInstance().time), 177 | TestEntity("Data 2", 100L, 4.5f, Calendar.getInstance().time) 178 | ), 179 | onColumnClick = {} 180 | ) 181 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/DrawCanvas.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import android.graphics.Bitmap 4 | import androidx.compose.foundation.gestures.awaitFirstDown 5 | import androidx.compose.foundation.gestures.forEachGesture 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.draw.drawBehind 10 | import androidx.compose.ui.geometry.Offset 11 | import androidx.compose.ui.graphics.* 12 | import androidx.compose.ui.input.pointer.PointerEvent 13 | import androidx.compose.ui.input.pointer.PointerInputChange 14 | import androidx.compose.ui.input.pointer.consumePositionChange 15 | import androidx.compose.ui.input.pointer.pointerInput 16 | import androidx.compose.ui.platform.LocalContext 17 | import androidx.compose.ui.unit.Dp 18 | import androidx.compose.ui.unit.dp 19 | import androidx.core.graphics.scale 20 | import java.io.ByteArrayOutputStream 21 | 22 | private enum class DrawAction{ 23 | Idle, 24 | Down, 25 | Up, 26 | Move 27 | } 28 | 29 | @Composable 30 | fun DrawCanvas( 31 | modifier: Modifier, 32 | penColor: Color = Color.Black, 33 | penWidth: Dp = 2.dp, 34 | erase: Boolean = false, 35 | waterMark: Bitmap? = null, 36 | waterMarkOnFront: Boolean = false, 37 | onErase: () -> Unit = {}, 38 | onDraw: (ByteArray) -> Unit = {} 39 | ){ 40 | val path by remember { mutableStateOf(Path()) } 41 | 42 | if (erase){ 43 | path.reset() 44 | onErase() 45 | } 46 | 47 | var motionEvent by remember { mutableStateOf(DrawAction.Idle) } 48 | var currentPosition by remember { mutableStateOf(Offset.Unspecified) } 49 | var previousPosition by remember { mutableStateOf(Offset.Unspecified) } 50 | 51 | val painter = Paint().apply { 52 | style = PaintingStyle.Stroke 53 | color = penColor 54 | strokeWidth = penWidth.value * LocalContext.current.resources.displayMetrics.density 55 | strokeCap = StrokeCap.Round 56 | strokeJoin = StrokeJoin.Round 57 | } 58 | 59 | val canvasModifier = modifier 60 | .pointerInput(Unit) { 61 | forEachGesture { 62 | awaitPointerEventScope { 63 | // Wait for at least one pointer to press down, and set first contact position 64 | awaitFirstDown().also { 65 | motionEvent = DrawAction.Down 66 | currentPosition = it.position 67 | previousPosition = currentPosition 68 | } 69 | 70 | do { 71 | // This PointerEvent contains details including events, id, position and more 72 | val event: PointerEvent = awaitPointerEvent() 73 | if (currentPosition == Offset.Unspecified) { 74 | motionEvent = DrawAction.Down 75 | currentPosition = event.changes.first().position 76 | previousPosition = currentPosition 77 | } 78 | event.changes 79 | .forEachIndexed { _: Int, pointerInputChange: PointerInputChange -> 80 | 81 | // This necessary to prevent other gestures or scrolling 82 | // when at least one pointer is down on canvas to draw 83 | pointerInputChange.consumePositionChange() 84 | } 85 | motionEvent = DrawAction.Move 86 | currentPosition = event.changes.first().position 87 | } while (event.changes.any { it.pressed }) 88 | 89 | motionEvent = DrawAction.Up 90 | } 91 | } 92 | } 93 | 94 | Box(modifier = canvasModifier.drawBehind { 95 | when(motionEvent){ 96 | DrawAction.Down -> { 97 | if (currentPosition.x != 0f && currentPosition.y != 0f) 98 | path.moveTo(currentPosition.x, currentPosition.y) 99 | previousPosition = currentPosition 100 | } 101 | DrawAction.Move -> { 102 | if (currentPosition != Offset.Unspecified && currentPosition.x != 0f && currentPosition.y != 0f) { 103 | //path.lineTo(currentPosition.x, currentPosition.y) 104 | path.quadraticBezierTo( 105 | previousPosition.x, 106 | previousPosition.y, 107 | (previousPosition.x + currentPosition.x) / 2, 108 | (previousPosition.y + currentPosition.y) / 2 109 | ) 110 | } 111 | previousPosition = currentPosition 112 | } 113 | DrawAction.Up -> { 114 | path.lineTo(currentPosition.x, currentPosition.y) 115 | // Change state to idle to not draw in wrong position if recomposition happens 116 | currentPosition = Offset.Unspecified 117 | previousPosition = currentPosition 118 | motionEvent = DrawAction.Idle 119 | } 120 | else -> Unit 121 | } 122 | val drawingBitmap = ImageBitmap(size.width.toInt(), size.height.toInt(), ImageBitmapConfig.Argb8888).asAndroidBitmap() 123 | val drawingCanvas = Canvas(drawingBitmap.asImageBitmap()) 124 | 125 | if (!erase){ 126 | if (waterMark != null && !waterMarkOnFront){ 127 | drawingCanvas.drawImage(waterMark.scale(size.width.toInt(), size.height.toInt()).asImageBitmap(), Offset(0f, 0f), painter ) 128 | } 129 | 130 | drawingCanvas.drawPath( 131 | path, 132 | painter 133 | //style = Stroke(width = penWidth.toPx(), cap = StrokeCap.Round, join = StrokeJoin.Round) 134 | ) 135 | 136 | if (waterMark != null && waterMarkOnFront){ 137 | drawingCanvas.drawImage(waterMark.scale(size.width.toInt(), size.height.toInt()).asImageBitmap(), Offset(0f, 0f), painter ) 138 | } 139 | 140 | } 141 | 142 | drawImage(drawingBitmap.asImageBitmap()) 143 | val msBmp = ByteArrayOutputStream() 144 | drawingBitmap.compress(Bitmap.CompressFormat.PNG, 100, msBmp) 145 | onDraw(msBmp.toByteArray()) 146 | }) 147 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/FlipCardComposable.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.animation.core.FastOutSlowInEasing 4 | import androidx.compose.animation.core.animateFloatAsState 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.material.ExperimentalMaterialApi 9 | import androidx.compose.material.Card 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.graphicsLayer 13 | 14 | enum class CardFace(val angle: Float) { 15 | Front(0f) { 16 | override val next: CardFace 17 | get() = Back 18 | }, 19 | Back(180f) { 20 | override val next: CardFace 21 | get() = Front 22 | }; 23 | 24 | abstract val next: CardFace 25 | } 26 | 27 | enum class RotationAxis { 28 | AxisX, 29 | AxisY, 30 | } 31 | 32 | @OptIn(ExperimentalMaterialApi::class) 33 | @Composable 34 | fun FlipCard( 35 | cardFace: CardFace, 36 | onClick: (CardFace) -> Unit, 37 | modifier: Modifier = Modifier, 38 | axis: RotationAxis = RotationAxis.AxisY, 39 | durationEffect: Int = 600, 40 | back: @Composable () -> Unit = {}, 41 | front: @Composable () -> Unit = {}, 42 | ) { 43 | val rotation = animateFloatAsState( 44 | targetValue = cardFace.angle, 45 | animationSpec = tween( 46 | durationMillis = durationEffect, 47 | easing = FastOutSlowInEasing, 48 | ) 49 | ) 50 | Card( 51 | onClick = { onClick(cardFace) }, 52 | modifier = modifier 53 | .graphicsLayer { 54 | if (axis == RotationAxis.AxisX) { 55 | rotationX = rotation.value 56 | } else { 57 | rotationY = rotation.value 58 | } 59 | cameraDistance = 12f * density 60 | }, 61 | ) { 62 | if (rotation.value <= 90f) { 63 | Box( 64 | Modifier.fillMaxSize() 65 | ) { 66 | front() 67 | } 68 | } else { 69 | Box( 70 | Modifier 71 | .fillMaxSize() 72 | .graphicsLayer { 73 | if (axis == RotationAxis.AxisX) { 74 | rotationX = 180f 75 | } else { 76 | rotationY = 180f 77 | } 78 | }, 79 | ) { 80 | back() 81 | } 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/LabelledSwitch.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.selection.toggleable 5 | import androidx.compose.material.ContentAlpha 6 | import androidx.compose.material.LocalContentAlpha 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.semantics.Role 12 | import androidx.compose.ui.text.TextStyle 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun LabelledSwitch( 17 | modifier: Modifier = Modifier, 18 | checked: Boolean, 19 | label: String, 20 | labelStyle: TextStyle = MaterialTheme.typography.labelMedium, 21 | leadingIcon: @Composable () -> Unit = {}, 22 | enabled: Boolean = true, 23 | colors: SwitchColors = SwitchDefaults.colors(), 24 | onCheckedChange: ((Boolean) -> Unit) 25 | ) { 26 | 27 | Box( 28 | modifier = modifier 29 | .fillMaxWidth() 30 | .height(56.dp) 31 | .toggleable( 32 | value = checked, 33 | onValueChange = onCheckedChange, 34 | role = Role.Switch, 35 | enabled = enabled 36 | ) 37 | .padding(horizontal = 8.dp) 38 | 39 | ) { 40 | CompositionLocalProvider( 41 | LocalContentAlpha provides 42 | if (enabled) ContentAlpha.high else ContentAlpha.disabled 43 | ) { 44 | Row( 45 | modifier = Modifier.fillMaxWidth(0.9f).align(Alignment.CenterStart), 46 | verticalAlignment = Alignment.CenterVertically, 47 | horizontalArrangement = Arrangement.Start 48 | ) { 49 | leadingIcon() 50 | Text( 51 | text = label, 52 | style = labelStyle, 53 | modifier = Modifier 54 | .padding(end = 16.dp) 55 | ) 56 | } 57 | } 58 | 59 | Switch( 60 | checked = checked, 61 | onCheckedChange = null, 62 | enabled = enabled, 63 | colors = colors, 64 | modifier = Modifier.align(Alignment.CenterEnd).padding(start = 8.dp) 65 | ) 66 | } 67 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/LocalCompositionElements.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import io.github.afalabarce.jetpackcompose.authmanager.entities.BiometricCapabilities 5 | import io.github.afalabarce.jetpackcompose.networking.NetworkStatus 6 | 7 | val LocalNetworkStatus = compositionLocalOf { 8 | NetworkStatus.Available 9 | } 10 | 11 | val LocalBiometricCapabilities = compositionLocalOf { BiometricCapabilities() } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/ModifierExtensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.ui.Modifier 5 | import androidx.compose.ui.composed 6 | import androidx.compose.ui.draw.drawWithCache 7 | import androidx.compose.ui.geometry.* 8 | import androidx.compose.ui.graphics.* 9 | import androidx.compose.ui.graphics.drawscope.* 10 | import androidx.compose.ui.platform.debugInspectorInfo 11 | import androidx.compose.ui.unit.Dp 12 | 13 | /** 14 | * This file is from project https://github.com/afalabarce/jetpackcompose 15 | */ 16 | /** 17 | * Modify element to add border with appearance specified with a [border] and a [shape], pad the 18 | * content by the [BorderStroke.width] and clip it. 19 | * 20 | * @sample androidx.compose.foundation.samples.BorderSample() 21 | * 22 | * @param border [BorderStroke] class that specifies border appearance, such as size and color 23 | * @param shape shape of the border 24 | */ 25 | fun Modifier.dashedBorder(border: BorderStroke, shape: Shape = RectangleShape, on: Dp, off: Dp) = 26 | dashedBorder(width = border.width, brush = border.brush, shape = shape, on, off) 27 | 28 | /** 29 | * Returns a [Modifier] that adds border with appearance specified with [width], [color] and a 30 | * [shape], pads the content by the [width] and clips it. 31 | * 32 | * @sample androidx.compose.foundation.samples.BorderSampleWithDataClass() 33 | * 34 | * @param width width of the border. Use [Dp.Hairline] for a hairline border. 35 | * @param color color to paint the border with 36 | * @param shape shape of the border 37 | * @param on the size of the solid part of the dashes 38 | * @param off the size of the space between dashes 39 | */ 40 | fun Modifier.dashedBorder(width: Dp, color: Color, shape: Shape = RectangleShape, on: Dp, off: Dp) = 41 | dashedBorder(width, SolidColor(color), shape, on, off) 42 | 43 | /** 44 | * Returns a [Modifier] that adds border with appearance specified with [width], [brush] and a 45 | * [shape], pads the content by the [width] and clips it. 46 | * 47 | * @sample androidx.compose.foundation.samples.BorderSampleWithBrush() 48 | * 49 | * @param width width of the border. Use [Dp.Hairline] for a hairline border. 50 | * @param brush brush to paint the border with 51 | * @param shape shape of the border 52 | */ 53 | fun Modifier.dashedBorder(width: Dp, brush: Brush, shape: Shape, on: Dp, off: Dp): Modifier = 54 | composed( 55 | factory = { 56 | this.then( 57 | Modifier.drawWithCache { 58 | val outline: Outline = shape.createOutline(size, layoutDirection, this) 59 | val borderSize = if (width == Dp.Hairline) 1f else width.toPx() 60 | 61 | var insetOutline: Outline? = null // outline used for roundrect/generic shapes 62 | var stroke: Stroke? = null // stroke to draw border for all outline types 63 | var pathClip: Path? = null // path to clip roundrect/generic shapes 64 | var inset = 0f // inset to translate before drawing the inset outline 65 | // path to draw generic shapes or roundrects with different corner radii 66 | var insetPath: Path? = null 67 | if (borderSize > 0 && size.minDimension > 0f) { 68 | if (outline is Outline.Rectangle) { 69 | stroke = Stroke( 70 | borderSize, pathEffect = PathEffect.dashPathEffect( 71 | floatArrayOf(on.toPx(), off.toPx()) 72 | ) 73 | ) 74 | } else { 75 | // Multiplier to apply to the border size to get a stroke width that is 76 | // large enough to cover the corners while not being too large to overly 77 | // square off the internal shape. The resultant shape will be 78 | // clipped to the desired shape. Any value lower will show artifacts in 79 | // the corners of shapes. A value too large will always square off 80 | // the internal shape corners. For example, for a rounded rect border 81 | // a large multiplier will always have squared off edges within the 82 | // inner section of the stroke, however, having a smaller multiplier 83 | // will still keep the rounded effect for the inner section of the 84 | // border 85 | val strokeWidth = 1.2f * borderSize 86 | inset = borderSize - strokeWidth / 2 87 | val insetSize = Size( 88 | size.width - inset * 2, 89 | size.height - inset * 2 90 | ) 91 | insetOutline = shape.createOutline(insetSize, layoutDirection, this) 92 | stroke = Stroke( 93 | strokeWidth, pathEffect = PathEffect.dashPathEffect( 94 | floatArrayOf(on.toPx(), off.toPx()) 95 | ) 96 | ) 97 | pathClip = if (outline is Outline.Rounded) { 98 | Path().apply { addRoundRect(outline.roundRect) } 99 | } else if (outline is Outline.Generic) { 100 | outline.path 101 | } else { 102 | // should not get here because we check for Outline.Rectangle 103 | // above 104 | null 105 | } 106 | 107 | insetPath = 108 | if (insetOutline is Outline.Rounded && 109 | !insetOutline.roundRect.isSimple 110 | ) { 111 | // Rounded rect with non equal corner radii needs a path 112 | // to be pre-translated 113 | Path().apply { 114 | addRoundRect(insetOutline.roundRect) 115 | translate(Offset(inset, inset)) 116 | } 117 | } else if (insetOutline is Outline.Generic) { 118 | // Generic paths must be created and pre-translated 119 | Path().apply { 120 | addPath(insetOutline.path, Offset(inset, inset)) 121 | } 122 | } else { 123 | // Drawing a round rect with equal corner radii without 124 | // usage of a path 125 | null 126 | } 127 | } 128 | } 129 | 130 | onDrawWithContent { 131 | drawContent() 132 | // Only draw the border if a have a valid stroke parameter. If we have 133 | // an invalid border size we will just draw the content 134 | if (stroke != null) { 135 | if (insetOutline != null && pathClip != null) { 136 | val isSimpleRoundRect = insetOutline is Outline.Rounded && 137 | insetOutline.roundRect.isSimple 138 | withTransform({ 139 | clipPath(pathClip) 140 | // we are drawing the round rect not as a path so we must 141 | // translate ourselves othe 142 | if (isSimpleRoundRect) { 143 | translate(inset, inset) 144 | } 145 | }) { 146 | if (isSimpleRoundRect) { 147 | // If we don't have an insetPath then we are drawing 148 | // a simple round rect with the corner radii all identical 149 | val rrect = (insetOutline as Outline.Rounded).roundRect 150 | drawRoundRect( 151 | brush = brush, 152 | topLeft = Offset(rrect.left, rrect.top), 153 | size = Size(rrect.width, rrect.height), 154 | cornerRadius = rrect.topLeftCornerRadius, 155 | style = stroke 156 | ) 157 | } else if (insetPath != null) { 158 | drawPath(insetPath, brush, style = stroke) 159 | } 160 | } 161 | // Clip rect to ensure the stroke does not extend the bounds 162 | // of the composable. 163 | clipRect { 164 | // Draw a hairline stroke to cover up non-anti-aliased pixels 165 | // generated from the clip 166 | if (isSimpleRoundRect) { 167 | val rrect = (outline as Outline.Rounded).roundRect 168 | drawRoundRect( 169 | brush = brush, 170 | topLeft = Offset(rrect.left, rrect.top), 171 | size = Size(rrect.width, rrect.height), 172 | cornerRadius = rrect.topLeftCornerRadius, 173 | style = Stroke( 174 | Stroke.HairlineWidth, 175 | pathEffect = PathEffect.dashPathEffect( 176 | floatArrayOf(on.toPx(), off.toPx()) 177 | ) 178 | ) 179 | ) 180 | } else { 181 | drawPath( 182 | pathClip, brush = brush, style = Stroke( 183 | Stroke.HairlineWidth, 184 | pathEffect = PathEffect.dashPathEffect( 185 | floatArrayOf(on.toPx(), off.toPx()) 186 | ) 187 | ) 188 | ) 189 | } 190 | } 191 | } else { 192 | // Rectangular border fast path 193 | val strokeWidth = stroke.width 194 | val halfStrokeWidth = strokeWidth / 2 195 | drawRect( 196 | brush = brush, 197 | topLeft = Offset(halfStrokeWidth, halfStrokeWidth), 198 | size = Size( 199 | size.width - strokeWidth, 200 | size.height - strokeWidth 201 | ), 202 | style = stroke 203 | ) 204 | } 205 | } 206 | } 207 | } 208 | ) 209 | }, 210 | inspectorInfo = debugInspectorInfo { 211 | name = "border" 212 | properties["width"] = width 213 | if (brush is SolidColor) { 214 | properties["color"] = brush.value 215 | value = brush.value 216 | } else { 217 | properties["brush"] = brush 218 | } 219 | properties["shape"] = shape 220 | } 221 | ) -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/NoPaddingAlertDialog.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.material.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.CompositionLocalProvider 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.graphics.Shape 10 | import androidx.compose.ui.unit.dp 11 | import androidx.compose.ui.window.Dialog 12 | import androidx.compose.ui.window.DialogProperties 13 | 14 | /** 15 | * Credits to 16 | * https://stackoverflow.com/questions/69482723/how-to-remove-padding-between-alertdialog-and-title-text-with-compose 17 | */ 18 | 19 | @Composable 20 | fun NoPaddingAlertDialog( 21 | onDismissRequest: () -> Unit, 22 | modifier: Modifier = Modifier, 23 | title: @Composable (() -> Unit)? = null, 24 | text: @Composable (() -> Unit)? = null, 25 | confirmButton: @Composable () -> Unit, 26 | dismissButton: @Composable (() -> Unit)? = null, 27 | shape: Shape = MaterialTheme.shapes.medium, 28 | backgroundColor: Color = MaterialTheme.colors.surface, 29 | contentColor: Color = contentColorFor(backgroundColor), 30 | properties: DialogProperties = DialogProperties() 31 | ) { 32 | Dialog( 33 | onDismissRequest = onDismissRequest, 34 | properties = properties 35 | ) { 36 | Surface( 37 | modifier = modifier, 38 | shape = shape, 39 | color = backgroundColor, 40 | contentColor = contentColor 41 | ) { 42 | Column( 43 | modifier = Modifier 44 | .fillMaxWidth() 45 | ) { 46 | title?.let { 47 | CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { 48 | val textStyle = MaterialTheme.typography.subtitle1 49 | ProvideTextStyle(textStyle, it) 50 | } 51 | } 52 | text?.let { 53 | CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.high) { 54 | val textStyle = MaterialTheme.typography.subtitle1 55 | ProvideTextStyle(textStyle, it) 56 | } 57 | } 58 | Box( 59 | Modifier 60 | .fillMaxWidth() 61 | .padding(all = 8.dp) 62 | ) { 63 | Row( 64 | horizontalArrangement = Arrangement.End, 65 | modifier = Modifier.fillMaxWidth() 66 | ) { 67 | dismissButton?.invoke() 68 | confirmButton() 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/OrbitalMenu.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.animation.core.LinearOutSlowInEasing 5 | import androidx.compose.animation.core.animateIntOffsetAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.clickable 9 | import androidx.compose.foundation.layout.Box 10 | import androidx.compose.foundation.layout.BoxScope 11 | import androidx.compose.foundation.layout.offset 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.material3.MaterialTheme 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.drawBehind 18 | import androidx.compose.ui.geometry.Offset 19 | import androidx.compose.ui.geometry.Size 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.graphics.drawscope.DrawStyle 22 | import androidx.compose.ui.graphics.drawscope.Stroke 23 | import androidx.compose.ui.layout.onGloballyPositioned 24 | import androidx.compose.ui.layout.positionInParent 25 | import androidx.compose.ui.platform.LocalConfiguration 26 | import androidx.compose.ui.unit.Dp 27 | import androidx.compose.ui.unit.IntOffset 28 | import androidx.compose.ui.unit.IntSize 29 | import androidx.compose.ui.unit.dp 30 | import androidx.compose.ui.zIndex 31 | import kotlin.math.cos 32 | import kotlin.math.sin 33 | 34 | enum class SatellitePosition (private val value: Int){ 35 | NORTH(0), 36 | NORTH_EAST(1), 37 | EAST(2), 38 | SOUTH_EAST(3), 39 | SOUTH(4), 40 | SOUTH_WEST(5), 41 | WEST(6), 42 | NORTH_WEST(7); 43 | 44 | operator fun inc(): SatellitePosition = SatellitePosition.values().first { x -> x.value == (this.value + 1) % SatellitePosition.values().size } 45 | operator fun plus(intValue: Int): SatellitePosition = SatellitePosition.values().first { x -> x.value == (this.value + intValue) % SatellitePosition.values().size } 46 | } 47 | 48 | data class Satellite( 49 | val satelliteKey: String, 50 | val orbit: Int, 51 | val satellitePosition: SatellitePosition, 52 | val content: @Composable BoxScope.() -> Unit, 53 | ) 54 | 55 | @Composable 56 | fun OrbitalMenu( 57 | modifier: Modifier, 58 | isExpanded: Boolean, 59 | orbitColor: Color = MaterialTheme.colorScheme.primary, 60 | orbitWidth: Dp = 2.dp, 61 | orbitStyle: DrawStyle? = null, 62 | core: @Composable BoxScope.() -> Unit, 63 | satellites: List, 64 | onCorePositioned: () -> Unit, 65 | onClickCore: () -> Unit, 66 | onClickSatellite: (Satellite) -> Unit 67 | ){ 68 | val orbitsNumber = satellites.maxOfOrNull { satellite -> satellite.orbit } ?: 0 69 | val configuration = LocalConfiguration.current 70 | var fullScreenSize by remember { mutableStateOf(Size.Zero) } 71 | var screenSize by remember { mutableStateOf(Size.Zero) } 72 | var coreSize by remember { mutableStateOf(0.dp) } 73 | var isComposableExpanded by remember { mutableStateOf(false) } 74 | var coreCenterPosition: IntOffset by remember { mutableStateOf(IntOffset.Zero) } 75 | var orbitRadius: Map by remember { 76 | mutableStateOf( 77 | mapOf(*IntRange(1, orbitsNumber).map { orbit -> orbit to 0f }.toTypedArray()) 78 | ) 79 | } 80 | 81 | Box( 82 | modifier = modifier 83 | .onGloballyPositioned { coordinates -> 84 | screenSize = Size( 85 | coordinates.size.width.toFloat(), 86 | coordinates.size.height.toFloat(), 87 | ) 88 | 89 | fullScreenSize = Size( 90 | coordinates.size.width.toFloat(), 91 | coordinates.size.height.toFloat(), 92 | ) 93 | } 94 | .drawBehind { 95 | if (isExpanded) { 96 | for (orbit in orbitRadius) { 97 | drawCircle( 98 | color = orbitColor, 99 | style = orbitStyle ?: Stroke(orbitWidth.value), 100 | radius = orbit.value 101 | ) 102 | } 103 | } 104 | } 105 | ){ 106 | Box( // Container for the core 107 | modifier = Modifier.zIndex(10f) 108 | .onGloballyPositioned { coreCoordinates -> 109 | coreSize = Dp( 110 | listOf( 111 | coreCoordinates.size.width.toFloat(), 112 | coreCoordinates.size.width.toFloat() 113 | ).maxOf { x -> x } 114 | ) 115 | coreCenterPosition = coreCoordinates 116 | .positionInParent() 117 | .let { old -> 118 | IntOffset( 119 | x = (old.x + coreCoordinates.size.width* 0.5f).toInt(), 120 | y = (old.y + coreCoordinates.size.height* 0.5f).toInt() 121 | ) 122 | } 123 | screenSize = Size( 124 | fullScreenSize.width - coreSize.value, 125 | fullScreenSize.height - coreSize.value 126 | ) 127 | orbitRadius = orbitRadius 128 | .map { (orbit, _) -> 129 | val multiplier = (orbit.toFloat() / orbitsNumber) 130 | 131 | orbit to ( 132 | (if (orbit == 1) coreSize.value else coreSize.value* 0.5f) + ( 133 | if (configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) 134 | screenSize.height 135 | else 136 | screenSize.width 137 | )* 0.5f) * multiplier 138 | } 139 | .toMap() 140 | isComposableExpanded = isExpanded && (coreCenterPosition.x != 0 || coreCenterPosition.y != 0) 141 | onCorePositioned() 142 | } 143 | .align(alignment = Alignment.Center) 144 | .clickable { onClickCore() } 145 | ){ 146 | core() 147 | } 148 | 149 | if (coreCenterPosition != IntOffset.Zero) 150 | satellites.forEach { satellite -> 151 | SatelliteComposable( 152 | modifier = Modifier.size(48.dp).zIndex(9f), 153 | isVisible = isComposableExpanded, 154 | satellite = satellite, 155 | centerPosition = coreCenterPosition, 156 | orbitRadius = orbitRadius[satellite.orbit]!!, 157 | onClick = onClickSatellite 158 | ) 159 | } 160 | } 161 | } 162 | 163 | @Composable 164 | private fun SatelliteComposable( 165 | modifier: Modifier, 166 | isVisible: Boolean, 167 | satellite: Satellite, 168 | centerPosition: IntOffset, 169 | orbitRadius: Float, 170 | onClick: (Satellite) -> Unit 171 | ) { 172 | var composableVisibility by remember { mutableStateOf(true) } 173 | var satelliteSize by remember { mutableStateOf(IntSize.Zero) } 174 | var animatedCenterPosition by remember { mutableStateOf(centerPosition) } 175 | var composablePosition by remember { mutableStateOf(animatedCenterPosition) } 176 | val position = when (satellite.satellitePosition) { 177 | SatellitePosition.NORTH -> Offset( 178 | x = centerPosition.x - satelliteSize.width.toFloat() * 0.5f, 179 | y = centerPosition.y - (orbitRadius + satelliteSize.height * 0.5f) 180 | ) 181 | SatellitePosition.EAST -> Offset( 182 | x = centerPosition.x + orbitRadius - satelliteSize.width * 0.5f, 183 | y = centerPosition.y - satelliteSize.height.toFloat() * 0.5f 184 | ) 185 | SatellitePosition.WEST -> Offset( 186 | x = centerPosition.x - (orbitRadius + satelliteSize.width * 0.5f), 187 | y = centerPosition.y - satelliteSize.height.toFloat() * 0.5f 188 | ) 189 | SatellitePosition.SOUTH -> Offset( 190 | x = centerPosition.x - satelliteSize.width * 0.5f, 191 | y = centerPosition.y + (orbitRadius - satelliteSize.height * 0.5f) 192 | ) 193 | SatellitePosition.NORTH_EAST -> Offset( 194 | x = centerPosition.x + orbitRadius * cos(45f), 195 | y = centerPosition.y - orbitRadius * sin(45f) 196 | ) 197 | SatellitePosition.SOUTH_EAST -> Offset( 198 | x = centerPosition.x + orbitRadius * cos(45f), 199 | y = centerPosition.y + orbitRadius * sin(45f) - satelliteSize.height * 0.9f 200 | ) 201 | 202 | SatellitePosition.SOUTH_WEST -> Offset( 203 | x = centerPosition.x + (orbitRadius * cos(135f) + satelliteSize.width * 0.9f), 204 | y = centerPosition.y + orbitRadius * sin(45f) - satelliteSize.height * 0.9f 205 | ) 206 | 207 | SatellitePosition.NORTH_WEST -> Offset( 208 | x = centerPosition.x + (orbitRadius * cos(135f) + satelliteSize.width * 0.9f), 209 | y = centerPosition.y - (orbitRadius * sin(45f)) 210 | ) 211 | } 212 | 213 | val animatedOffsetEffect by animateIntOffsetAsState( 214 | targetValue = composablePosition, 215 | label = satellite.satelliteKey, 216 | animationSpec = tween(700, easing = LinearOutSlowInEasing) 217 | ) { 218 | composableVisibility = isVisible 219 | } 220 | 221 | if (composableVisibility) { 222 | Box( 223 | modifier = modifier 224 | .offset { animatedOffsetEffect } 225 | .onGloballyPositioned { satelliteCoordinates -> 226 | satelliteSize = satelliteCoordinates.size 227 | animatedCenterPosition = IntOffset( 228 | (centerPosition.x - (satelliteSize.width * 0.5f)).toInt(), 229 | (centerPosition.y - (satelliteSize.height * 0.5f)).toInt() 230 | ) 231 | composablePosition = if (isVisible) 232 | IntOffset(position.x.toInt(), position.y.toInt()) 233 | else 234 | animatedCenterPosition 235 | } 236 | .background(Color.Red) 237 | .clickable { onClick(satellite) } 238 | ) { 239 | satellite.content(this) 240 | } 241 | } 242 | 243 | composablePosition = if (!isVisible) 244 | animatedCenterPosition 245 | else 246 | IntOffset(position.x.toInt(), position.y.toInt()).also { composableVisibility = true } 247 | } 248 | -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/PasswordTextField.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Visibility 9 | import androidx.compose.material.icons.filled.VisibilityOff 10 | import androidx.compose.material3.Icon 11 | import androidx.compose.material3.IconButton 12 | import androidx.compose.material3.LocalTextStyle 13 | import androidx.compose.material3.OutlinedTextField 14 | import androidx.compose.material3.OutlinedTextFieldDefaults 15 | import androidx.compose.material3.TextFieldColors 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.runtime.mutableStateOf 18 | import androidx.compose.runtime.remember 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.runtime.setValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.Shape 24 | import androidx.compose.ui.text.TextStyle 25 | import androidx.compose.ui.text.input.PasswordVisualTransformation 26 | import androidx.compose.ui.text.input.TextFieldValue 27 | import androidx.compose.ui.text.input.VisualTransformation 28 | 29 | @Composable 30 | fun OutlinedPasswordTextField( 31 | value: String, 32 | onPasswordChange: (String) -> Unit, 33 | modifier: Modifier = Modifier, 34 | enabled: Boolean = true, 35 | readOnly: Boolean = false, 36 | textStyle: TextStyle = LocalTextStyle.current, 37 | label: @Composable (() -> Unit)? = null, 38 | placeholder: @Composable (() -> Unit)? = null, 39 | leadingIcon: @Composable (() -> Unit)? = null, 40 | trailingIcon: @Composable (() -> Unit)? = null, 41 | prefix: @Composable (() -> Unit)? = null, 42 | suffix: @Composable (() -> Unit)? = null, 43 | supportingText: @Composable (() -> Unit)? = null, 44 | isError: Boolean = false, 45 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 46 | keyboardActions: KeyboardActions = KeyboardActions.Default, 47 | singleLine: Boolean = false, 48 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 49 | minLines: Int = 1, 50 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 51 | shape: Shape = OutlinedTextFieldDefaults.shape, 52 | colors: TextFieldColors = OutlinedTextFieldDefaults.colors() 53 | ){ 54 | var isPasswordVisible by remember { mutableStateOf(false) } 55 | OutlinedTextField( 56 | value = value, 57 | onValueChange = onPasswordChange, 58 | modifier = modifier, 59 | enabled = enabled, 60 | readOnly = readOnly, 61 | textStyle = textStyle, 62 | label = label, 63 | placeholder = placeholder, 64 | leadingIcon = leadingIcon, 65 | trailingIcon = { 66 | Row(verticalAlignment = Alignment.CenterVertically) { 67 | trailingIcon?.invoke() 68 | IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) { 69 | Icon( 70 | imageVector = if (isPasswordVisible) 71 | Icons.Default.VisibilityOff 72 | else 73 | Icons.Default.Visibility, 74 | contentDescription = null) 75 | } 76 | } 77 | }, 78 | visualTransformation = if(isPasswordVisible) 79 | VisualTransformation.None 80 | else 81 | PasswordVisualTransformation(), 82 | prefix = prefix, 83 | suffix = suffix, 84 | supportingText = supportingText, 85 | isError = isError, 86 | keyboardOptions = keyboardOptions, 87 | keyboardActions = keyboardActions, 88 | singleLine = singleLine, 89 | maxLines = maxLines, 90 | minLines = minLines, 91 | interactionSource = interactionSource, 92 | shape = shape, 93 | colors = colors, 94 | ) 95 | } 96 | 97 | @Composable 98 | fun OutlinedPasswordTextField( 99 | value: TextFieldValue, 100 | onPasswordChange: (TextFieldValue) -> Unit, 101 | modifier: Modifier = Modifier, 102 | enabled: Boolean = true, 103 | readOnly: Boolean = false, 104 | textStyle: TextStyle = LocalTextStyle.current, 105 | label: @Composable (() -> Unit)? = null, 106 | placeholder: @Composable (() -> Unit)? = null, 107 | leadingIcon: @Composable (() -> Unit)? = null, 108 | trailingIcon: @Composable (() -> Unit)? = null, 109 | prefix: @Composable (() -> Unit)? = null, 110 | suffix: @Composable (() -> Unit)? = null, 111 | supportingText: @Composable (() -> Unit)? = null, 112 | isError: Boolean = false, 113 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 114 | keyboardActions: KeyboardActions = KeyboardActions.Default, 115 | singleLine: Boolean = false, 116 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 117 | minLines: Int = 1, 118 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 119 | shape: Shape = OutlinedTextFieldDefaults.shape, 120 | colors: TextFieldColors = OutlinedTextFieldDefaults.colors() 121 | ){ 122 | var isPasswordVisible by remember { mutableStateOf(false) } 123 | OutlinedTextField( 124 | value = value, 125 | onValueChange = onPasswordChange, 126 | modifier = modifier, 127 | enabled = enabled, 128 | readOnly = readOnly, 129 | textStyle = textStyle, 130 | label = label, 131 | placeholder = placeholder, 132 | leadingIcon = leadingIcon, 133 | trailingIcon = { 134 | Row(verticalAlignment = Alignment.CenterVertically) { 135 | trailingIcon?.invoke() 136 | IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) { 137 | Icon( 138 | imageVector = if (isPasswordVisible) 139 | Icons.Default.VisibilityOff 140 | else 141 | Icons.Default.Visibility, 142 | contentDescription = null) 143 | } 144 | } 145 | }, 146 | visualTransformation = if(isPasswordVisible) 147 | VisualTransformation.None 148 | else 149 | PasswordVisualTransformation(), 150 | prefix = prefix, 151 | suffix = suffix, 152 | supportingText = supportingText, 153 | isError = isError, 154 | keyboardOptions = keyboardOptions, 155 | keyboardActions = keyboardActions, 156 | singleLine = singleLine, 157 | maxLines = maxLines, 158 | minLines = minLines, 159 | interactionSource = interactionSource, 160 | shape = shape, 161 | colors = colors, 162 | ) 163 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/PermissionManager.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.LaunchedEffect 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateListOf 9 | import androidx.compose.runtime.mutableStateOf 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.runtime.setValue 12 | import androidx.compose.ui.Modifier 13 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 14 | import com.google.accompanist.permissions.rememberMultiplePermissionsState 15 | 16 | @OptIn(ExperimentalPermissionsApi::class) 17 | @Composable 18 | fun PermissionManager( 19 | vararg permission: String, 20 | modifier: Modifier = Modifier, 21 | showDeniedIfNeeded: Boolean = true, 22 | onDenied: @Composable BoxScope.(Array) -> Unit, 23 | onGranted: @Composable BoxScope.(Array) -> Unit, 24 | ){ 25 | val deniedPermissions = remember { mutableStateListOf() } 26 | val grantedPermissions = remember { mutableStateListOf() } 27 | var launchedPermissionRequest by remember { 28 | mutableStateOf(false) 29 | } 30 | val permissions = rememberMultiplePermissionsState(permissions = permission.toList()){ permissionResults -> 31 | 32 | launchedPermissionRequest = true 33 | deniedPermissions.addAll(permissionResults.filterValues { isGranted -> !isGranted }.keys) 34 | grantedPermissions.addAll(permissionResults.filterValues { isGranted -> isGranted }.keys) 35 | } 36 | 37 | Box( 38 | modifier = modifier 39 | ){ 40 | if (launchedPermissionRequest) { 41 | if (grantedPermissions.isNotEmpty()) { 42 | this.onGranted(grantedPermissions.toTypedArray()) 43 | } 44 | 45 | if (!permissions.allPermissionsGranted && showDeniedIfNeeded) { 46 | this.onDenied(deniedPermissions.toTypedArray()) 47 | } 48 | }else{ 49 | LaunchedEffect(permissions){ 50 | permissions.launchMultiplePermissionRequest() 51 | } 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/PolygonalProgressBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import android.graphics.PointF 4 | import android.util.Log 5 | import androidx.compose.animation.animateColor 6 | import androidx.compose.animation.core.* 7 | import androidx.compose.foundation.Canvas 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.size 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.rotate 15 | import androidx.compose.ui.geometry.Size 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.graphics.Path 18 | import androidx.compose.ui.graphics.StrokeCap 19 | import androidx.compose.ui.graphics.StrokeJoin 20 | import androidx.compose.ui.graphics.drawscope.DrawScope 21 | import androidx.compose.ui.graphics.drawscope.Stroke 22 | import androidx.compose.ui.unit.Dp 23 | import kotlin.math.* 24 | 25 | fun Float.round(decimals: Int): Float { 26 | var multiplier = 1.0f 27 | repeat(decimals) { multiplier *= 10 } 28 | return round(this * multiplier) / multiplier 29 | } 30 | 31 | private fun pointByDistanceInsideTwoPoints(pointA: PointF, pointB: PointF, newPointDistance: Float): PointF{ 32 | val preAngle = (pointB.y - pointA.y).absoluteValue / (pointB.x - pointA.x).absoluteValue 33 | val angle = (atan(preAngle) * 180 / PI).absoluteValue 34 | val angleCA = (180 - 90 - angle) / 180 * PI 35 | val co = newPointDistance * sin(angleCA) 36 | val ca = newPointDistance * cos(angleCA) 37 | val multiplierY = if (pointB.y > pointA.y) 1 else -1 38 | val multiplierX = if (pointB.x > pointA.x) 1 else -1 39 | 40 | Log.i("Polygonal Angle", angle.toString()) 41 | 42 | return if (pointB.y == pointA.y) 43 | PointF(pointA.x + newPointDistance * multiplierX, pointA.y) 44 | else if (pointB.x == pointA.x) 45 | PointF(pointA.x, pointA.y + multiplierY * newPointDistance) 46 | else 47 | PointF(pointA.x + co.toFloat() * multiplierX, pointA.y + ca.toFloat() * multiplierY) 48 | } 49 | 50 | private fun polygonPerimeter(vertexNumber: Int, containerCircleDiameter: Float) = polygonEdgeLength(vertexNumber, containerCircleDiameter) * vertexNumber 51 | 52 | private fun polygonEdgeLength(vertexNumber: Int, containerCircleDiameter: Float): Float { 53 | val radius = containerCircleDiameter / 2f 54 | val point1 = PointF( 55 | radius * cos(2 * Math.PI * 1 / vertexNumber).toFloat(), 56 | radius * sin(2 * Math.PI * 1 / vertexNumber).toFloat() 57 | ) 58 | val point2 = PointF( 59 | radius * cos(2 * Math.PI * 2 / vertexNumber).toFloat(), 60 | radius * sin(2 * Math.PI * 2 / vertexNumber).toFloat() 61 | ) 62 | 63 | return sqrt((point2.x - point1.x).pow(2) + (point2.y - point1.y).pow(2)) 64 | } 65 | 66 | @Composable 67 | fun PolygonalProgressBar( 68 | modifier: Modifier = Modifier, 69 | rotationDegress: Float = 0f, 70 | size: Dp, 71 | backgroundColor: Color = Color.Gray, 72 | barColor: Color = Color.Green, 73 | stroke: Float = 8f, 74 | vertexNumber: Int = 2, 75 | progress: Float = 0f, 76 | isInfinite: Boolean = false, 77 | isPulsation: Boolean = false, 78 | infiniteDelayInMillis: Int = 800, 79 | pulsationTimeInMillis: Int = 2000, 80 | ){ 81 | check(progress in 0f..1f){ 82 | throw IllegalArgumentException("progress is a number between 0 and 1") 83 | } 84 | 85 | //region animation objects definition 86 | 87 | val infiniteTransition = rememberInfiniteTransition() 88 | 89 | val vertexIdx by infiniteTransition.animateValue( 90 | initialValue = 1, 91 | targetValue = vertexNumber + 1, 92 | typeConverter = Int.VectorConverter, 93 | animationSpec = infiniteRepeatable( 94 | animation = tween(infiniteDelayInMillis, easing = LinearEasing), 95 | repeatMode = RepeatMode.Restart 96 | ) 97 | ) 98 | 99 | val infiniteRotation by infiniteTransition.animateFloat( 100 | initialValue = 0f, 101 | targetValue = 360f, 102 | animationSpec = infiniteRepeatable( 103 | tween(infiniteDelayInMillis, easing = LinearEasing) 104 | ) 105 | ) 106 | 107 | val pulsationColor by infiniteTransition.animateColor( 108 | initialValue = backgroundColor, 109 | targetValue = barColor, 110 | animationSpec = infiniteRepeatable( 111 | animation = tween(pulsationTimeInMillis, easing = FastOutSlowInEasing), 112 | repeatMode = RepeatMode.Reverse 113 | ) 114 | ) 115 | 116 | //endregion 117 | 118 | Box( 119 | modifier = modifier.rotate(if (isInfinite && vertexNumber == 0) infiniteRotation else rotationDegress), 120 | contentAlignment = Alignment.Center 121 | ){ 122 | //region Background Canvas 123 | 124 | Canvas(modifier = Modifier 125 | .size(size) 126 | .align(Alignment.Center)){ 127 | 128 | val polygonPath = Path().apply { 129 | val scope: DrawScope = this@Canvas 130 | val center = scope.center 131 | check(vertexNumber == 0 || vertexNumber >= 2) 132 | when (vertexNumber){ 133 | 0 -> { 134 | if (isInfinite){ 135 | drawCircle(color = backgroundColor, radius = (scope.size.width - stroke) / 2f, center = center, style = Stroke(stroke)) 136 | drawArc( 137 | color = barColor, 138 | startAngle = 0f, 139 | sweepAngle = 90f, 140 | useCenter = false, 141 | style = Stroke(stroke), 142 | size = Size(size.toPx() - stroke / 2, size.toPx() - stroke / 2) 143 | ) 144 | }else { 145 | drawArc(color = backgroundColor, startAngle = 0f, sweepAngle = 360f, useCenter = false, style = Stroke(stroke), size = Size(size.toPx() - stroke / 2, size.toPx() - stroke / 2)) 146 | drawArc( 147 | color = barColor, 148 | startAngle = 0f, 149 | sweepAngle = 360f * progress, 150 | useCenter = false, 151 | style = Stroke(stroke), 152 | size = Size(size.toPx() - stroke / 2, size.toPx() - stroke / 2) 153 | ) 154 | } 155 | } 156 | 157 | 2 -> { 158 | moveTo(0f, stroke) 159 | lineTo(scope.size.width, stroke) 160 | } 161 | else -> { 162 | val radius = (scope.size.width - stroke) / 2f 163 | val vertex = IntRange(1, vertexNumber).map { nVertex -> 164 | PointF( 165 | radius * cos(2 * Math.PI * nVertex/vertexNumber).toFloat() + center.x, 166 | radius * sin(2 * Math.PI * nVertex/vertexNumber).toFloat() + center.y 167 | ) 168 | } 169 | 170 | val firstVertex = vertex.first() 171 | moveTo(firstVertex.x, firstVertex.y) 172 | vertex.filterIndexed { x, _ -> x > 0 }.forEach { p -> lineTo(p.x, p.y) } 173 | 174 | } 175 | } 176 | 177 | close() 178 | } 179 | 180 | // Background Path 181 | val pathColor = if (isPulsation) 182 | pulsationColor 183 | else 184 | backgroundColor 185 | 186 | this.drawPath( 187 | path = polygonPath, 188 | color = pathColor, 189 | style = Stroke(stroke, cap = StrokeCap.Round, join = StrokeJoin.Round) 190 | ) 191 | } 192 | 193 | //endregion 194 | 195 | if (isInfinite && vertexNumber > 0){ 196 | //region Infinite non circular progressbar 197 | Canvas( 198 | modifier = Modifier 199 | .size(size) 200 | .align(Alignment.Center) 201 | ){ 202 | val polygonInfinitePath = Path().apply { 203 | val scope: DrawScope = this@Canvas 204 | val center = scope.center 205 | check(vertexNumber >= 2) 206 | when (vertexNumber){ 207 | 2 -> { 208 | moveTo(0f, stroke) 209 | lineTo(scope.size.width, stroke) 210 | } 211 | else -> { 212 | val radius = (scope.size.width - stroke) / 2f 213 | val vertex = IntRange(vertexIdx - 2 , vertexIdx).map { nVertex -> 214 | PointF( 215 | radius * cos(2 * Math.PI * nVertex/vertexNumber).toFloat() + center.x, 216 | radius * sin(2 * Math.PI * nVertex/vertexNumber).toFloat() + center.y 217 | ) 218 | } 219 | 220 | val firstVertex = vertex.first() 221 | moveTo(firstVertex.x, firstVertex.y) 222 | vertex.filterIndexed { x, _ -> x > 0 }.forEach { p -> 223 | lineTo(p.x, p.y) 224 | } 225 | } 226 | } 227 | } 228 | 229 | this.drawPath( 230 | path = polygonInfinitePath, 231 | color = barColor, 232 | style = Stroke(stroke, cap = StrokeCap.Round, join = StrokeJoin.Round), 233 | ) 234 | } 235 | 236 | //endregion 237 | 238 | }else if (!isInfinite && !isPulsation && vertexNumber > 0){ 239 | 240 | //Deterministic progressBar 241 | var perimeter = polygonPerimeter( 242 | vertexNumber = vertexNumber, 243 | containerCircleDiameter = size.value 244 | ) 245 | val edgeLength = polygonEdgeLength( 246 | vertexNumber = vertexNumber, 247 | containerCircleDiameter = size.value 248 | ) 249 | 250 | val progressValue = perimeter * progress 251 | val edgeProgressNumber = ceil(progressValue / edgeLength) + 1 252 | val lengthOfLastEdge = ((edgeProgressNumber - 1) * edgeLength) - progressValue 253 | Log.i("PolygonalProgressBar", "Perimeter / Percentaje / Edge Number / Edge Length / Length Last edge: $perimeter / $progressValue (${progress * 100}) / $edgeProgressNumber / $edgeLength / $lengthOfLastEdge") 254 | Canvas( 255 | modifier = Modifier 256 | .size(size) 257 | .align(Alignment.Center) 258 | ){ 259 | val polygonDeterministicPath = Path().apply { 260 | val scope: DrawScope = this@Canvas 261 | val center = scope.center 262 | check(vertexNumber >= 2) 263 | when (vertexNumber){ 264 | 2 -> { 265 | moveTo(0f, stroke) 266 | lineTo(scope.size.width, stroke) 267 | } 268 | else -> { 269 | val radius = (scope.size.width - stroke) / 2f 270 | val vertex = IntRange(1 , edgeProgressNumber.toInt()).map { nVertex -> 271 | if (nVertex < edgeProgressNumber.toInt()) 272 | PointF( 273 | radius * cos(2 * Math.PI * nVertex/vertexNumber).toFloat() + center.x, 274 | radius * sin(2 * Math.PI * nVertex/vertexNumber).toFloat() + center.y 275 | ) 276 | else { 277 | val pointA = PointF( 278 | radius * cos(2 * Math.PI * (nVertex - 1) / vertexNumber).toFloat() + center.x, 279 | radius * sin(2 * Math.PI * (nVertex - 1) / vertexNumber).toFloat() + center.y 280 | ) 281 | val pointB = PointF( 282 | radius * cos(2 * Math.PI * nVertex / vertexNumber).toFloat() + center.x, 283 | radius * sin(2 * Math.PI * nVertex / vertexNumber).toFloat() + center.y 284 | ) 285 | 286 | Log.i("PolygonalProgressBar PreLast", pointA.toString() ) 287 | Log.i("PolygonalProgressBar Last", pointB.toString() ) 288 | 289 | val invertedSize = (pointB.y > pointA.y && pointB.x > pointA.x) || 290 | (pointB.y > pointA.y && pointB.x < pointA.x) || 291 | (pointB.y < pointA.y && pointB.x < pointA.x) 292 | //pointByDistanceInsideTwoPoints( pointA, pointB, if (invertedSize) edgeLength - lengthOfLastEdge else lengthOfLastEdge) 293 | pointByDistanceInsideTwoPoints( pointA, pointB, edgeLength - lengthOfLastEdge) 294 | } 295 | } 296 | 297 | val firstVertex = vertex.first() 298 | moveTo(firstVertex.x, firstVertex.y) 299 | vertex.filterIndexed { x, _ -> x > 0 }.forEachIndexed { index, p -> 300 | lineTo(p.x, p.y) 301 | } 302 | 303 | if (progress == 1f) 304 | close() 305 | } 306 | } 307 | } 308 | 309 | this.drawPath( 310 | path = polygonDeterministicPath, 311 | color = barColor, 312 | style = Stroke(stroke, cap = StrokeCap.Round, join = StrokeJoin.Round), 313 | ) 314 | } 315 | } 316 | } 317 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/RadioGroup.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Box 8 | import androidx.compose.foundation.layout.Column 9 | import androidx.compose.foundation.layout.Row 10 | import androidx.compose.foundation.layout.Spacer 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.RadioButton 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Alignment.Companion.CenterVertically 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.draw.clip 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.Shape 23 | import androidx.compose.ui.unit.Dp 24 | import androidx.compose.ui.unit.dp 25 | import androidx.constraintlayout.compose.ConstraintLayout 26 | import androidx.constraintlayout.compose.Dimension 27 | 28 | @Composable 29 | inline fun RadioButtonGroup( 30 | modifier: Modifier = Modifier, 31 | crossinline radioButtonLabel: @Composable (T) -> Unit = { }, 32 | crossinline radioButtonBody: @Composable (T) -> Unit = { }, 33 | radioButtonValues: Array, 34 | selectedValue: T?, 35 | borderStroke: BorderStroke? = null, 36 | dividerHeight: Dp = 4.dp, 37 | excludedValues: Array = emptyArray(), 38 | radioButtonItemShape: Shape = MaterialTheme.shapes.medium, 39 | crossinline onCheckedChanged: (T) -> Unit 40 | ) { 41 | Column( 42 | modifier = modifier 43 | ) { 44 | radioButtonValues 45 | .filter { notExcluded -> !excludedValues.any { excluded -> excluded == notExcluded } } 46 | .forEachIndexed{ index, item -> 47 | if (index > 0) 48 | Spacer(modifier = Modifier.size(dividerHeight)) 49 | 50 | ConstraintLayout( 51 | modifier = Modifier 52 | .clip(radioButtonItemShape) 53 | .border(borderStroke ?: BorderStroke(0.dp, Color.Unspecified), radioButtonItemShape) 54 | .fillMaxWidth() 55 | .clickable { onCheckedChanged(item) }, 56 | ) { 57 | val (radioButtonView, titleView, bodyView) = createRefs() 58 | RadioButton( 59 | modifier = Modifier.constrainAs(radioButtonView){ 60 | top.linkTo(parent.top) 61 | start.linkTo(parent.start) 62 | }, 63 | selected = item == selectedValue, 64 | onClick = { onCheckedChanged(item) } 65 | ) 66 | 67 | Column( 68 | modifier = Modifier.constrainAs(titleView){ 69 | top.linkTo(radioButtonView.top) 70 | bottom.linkTo(radioButtonView.bottom) 71 | start.linkTo(radioButtonView.end, 2.dp) 72 | end.linkTo(parent.end) 73 | width = Dimension.fillToConstraints 74 | }, 75 | horizontalAlignment = Alignment.Start, 76 | verticalArrangement = Arrangement.Center 77 | ) { 78 | radioButtonLabel(item) 79 | } 80 | 81 | Column( 82 | modifier = Modifier.constrainAs(bodyView){ 83 | top.linkTo(titleView.bottom) 84 | start.linkTo(titleView.start) 85 | end.linkTo(parent.end, 4.dp) 86 | width = Dimension.fillToConstraints 87 | }, 88 | horizontalAlignment = Alignment.Start, 89 | verticalArrangement = Arrangement.Top) { 90 | radioButtonBody(item) 91 | } 92 | } 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/ScaffoldWizard.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.BorderStroke 5 | import androidx.compose.foundation.ExperimentalFoundationApi 6 | import androidx.compose.foundation.layout.PaddingValues 7 | import androidx.compose.foundation.layout.WindowInsets 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.material3.Button 12 | import androidx.compose.material3.ButtonColors 13 | import androidx.compose.material3.ButtonDefaults 14 | import androidx.compose.material3.ButtonElevation 15 | import androidx.compose.material3.FabPosition 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.Scaffold 18 | import androidx.compose.material3.ScaffoldDefaults 19 | import androidx.compose.material3.contentColorFor 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.graphics.Color 27 | import androidx.compose.ui.graphics.Shape 28 | import androidx.compose.ui.unit.dp 29 | import androidx.constraintlayout.compose.ConstraintLayout 30 | import androidx.constraintlayout.compose.Dimension 31 | import androidx.compose.foundation.pager.HorizontalPager 32 | import com.google.accompanist.pager.HorizontalPagerIndicator 33 | import androidx.compose.foundation.pager.rememberPagerState 34 | import androidx.compose.runtime.LaunchedEffect 35 | import androidx.compose.runtime.mutableIntStateOf 36 | 37 | @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") 38 | @OptIn(ExperimentalFoundationApi::class) 39 | @Composable 40 | fun ScaffoldWizard( 41 | modifier: Modifier = Modifier, 42 | topBar: @Composable () -> Unit = {}, 43 | snackBarHost: @Composable () -> Unit = {}, 44 | floatingActionButton: @Composable () -> Unit = {}, 45 | previousButtonColors: ButtonColors = ButtonDefaults.buttonColors(), 46 | previousButtonShape: Shape = MaterialTheme.shapes.medium, 47 | previousButtonElevation: ButtonElevation? = null, 48 | previousButtonBorder: BorderStroke? = null, 49 | previousButtonContent: @Composable () -> Unit, 50 | nextButtonColors: ButtonColors = ButtonDefaults.buttonColors(), 51 | nextButtonShape: Shape = MaterialTheme.shapes.medium, 52 | nextButtonElevation: ButtonElevation? = null, 53 | nextButtonBorder: BorderStroke? = null, 54 | nextButtonContent: @Composable () -> Unit, 55 | finishButtonContent: @Composable () -> Unit, 56 | onPrevious: () -> Unit = {}, 57 | onNext: () -> Unit = {}, 58 | onFinish: () -> Unit = {}, 59 | pagerIndicatorActiveColor: Color = MaterialTheme.colorScheme.primary, 60 | pagerIndicatorInactiveColor: Color = MaterialTheme.colorScheme.secondary, 61 | floatingActionButtonPosition : FabPosition = FabPosition . End, 62 | containerColor: Color = MaterialTheme.colorScheme.background, 63 | contentColor: Color = contentColorFor(containerColor), 64 | contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, 65 | bottomBarPaddingValues: PaddingValues = PaddingValues(), 66 | contentPaddingValues: PaddingValues = PaddingValues(), 67 | pages: List<@Composable () -> Unit>, 68 | ){ 69 | var previousEnabled by remember { mutableStateOf(false) } 70 | var nextEnabled by remember { mutableStateOf(true) } 71 | var addPage by remember { mutableIntStateOf(0) } 72 | val pagerState = rememberPagerState(0){ 73 | pages.size 74 | } 75 | 76 | previousEnabled = pagerState.currentPage != 0 77 | nextEnabled = pagerState.currentPage != pages.size - 1 78 | 79 | Scaffold( 80 | modifier = modifier, 81 | topBar = topBar, 82 | snackbarHost = snackBarHost, 83 | floatingActionButton = floatingActionButton, 84 | floatingActionButtonPosition = floatingActionButtonPosition, 85 | containerColor = containerColor, 86 | contentColor = contentColor, 87 | contentWindowInsets = contentWindowInsets, 88 | bottomBar = { 89 | ConstraintLayout(modifier = Modifier 90 | .fillMaxWidth() 91 | .padding(bottomBarPaddingValues)) { 92 | val (previous, next) = createRefs() 93 | Button( 94 | modifier = Modifier.constrainAs(previous){ 95 | top.linkTo(parent.top) 96 | start.linkTo(parent.start) 97 | bottom.linkTo(parent.bottom) 98 | width = Dimension.percent(0.45f) 99 | }, 100 | enabled = previousEnabled, 101 | colors = previousButtonColors, 102 | shape = previousButtonShape, 103 | elevation = previousButtonElevation, 104 | border = previousButtonBorder, 105 | onClick = { 106 | if (previousEnabled) { 107 | addPage = -1 108 | onPrevious() 109 | } 110 | 111 | } 112 | ) { 113 | previousButtonContent() 114 | } 115 | 116 | Button( 117 | modifier = Modifier.constrainAs(next){ 118 | top.linkTo(parent.top) 119 | end.linkTo(parent.end) 120 | bottom.linkTo(parent.bottom) 121 | width = Dimension.percent(0.45f) 122 | }, 123 | colors = nextButtonColors, 124 | shape = nextButtonShape, 125 | elevation = nextButtonElevation, 126 | border = nextButtonBorder, 127 | onClick = { 128 | if (nextEnabled) { 129 | addPage = 1 130 | onNext() 131 | }else{ 132 | onFinish() 133 | } 134 | } 135 | ) { 136 | if (nextEnabled) 137 | nextButtonContent() 138 | else 139 | finishButtonContent() 140 | } 141 | } 142 | } 143 | ) { containerPaddingValues -> 144 | ConstraintLayout(modifier = Modifier 145 | .fillMaxSize() 146 | .padding(containerPaddingValues) 147 | .padding(contentPaddingValues), 148 | ) { 149 | val (horizontalPager, pagerIndicator) = createRefs() 150 | 151 | HorizontalPagerIndicator( 152 | pagerState = pagerState, 153 | modifier = Modifier.constrainAs(pagerIndicator){ 154 | start.linkTo(parent.start, 16.dp) 155 | end.linkTo(parent.end, 16.dp) 156 | bottom.linkTo(parent.bottom, 4.dp) 157 | }, 158 | pageCount = pages.size, 159 | activeColor = pagerIndicatorActiveColor, 160 | inactiveColor = pagerIndicatorInactiveColor, 161 | ) 162 | 163 | HorizontalPager( 164 | modifier = Modifier.constrainAs(horizontalPager){ 165 | top.linkTo(parent.top) 166 | start.linkTo(parent.start) 167 | end.linkTo(parent.end) 168 | bottom.linkTo(pagerIndicator.top, 4.dp) 169 | width = Dimension.fillToConstraints 170 | height = Dimension.fillToConstraints 171 | }, 172 | userScrollEnabled = true, 173 | state = pagerState, 174 | ){ 175 | if (!pagerState.isScrollInProgress) { 176 | addPage = 0 177 | pages[pagerState.currentPage]() 178 | } 179 | } 180 | 181 | LaunchedEffect(addPage){ 182 | pagerState.animateScrollToPage(pagerState.currentPage + addPage ) 183 | } 184 | } 185 | } 186 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/SetUiContent.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.activity.ComponentActivity 4 | import androidx.activity.compose.setContent 5 | import androidx.biometric.BiometricManager 6 | import androidx.biometric.BiometricManager.BIOMETRIC_SUCCESS 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.runtime.getValue 10 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 11 | import io.github.afalabarce.jetpackcompose.authmanager.entities.BiometricCapabilities 12 | import io.github.afalabarce.jetpackcompose.networking.NetworkStatus 13 | import io.github.afalabarce.jetpackcompose.networking.NetworkStatusTracker 14 | 15 | fun ComponentActivity.setUiContent(content: @Composable () -> Unit){ 16 | setContent { 17 | val currentNetworkStatus by NetworkStatusTracker(this@setUiContent) 18 | .networkStatus.collectAsStateWithLifecycle(initialValue = NetworkStatus.Available) 19 | val currentBiometricManager = BiometricManager.from(this@setUiContent) 20 | val canBiometricAuth = currentBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS || 21 | currentBiometricManager.canAuthenticate( 22 | BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.BIOMETRIC_STRONG 23 | ) == BIOMETRIC_SUCCESS 24 | 25 | CompositionLocalProvider ( 26 | LocalNetworkStatus provides currentNetworkStatus, 27 | LocalBiometricCapabilities provides BiometricCapabilities( 28 | canDevicePattern = currentBiometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL) == BIOMETRIC_SUCCESS, 29 | canBiometric = currentBiometricManager.canAuthenticate( 30 | BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.BIOMETRIC_STRONG 31 | ) == BIOMETRIC_SUCCESS, 32 | context = if (canBiometricAuth) this@setUiContent else null, 33 | biometricManager = if (canBiometricAuth) currentBiometricManager else null 34 | ) 35 | ){ 36 | content() 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/SpinnerSelector.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.focusable 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.fillMaxSize 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.offset 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.size 16 | import androidx.compose.foundation.rememberScrollState 17 | import androidx.compose.foundation.verticalScroll 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.rounded.ArrowDropDown 20 | import androidx.compose.material.icons.rounded.ArrowDropUp 21 | import androidx.compose.material3.Button 22 | import androidx.compose.material3.ButtonColors 23 | import androidx.compose.material3.ButtonDefaults 24 | import androidx.compose.material3.DropdownMenu 25 | import androidx.compose.material3.Icon 26 | import androidx.compose.material3.MaterialTheme 27 | import androidx.compose.material3.Text 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.runtime.mutableStateOf 31 | import androidx.compose.runtime.remember 32 | import androidx.compose.runtime.setValue 33 | import androidx.compose.ui.Alignment 34 | import androidx.compose.ui.Modifier 35 | import androidx.compose.ui.graphics.Color 36 | import androidx.compose.ui.graphics.Shape 37 | import androidx.compose.ui.graphics.vector.ImageVector 38 | import androidx.compose.ui.unit.Dp 39 | import androidx.compose.ui.unit.dp 40 | import androidx.constraintlayout.compose.ConstraintLayout 41 | 42 | @Composable 43 | fun SpinnerSelector( 44 | modifier: Modifier, 45 | readOnly: Boolean = false, 46 | hintText: String = "", 47 | label: @Composable () -> Unit = {}, 48 | colors: ButtonColors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.background), 49 | border: BorderStroke = BorderStroke(2.dp, MaterialTheme.colorScheme.primary), 50 | shape: Shape = MaterialTheme.shapes.small, 51 | componentHeight: Dp = 58.dp, 52 | height: Dp = 160.dp, 53 | selectedItem: T? = null, 54 | expandedTrailingIcon: ImageVector? = null, 55 | collapsedTrailingIcon: ImageVector? = null, 56 | trailingIconTint: Color = MaterialTheme.colorScheme.onBackground, 57 | accentColor: Color = MaterialTheme.colorScheme.primary, 58 | onBackgroundColor: Color = MaterialTheme.colorScheme.background, 59 | items: List, 60 | onSelectedItem: (T) -> Unit = {}, 61 | itemComposable: @Composable (T) -> Unit = { i -> Text(i.toString()) } 62 | ) { 63 | var expandedDropDown by remember { mutableStateOf(false) } 64 | var spinnerValue: T? by remember { mutableStateOf(null) } 65 | 66 | if (selectedItem != null) 67 | spinnerValue = selectedItem 68 | Box(modifier = modifier.height(componentHeight.plus(12.dp))){ 69 | Button( 70 | onClick = { 71 | if (!readOnly) 72 | expandedDropDown = !expandedDropDown 73 | }, 74 | modifier = modifier 75 | .height(componentHeight) 76 | .offset(y = 6.dp) 77 | .align(Alignment.BottomStart), 78 | shape = shape, 79 | colors = colors, 80 | border = border, 81 | ) { 82 | ConstraintLayout( 83 | modifier = Modifier.fillMaxSize(), 84 | ) { 85 | val (spinnerItem, spinnerIcon) = createRefs() 86 | 87 | Icon( 88 | imageVector = if (expandedDropDown) 89 | expandedTrailingIcon ?: Icons.Rounded.ArrowDropUp 90 | else 91 | collapsedTrailingIcon ?: Icons.Rounded.ArrowDropDown, 92 | contentDescription = null, 93 | tint = trailingIconTint, 94 | modifier = Modifier.size(32.dp).constrainAs(spinnerIcon){ 95 | top.linkTo(parent.top) 96 | bottom.linkTo(parent.bottom) 97 | end.linkTo(parent.end) 98 | }.focusable(false) 99 | .clickable(true) { 100 | if (!readOnly) 101 | expandedDropDown = !expandedDropDown 102 | } 103 | ) 104 | 105 | Column( 106 | modifier = Modifier 107 | .constrainAs(spinnerItem){ 108 | top.linkTo(parent.top) 109 | bottom.linkTo(parent.bottom) 110 | start.linkTo(parent.start) 111 | end.linkTo(spinnerIcon.start) 112 | } 113 | .fillMaxWidth(0.93f) 114 | .fillMaxSize(), 115 | verticalArrangement = Arrangement.Center, 116 | horizontalAlignment = Alignment.Start 117 | ) { 118 | if (spinnerValue == null) 119 | label() 120 | else 121 | itemComposable(spinnerValue!!) 122 | } 123 | } 124 | 125 | DropdownMenu(expanded = expandedDropDown, 126 | onDismissRequest = { expandedDropDown = false } 127 | ) { 128 | Column(modifier = Modifier 129 | .fillMaxWidth() 130 | .height(height) 131 | .padding(8.dp) 132 | .verticalScroll(rememberScrollState())) { 133 | items.forEach { item -> 134 | Column(modifier = Modifier 135 | .fillMaxWidth() 136 | .clickable { 137 | spinnerValue = item 138 | expandedDropDown = false 139 | onSelectedItem(item) 140 | }) { 141 | itemComposable(item) 142 | } 143 | } 144 | } 145 | } 146 | } 147 | 148 | if (spinnerValue != null && hintText.isNotEmpty()){ 149 | Column( 150 | modifier = modifier 151 | .height(componentHeight.plus(12.dp)) 152 | .padding(start = 16.dp) 153 | .background(Color.Transparent), 154 | verticalArrangement = Arrangement.Top 155 | 156 | ) { 157 | Text( 158 | text = hintText, 159 | style = MaterialTheme.typography.labelSmall, 160 | color = accentColor, 161 | modifier = Modifier 162 | .background(onBackgroundColor) 163 | .padding(horizontal = 4.dp) 164 | ) 165 | } 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/SvgPickerSelector.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import android.os.Environment 4 | import androidx.annotation.StringRes 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.lazy.grid.GridCells 9 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 10 | import androidx.compose.foundation.lazy.grid.items 11 | import androidx.compose.foundation.lazy.grid.rememberLazyGridState 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.Button 14 | import androidx.compose.material3.ButtonDefaults 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.* 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.platform.LocalContext 21 | import androidx.compose.ui.res.stringResource 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.dp 24 | import androidx.lifecycle.ViewModel 25 | import androidx.lifecycle.viewModelScope 26 | import coil.compose.AsyncImage 27 | import coil.decode.SvgDecoder 28 | import coil.request.ImageRequest 29 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 30 | import com.google.accompanist.permissions.PermissionStatus 31 | import com.google.accompanist.permissions.rememberPermissionState 32 | import kotlinx.coroutines.Dispatchers 33 | import kotlinx.coroutines.flow.MutableStateFlow 34 | import kotlinx.coroutines.flow.StateFlow 35 | import kotlinx.coroutines.flow.update 36 | import kotlinx.coroutines.launch 37 | import kotlinx.coroutines.withContext 38 | import java.io.File 39 | import java.nio.file.* 40 | import java.util.* 41 | 42 | private class SvgPickerViewModel(private val svgIconsPaths: List = listOf( 43 | "${Environment.getExternalStorageDirectory().absolutePath}${File.separator}${Environment.DIRECTORY_PICTURES}", 44 | "${Environment.getExternalStorageDirectory().absolutePath}${File.separator}${Environment.DIRECTORY_DOWNLOADS}" 45 | )): ViewModel(){ 46 | private val svgIconsPathsWatcher = this.svgIconsPaths.map { path -> 47 | File(path).toPath() 48 | } 49 | private val pathWatcherService: WatchService = FileSystems.getDefault().newWatchService() 50 | private val _pictures by lazy { MutableStateFlow>(listOf()) } 51 | val pictures: StateFlow> 52 | get() = this._pictures 53 | 54 | init { 55 | this.svgIconsPathsWatcher.forEach { watcherPath -> 56 | watcherPath.register( 57 | this.pathWatcherService, 58 | StandardWatchEventKinds.ENTRY_CREATE, 59 | StandardWatchEventKinds.ENTRY_DELETE, 60 | StandardWatchEventKinds.ENTRY_MODIFY 61 | ) 62 | } 63 | 64 | this._pictures.update { 65 | this@SvgPickerViewModel.svgIconsPathsWatcher.flatMap { path -> 66 | path.toFile().listFiles()?.toList() ?: listOf() 67 | }.filter { f -> f.extension.lowercase(Locale.getDefault()) == "svg" }.map { svg -> 68 | svg.readBytes() 69 | }.distinct() 70 | } 71 | 72 | } 73 | 74 | fun refreshWatcher(){ 75 | viewModelScope.launch(Dispatchers.IO) { 76 | this@SvgPickerViewModel.folderWatcher { svgPictures -> 77 | this@SvgPickerViewModel._pictures.update { 78 | svgPictures.filter { f -> f.extension.lowercase(Locale.getDefault()) == "svg" }.map { svg -> 79 | svg.readBytes() 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | private suspend fun folderWatcher(onWatch: (List) -> Unit) { 87 | withContext(Dispatchers.IO) { 88 | while (true) { 89 | val key = this@SvgPickerViewModel.pathWatcherService.take() 90 | 91 | key.pollEvents().filter { evt -> 92 | listOf(StandardWatchEventKinds.ENTRY_CREATE, StandardWatchEventKinds.ENTRY_DELETE, StandardWatchEventKinds.ENTRY_MODIFY).any { x -> 93 | x == evt.kind() 94 | } && evt is WatchEvent<*> 95 | } .forEach { _ -> 96 | onWatch( 97 | this@SvgPickerViewModel.svgIconsPathsWatcher.flatMap { path -> 98 | path.toFile().listFiles()?.toList() ?: listOf() 99 | } 100 | ) 101 | key.reset() 102 | 103 | return@forEach 104 | } 105 | } 106 | } 107 | } 108 | } 109 | 110 | @OptIn(ExperimentalPermissionsApi::class) 111 | @Composable 112 | fun SvgPickerSelector( 113 | modifier: Modifier = Modifier, 114 | thumbnailSize: Dp = 128.dp, 115 | @StringRes giveMeMoreIconsTitle: Int = R.string.give_me_more_icons, 116 | svgIconsPaths: List = listOf( 117 | "${Environment.getExternalStorageDirectory().absolutePath}${File.separator}${Environment.DIRECTORY_PICTURES}", 118 | "${Environment.getExternalStorageDirectory().absolutePath}${File.separator}${Environment.DIRECTORY_DOWNLOADS}" 119 | ), 120 | thumbnailPadding: PaddingValues = PaddingValues(4.dp), 121 | onClickGiveMoreIcons: () -> Unit, 122 | onClickedItem: (ByteArray) -> Unit 123 | ){ 124 | val viewModel by remember{ mutableStateOf(SvgPickerViewModel(svgIconsPaths)) } 125 | val grantedPermission = rememberPermissionState(permission = android.Manifest.permission.READ_EXTERNAL_STORAGE) 126 | if (grantedPermission.status != PermissionStatus.Granted){ 127 | LaunchedEffect(key1 = "QueryReadStoragePermission",){ 128 | grantedPermission.launchPermissionRequest() 129 | } 130 | }else{ 131 | viewModel.refreshWatcher() 132 | } 133 | 134 | val svgPictures by viewModel.pictures.collectAsState() 135 | 136 | Column( 137 | modifier = modifier.fillMaxHeight(0.75f), 138 | horizontalAlignment = Alignment.CenterHorizontally, 139 | ) { 140 | Spacer(modifier = Modifier.height(8.dp)) 141 | Button( 142 | modifier = Modifier.fillMaxWidth(0.8f), 143 | colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.primary), 144 | onClick = onClickGiveMoreIcons, 145 | ) { 146 | Text(text = stringResource(id = giveMeMoreIconsTitle)) 147 | } 148 | Spacer(modifier = Modifier.height(8.dp)) 149 | LazyVerticalGrid( 150 | modifier = modifier.fillMaxSize(), 151 | columns = GridCells.Adaptive(thumbnailSize), 152 | contentPadding = thumbnailPadding, 153 | state = rememberLazyGridState() 154 | ){ 155 | items(svgPictures){ picture -> 156 | AsyncImage( 157 | model = ImageRequest.Builder(LocalContext.current) 158 | .data(picture) 159 | .decoderFactory(SvgDecoder.Factory()) 160 | .build(), 161 | contentDescription = null, 162 | modifier = Modifier 163 | .size(thumbnailSize) 164 | .padding(4.dp) 165 | .border( 166 | width = 1.dp, 167 | color = MaterialTheme.colorScheme.onBackground, 168 | shape = RoundedCornerShape(12.dp) 169 | ) 170 | .clickable { 171 | onClickedItem(picture) 172 | } 173 | ) 174 | } 175 | } 176 | } 177 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/SwipeableCard.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalMaterialApi::class) 2 | 3 | package io.github.afalabarce.jetpackcompose 4 | 5 | import androidx.compose.foundation.BorderStroke 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.gestures.Orientation 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.material.* 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.currentRecomposeScope 12 | import androidx.compose.runtime.rememberCoroutineScope 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.graphics.Shape 17 | import androidx.compose.ui.graphics.vector.ImageVector 18 | import androidx.compose.ui.platform.LocalDensity 19 | import androidx.compose.ui.unit.* 20 | import kotlinx.coroutines.* 21 | import kotlin.math.roundToInt 22 | 23 | data class SwipeAction(val order: Int, val key: String, val title: String, val imageVector: ImageVector, val color: Color, val tint: Color, val dockLeft: Boolean) 24 | 25 | private fun actionStateValue(swipeActions: Array): Int{ 26 | var returnValue = 0 27 | 28 | val withLeftActions = swipeActions.any { x -> x.dockLeft } 29 | val withRightActions = swipeActions.any { x -> !x.dockLeft } 30 | 31 | if (withLeftActions && !withRightActions) 32 | returnValue = 1 33 | else if (!withLeftActions && withRightActions) 34 | returnValue = 3 35 | else if (withLeftActions && withRightActions) 36 | returnValue = 2 37 | 38 | return returnValue 39 | } 40 | 41 | fun getAnchorMap(density: Density, buttonWidth: Dp, swipeActions: Array): Map{ 42 | val actionState = actionStateValue(swipeActions) 43 | val sizePx = with(density) { 44 | buttonWidth.times(swipeActions.count { x -> x.dockLeft }).toPx() 45 | } 46 | val sizePxR = with(density) { 47 | buttonWidth.times(swipeActions.count { x -> !x.dockLeft }).toPx() 48 | } 49 | 50 | return when (actionState) { 51 | 1 -> mapOf(0f to 0, sizePx to 1) 52 | 2 -> mapOf(0f to 0, sizePx to 1, -sizePxR to 2) 53 | 3 -> mapOf(0f to 0, -sizePxR to 2) 54 | else -> mapOf(0f to 0) 55 | } 56 | } 57 | 58 | /** 59 | * Swipeable Horizontal Card with custom actions 60 | * @param modifier Modifier to be applied to the layout of the card. 61 | * @param shape Defines the card's shape as well its shadow. A shadow is only 62 | * displayed if the [elevation] is greater than zero. 63 | * @param backgroundColor The background color. 64 | * @param contentColor The preferred content color provided by this card to its children. 65 | * Defaults to either the matching content color for [backgroundColor], or if [backgroundColor] 66 | * is not a color from the theme, this will keep the same value set above this card. 67 | * @param border Optional border to draw on top of the card 68 | * @param elevation The z-coordinate at which to place this card. This controls 69 | * the size of the shadow below the card. 70 | * @param buttonWidth SwipeActions fixed Width 71 | * @param swipeActions SwipeActions Array with left/right docking 72 | * @param onClickSwipeAction Raised event on SwipeAction tap 73 | * @param swipeBackColor Backcolor of swipe area 74 | * @param content Card Content 75 | */ 76 | @Composable 77 | fun SwipeableCard(modifier: Modifier = Modifier, 78 | shape: Shape = MaterialTheme.shapes.medium, 79 | backgroundColor: Color = MaterialTheme.colors.surface, 80 | buttonWidth: Dp, 81 | swipeBackColor: Color = Color.Transparent, 82 | contentColor: Color = contentColorFor(backgroundColor), 83 | border: BorderStroke? = null, 84 | elevation: Dp = 1.dp, 85 | swipeActions: Array = arrayOf(), 86 | onClickSwipeAction: (SwipeAction) -> Unit = { }, 87 | unSwipeOnClick: Boolean = true, 88 | content: @Composable () -> Unit) = SwipeableCard( 89 | modifier = modifier, 90 | shape = shape, 91 | backgroundColor = backgroundColor, 92 | buttonWidth = buttonWidth, 93 | swipeBackColor = swipeBackColor, 94 | contentColor = contentColor, 95 | border = border, 96 | anchors = getAnchorMap(LocalDensity.current, buttonWidth, swipeActions), 97 | elevation = elevation, 98 | swipeActions = swipeActions, 99 | onClickSwipeAction = onClickSwipeAction, 100 | unSwipeOnClick = unSwipeOnClick, 101 | content = content 102 | ) 103 | 104 | /** 105 | * Swipeable Horizontal Card with custom actions 106 | * @param modifier Modifier to be applied to the layout of the card. 107 | * @param shape Defines the card's shape as well its shadow. A shadow is only 108 | * displayed if the [elevation] is greater than zero. 109 | * @param backgroundColor The background color. 110 | * @param contentColor The preferred content color provided by this card to its children. 111 | * Defaults to either the matching content color for [backgroundColor], or if [backgroundColor] 112 | * is not a color from the theme, this will keep the same value set above this card. 113 | * @param border Optional border to draw on top of the card 114 | * @param elevation The z-coordinate at which to place this card. This controls 115 | * the size of the shadow below the card. 116 | * @param buttonWidth SwipeActions fixed Width 117 | * @param swipeActions SwipeActions Array with left/right docking 118 | * @param onClickSwipeAction Raised event on SwipeAction tap 119 | * @param swipeBackColor Backcolor of swipe area 120 | * @param content Card Content 121 | */ 122 | @Composable 123 | fun SwipeableCard(modifier: Modifier = Modifier, 124 | shape: Shape = MaterialTheme.shapes.medium, 125 | backgroundColor: Color = MaterialTheme.colors.surface, 126 | buttonWidth: Dp, 127 | anchors: Map = mapOf(0f to 0), 128 | swipeBackColor: Color = Color.Transparent, 129 | contentColor: Color = contentColorFor(backgroundColor), 130 | unSwipeOnClick: Boolean = true, 131 | border: BorderStroke? = null, 132 | elevation: Dp = 1.dp, 133 | swipeActions: Array = arrayOf(), 134 | onClickSwipeAction: (SwipeAction) -> Unit = { }, 135 | content: @Composable () -> Unit) { 136 | val swipeableState = rememberSwipeableState(0) 137 | val coroutineScope = rememberCoroutineScope() 138 | Box( 139 | modifier = modifier 140 | .background(swipeBackColor) 141 | .padding(0.dp) 142 | .swipeable( 143 | state = swipeableState, 144 | anchors = anchors, 145 | orientation = Orientation.Horizontal, 146 | thresholds = { _, _ -> FractionalThreshold(0.3f) }) 147 | ) { 148 | if (swipeActions.isNotEmpty()) { 149 | Row(modifier = Modifier.fillMaxSize()) { 150 | if (swipeActions.any { sw -> sw.dockLeft }) { 151 | Row(modifier = Modifier.fillMaxHeight()) { 152 | swipeActions.filter { x -> x.dockLeft }.sortedBy { o -> o.order } 153 | .map { action -> 154 | Button( 155 | onClick = { 156 | onClickSwipeAction(action) 157 | if (unSwipeOnClick){ 158 | coroutineScope.launch { 159 | launch { 160 | withContext(Dispatchers.Main){ 161 | swipeableState.animateTo(0) 162 | } 163 | } 164 | } 165 | } 166 | }, 167 | modifier = Modifier 168 | .fillMaxHeight() 169 | .width(buttonWidth), 170 | colors = ButtonDefaults.buttonColors(backgroundColor = action.color) 171 | ) { 172 | Column( 173 | modifier = Modifier.fillMaxSize(), 174 | verticalArrangement = Arrangement.Center, 175 | horizontalAlignment = Alignment.CenterHorizontally 176 | ) { 177 | Icon( 178 | imageVector = action.imageVector, 179 | contentDescription = null, 180 | modifier = Modifier.size(buttonWidth.div(2)), 181 | tint = action.tint 182 | ) 183 | Text( 184 | text = action.title, 185 | fontSize = 10.sp, 186 | color = action.tint 187 | ) 188 | } 189 | } 190 | } 191 | } 192 | } 193 | 194 | if (swipeActions.any { sw -> !sw.dockLeft }) { 195 | Row( 196 | modifier = Modifier.fillMaxSize(), 197 | horizontalArrangement = Arrangement.End 198 | ) { 199 | swipeActions.filter { x -> !x.dockLeft }.sortedBy { o -> o.order } 200 | .map { action -> 201 | Button( 202 | onClick = { 203 | onClickSwipeAction(action) 204 | if (unSwipeOnClick){ 205 | coroutineScope.launch { 206 | launch { 207 | withContext(Dispatchers.Main){ 208 | swipeableState.animateTo(0) 209 | } 210 | } 211 | } 212 | } 213 | }, 214 | modifier = Modifier 215 | .fillMaxHeight() 216 | .width(buttonWidth), 217 | colors = ButtonDefaults.buttonColors(backgroundColor = action.color) 218 | ) { 219 | Column( 220 | modifier = Modifier.fillMaxSize(), 221 | verticalArrangement = Arrangement.Center, 222 | horizontalAlignment = Alignment.CenterHorizontally 223 | ) { 224 | Icon( 225 | imageVector = action.imageVector, 226 | contentDescription = null, 227 | modifier = Modifier.size(buttonWidth.div(2)), 228 | tint = action.tint 229 | ) 230 | Text( 231 | text = action.title, 232 | fontSize = 10.sp, 233 | color = action.tint 234 | ) 235 | } 236 | } 237 | } 238 | } 239 | } 240 | } 241 | } 242 | 243 | Card( 244 | modifier = Modifier 245 | .fillMaxSize() 246 | .padding(0.dp) 247 | .offset { 248 | IntOffset( 249 | if (swipeActions.isEmpty()) 0 else swipeableState.offset.value.roundToInt(), 250 | 0 251 | ) 252 | }, 253 | shape = shape, 254 | backgroundColor = backgroundColor, 255 | contentColor = contentColor, 256 | border = border, 257 | elevation = elevation, 258 | content = content 259 | ) 260 | } 261 | 262 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/ViewModelService.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose 2 | 3 | import androidx.lifecycle.* 4 | import androidx.lifecycle.LifecycleService 5 | import kotlinx.coroutines.CoroutineScope 6 | import kotlinx.coroutines.Dispatchers 7 | 8 | open class ViewModelService: LifecycleService(), 9 | ViewModelStoreOwner, 10 | HasDefaultViewModelProviderFactory, CoroutineScope by CoroutineScope(Dispatchers.IO) { 11 | private val mViewModelStore: ViewModelStore = ViewModelStore() 12 | 13 | override val viewModelStore: ViewModelStore 14 | get() = mViewModelStore 15 | 16 | private var mDefaultViewModelProviderFactory: ViewModelProvider.Factory? = null 17 | override val defaultViewModelProviderFactory: ViewModelProvider.Factory 18 | get() { 19 | if (mDefaultViewModelProviderFactory == null) 20 | mDefaultViewModelProviderFactory = ViewModelProvider.AndroidViewModelFactory() 21 | 22 | return mDefaultViewModelProviderFactory!! 23 | } 24 | 25 | override fun onCreate() { 26 | super.onCreate() 27 | lifecycle.addObserver(object : LifecycleEventObserver { 28 | override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { 29 | if (source.lifecycle.currentState === Lifecycle.State.DESTROYED) { 30 | mViewModelStore.clear() 31 | source.lifecycle.removeObserver(this) 32 | } 33 | } 34 | }) 35 | } 36 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/Authenticator.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager 2 | 3 | import android.accounts.* 4 | import android.content.Context 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.util.Base64 8 | import android.widget.Toast 9 | import com.google.gson.GsonBuilder 10 | import io.github.afalabarce.jetpackcompose.authmanager.entities.IUser 11 | import io.github.afalabarce.jetpackcompose.authmanager.enums.LoginType 12 | import io.github.afalabarce.jetpackcompose.authmanager.enums.Operation 13 | import io.github.afalabarce.jetpackcompose.authmanager.exceptions.UnsupportedAccountTypeException 14 | 15 | /** 16 | * With this class you can manage the creation and update of your app user accounts. 17 | * 18 | * Usage: 19 | * 20 | * 1. Create your own class YourOwnAuthenticatorService inherits from Service. 21 | * 22 | * 1.1. Override onBind method, with Authenticator implementation: 23 | * 24 | * override fun onBind(intent: Intent?) = Authenticator( 25 | * this, 26 | * YourLoginActivity::class.java, 27 | * R.string.your_account_type_name_resource_id, 28 | * R.string.your_custom_message_on_single_account_error, 29 | * R.string.your_custom_message_on_unsupported_account_error, 30 | * R.string.your_custom_message_on_unsupported_token_error, 31 | * R.string.your_custom_message_on_unsupported_features_error, 32 | * true/false // true if only one account is allowed, false if app are designed to multiple accounts 33 | * ) 34 | * 35 | * 2. Create your own LoginActivity. This activity need to handle some extras from intent: 36 | * 37 | * 2.1. At your onCreate method, handle some values from activity's intent, to prepare for account creation, from app or from account manager: 38 | * 39 | * 2.1.1. Instantiate is own Authenticator (like 1.1). 40 | * 2.1.2. Load all data from this.intent.extras: 41 | * this.fromApp = try{ (this.intent.extras!![Authenticator.ACTION_LOGIN_TYPE] as AuthenticatorLoginType) == AuthenticatorLoginType.App} catch(_: Exception){ false } 42 | * this.loginUser = try{ this.intent.getSerializableExtra(Authenticator.KEY_ACCOUNT) as YourAppAccountIUser }catch(_: Exception { null } 43 | * 2.1.3. Usually, if you create your login data from app, you need to clean all fields. 44 | * 2.1.4. Usually, if you update your login data from app, you need to load persisted data at this.loginUser (2.1.2 extracted data) 45 | * 2.1.5. If login process is successful, you can persist your user account info: 46 | * this.authenticator.saveUserAccount(this, R.string.your_account_type_name_resource_id, this.loginUser) 47 | * 48 | * 3. At your AndroidManifest.xml 49 | * 50 | * 3.1. Add some needed permissions: 51 | * 52 | * 53 | * 54 | * 55 | * 56 | * 57 | * 58 | * 3.2. Add a reference to your YourOwnAuthenticatorService into section 59 | * 60 | * 61 | * 62 | * 63 | * 64 | * 67 | * 68 | * 69 | * 4. Add a resource to xml/authenticator.xml, with this content: 70 | * 71 | * <?xml version="1.0" encoding="UTF-8"?> 72 | * 73 | * <account-authenticator xmlns:android="http://schemas.android.com/apk/res/android" 74 | * 75 | * android:accountType="@string/your_account_type_name_resource_id" 76 | * android:icon="@mipmap/ic_launcher" 77 | * android:label="@string/app_name" 78 | * android:smallIcon="@mipmap/ic_launcher"> 79 | * </account-authenticator> 80 | * 81 | * AND THAT'S ALL!! 82 | */ 83 | class Authenticator @JvmOverloads constructor( 84 | val context: Context, 85 | val loginActivityClass: Class<*>, 86 | val accountTypeResourceId: Int, 87 | private val errorOnlyOneAccountAllowedMessageId: Int, 88 | private val unsupportedAccountTypeExceptionResId: Int, 89 | private val unsupportedAuthTokenTypeExceptionResId: Int, 90 | private val unsupportedFeaturesExceptionResId: Int, 91 | val onlyOneAccount: Boolean = false 92 | ) : AbstractAccountAuthenticator(context) { 93 | companion object{ 94 | const val ACTION_OPERATION = "ActionOperation" 95 | const val ACTION_LOGIN_TYPE = "ActionLoginType" 96 | const val KEY_AUTH_TOKEN_TYPE = "AuthTokenType" 97 | const val KEY_REQUIRED_FEATURES = "RequiredFeatures" 98 | const val KEY_LOGIN_OPTIONS = "LoginOptions" 99 | const val KEY_ACCOUNT = "Account" 100 | 101 | /** 102 | * Get all user accounts register into the system of provided account type 103 | */ 104 | fun loadUserAccounts(context: Context, accountTypeResId: Int): List{ 105 | try { 106 | val manager = AccountManager.get(context) 107 | return manager.getAccountsByType(context.getString(accountTypeResId)).asList() 108 | }catch (_: Exception){ 109 | 110 | } 111 | 112 | return listOf() 113 | } 114 | 115 | /** 116 | * Gets system user account identified by their account type and name 117 | */ 118 | fun getUserAccount(context: Context, accountTypeResId: Int, accountName: String): Account?{ 119 | try{ 120 | return Authenticator.loadUserAccounts( 121 | context = context, 122 | accountTypeResId = accountTypeResId 123 | ).firstOrNull { x -> x.name.lowercase() == accountName.lowercase() } 124 | }catch (_: Exception){ 125 | 126 | } 127 | 128 | return null 129 | } 130 | 131 | /** 132 | * Gets system user account identified by their account type and name, returns logic accound data 133 | */ 134 | inline fun getUserAccount(context: Context, accountTypeResId: Int, accountName: String): T?{ 135 | try{ 136 | val manager = AccountManager.get(context) 137 | val account = Authenticator.loadUserAccounts( 138 | context = context, 139 | accountTypeResId = accountTypeResId 140 | ).firstOrNull { x -> x.name.lowercase() == accountName.lowercase() } 141 | val accountData = Base64.decode(manager.getPassword(account), Base64.DEFAULT) 142 | return GsonBuilder().create().fromJson(String(accountData), T::class.java) 143 | }catch (_: Exception){ 144 | 145 | } 146 | 147 | return null 148 | } 149 | 150 | /** 151 | * Saves user account into the android Account Manager 152 | */ 153 | fun saveUserAccount(context: Context, accountTypeResId: Int, userData: T): Boolean{ 154 | 155 | try { 156 | val manager = AccountManager.get(context) 157 | val serializedUser = Base64.encodeToString(GsonBuilder().create().toJson(userData).toByteArray(Charsets.UTF_8), Base64.DEFAULT) 158 | val currentAccount = getUserAccount( 159 | context = context, 160 | accountTypeResId = accountTypeResId, 161 | accountName = userData.userName 162 | ) 163 | 164 | return if (currentAccount == null){ 165 | val newAccount = Account(userData.userName, context.getString(accountTypeResId)) 166 | manager.addAccountExplicitly(newAccount, serializedUser, Bundle.EMPTY) 167 | }else{ 168 | manager.setPassword(currentAccount, serializedUser) 169 | true 170 | } 171 | 172 | }catch (_: Exception){ 173 | 174 | } 175 | 176 | return false 177 | } 178 | } 179 | 180 | @Throws(UnsupportedAccountTypeException::class) 181 | private fun validateAccountType(accountType: String){ 182 | if (accountType != this.context.getString(this.accountTypeResourceId)) 183 | throw UnsupportedAccountTypeException(this.context, this.unsupportedAccountTypeExceptionResId) 184 | } 185 | 186 | override fun addAccount( 187 | accountAuthResponse: AccountAuthenticatorResponse?, 188 | pAccountType: String?, 189 | pAuthTokenType: String?, 190 | pRequiredFeatures: Array?, 191 | pLoginOptions: Bundle? 192 | ): Bundle { 193 | if (this.onlyOneAccount && loadUserAccounts(this.context, this.accountTypeResourceId).isNotEmpty()){ 194 | Toast.makeText(this.context, this.errorOnlyOneAccountAllowedMessageId, Toast.LENGTH_LONG).show() 195 | return Bundle.EMPTY 196 | } 197 | 198 | try{ 199 | validateAccountType(pAccountType ?: this.context.getString(this.accountTypeResourceId)) 200 | 201 | val createIntent = Intent( 202 | this.context, 203 | this.loginActivityClass 204 | ).apply { 205 | putExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE, accountAuthResponse) 206 | putExtra(AccountManager.KEY_ACCOUNT_TYPE, this@Authenticator.context.getString(this@Authenticator.accountTypeResourceId)); 207 | putExtra(KEY_AUTH_TOKEN_TYPE, pAuthTokenType); 208 | putExtra(KEY_LOGIN_OPTIONS, pLoginOptions); 209 | putExtra(Authenticator.ACTION_OPERATION, Operation.NewAccount); 210 | putExtra(Authenticator.ACTION_LOGIN_TYPE, LoginType.Authenticator); 211 | } 212 | 213 | return Bundle().apply { putParcelable(AccountManager.KEY_INTENT, createIntent) } 214 | }catch (_: Exception){ 215 | 216 | } 217 | 218 | return Bundle.EMPTY 219 | } 220 | 221 | @Throws(NetworkErrorException::class) 222 | override fun updateCredentials( 223 | accountAuthResponse: AccountAuthenticatorResponse?, 224 | account: Account?, 225 | authTokenType: String?, 226 | loginOptions: Bundle? 227 | ): Bundle { 228 | try{ 229 | val updateIntent = Intent( 230 | this.context, 231 | this.loginActivityClass 232 | ).apply { 233 | putExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE, accountAuthResponse) 234 | putExtra(AccountManager.KEY_ACCOUNT_TYPE, this@Authenticator.context.getString(this@Authenticator.accountTypeResourceId)); 235 | putExtra(KEY_AUTH_TOKEN_TYPE, authTokenType); 236 | putExtra(KEY_LOGIN_OPTIONS, loginOptions); 237 | putExtra(Authenticator.ACTION_OPERATION, Operation.UpdateAccount); 238 | putExtra(Authenticator.ACTION_LOGIN_TYPE, LoginType.Authenticator); 239 | putExtra(Authenticator.KEY_ACCOUNT, account); 240 | } 241 | 242 | return Bundle().apply { putParcelable(AccountManager.KEY_INTENT, updateIntent) } 243 | }catch (_: Exception){ 244 | 245 | } 246 | 247 | return Bundle.EMPTY 248 | } 249 | 250 | //region Unnecessary but overridable functions 251 | 252 | override fun editProperties(p0: AccountAuthenticatorResponse?, p1: String?): Bundle? = null 253 | 254 | override fun confirmCredentials(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Bundle?): Bundle? = null 255 | 256 | override fun getAuthToken(p0: AccountAuthenticatorResponse?, p1: Account?, p2: String?, p3: Bundle?): Bundle? = null 257 | 258 | override fun getAuthTokenLabel(p0: String?): String? = null 259 | 260 | override fun hasFeatures(p0: AccountAuthenticatorResponse?, p1: Account?, p2: Array?): Bundle? = null 261 | 262 | //endregion 263 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/entities/BiometricCapabilities.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.entities 2 | 3 | import android.content.Context 4 | import androidx.appcompat.app.AppCompatActivity 5 | import androidx.biometric.BiometricManager 6 | import androidx.biometric.BiometricPrompt 7 | import androidx.core.content.ContextCompat 8 | import androidx.core.os.CancellationSignal 9 | import androidx.fragment.app.FragmentActivity 10 | 11 | 12 | class BiometricCapabilities( 13 | val canDevicePattern: Boolean = false, 14 | val canBiometric: Boolean = false, 15 | private val context: Context? = null, 16 | private val biometricManager: BiometricManager? = null 17 | ){ 18 | /** 19 | * indicate if current device has biometric (or pattern, pin, etc) authentication capabilities 20 | */ 21 | val canBiometricAuthentication 22 | get() = this.canBiometric || this.canDevicePattern 23 | 24 | /** 25 | * Show (if applicable) an authentication dialog 26 | * @param title title for biometric authentication dialog 27 | * @param subTitle subtitle for biometric authentication dialog 28 | * @param description description text for biometric authentication dialog 29 | * @param negativeText negativeText for cancel biometric authentication 30 | * @param onBiometricAuthentication lambda function with the result of authentication 31 | */ 32 | fun showBiometricPrompt( 33 | title: String, 34 | subTitle: String = "", 35 | description: String, 36 | negativeText: String, 37 | onBiometricAuthentication: (isSuccess: Boolean, errorCode: Int, errorDescription: String) -> Unit = { _, _, _ -> } 38 | ){ 39 | if (biometricManager != null && context != null){ 40 | val promptInfo = BiometricPrompt.PromptInfo.Builder() 41 | .setAllowedAuthenticators( 42 | if (canBiometric) 43 | BiometricManager.Authenticators.BIOMETRIC_WEAK or BiometricManager.Authenticators.BIOMETRIC_STRONG 44 | else 45 | BiometricManager.Authenticators.DEVICE_CREDENTIAL 46 | ) 47 | .setTitle(title) 48 | .setSubtitle(subTitle) 49 | .setDescription(description) 50 | .setNegativeButtonText(negativeText) 51 | .build() 52 | val mainExecutor = ContextCompat.getMainExecutor(this.context) 53 | val biometricPrompt = BiometricPrompt ( 54 | this.context as FragmentActivity, 55 | mainExecutor, 56 | object : BiometricPrompt.AuthenticationCallback() { 57 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { 58 | super.onAuthenticationError(errorCode, errString) 59 | onBiometricAuthentication(false, errorCode, errString as String) 60 | } 61 | 62 | override fun onAuthenticationFailed() { 63 | super.onAuthenticationFailed() 64 | onBiometricAuthentication(false, -1, "") 65 | } 66 | 67 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { 68 | super.onAuthenticationSucceeded(result) 69 | onBiometricAuthentication(true, -1, "") 70 | } 71 | } 72 | ) 73 | 74 | biometricPrompt.authenticate(promptInfo) 75 | }else{ 76 | onBiometricAuthentication(true, -1, "") 77 | } 78 | 79 | } 80 | 81 | private fun getCancellationSignal(onCancel: () -> Unit): CancellationSignal { 82 | val cancellationSignal = CancellationSignal() 83 | cancellationSignal.setOnCancelListener (onCancel) 84 | 85 | return cancellationSignal 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/entities/IUser.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.entities 2 | 3 | import android.accounts.Account 4 | import android.accounts.AccountManager 5 | import android.content.Context 6 | import android.util.Base64 7 | import com.google.gson.GsonBuilder 8 | 9 | open class IUser( 10 | val login: String = "", 11 | val password: String = "", 12 | val userName: String = "", 13 | val isCreator: Boolean = false, 14 | val defaultUser: Boolean = false, 15 | ) { 16 | override fun toString(): String = this.userName 17 | companion object{ 18 | inline fun fromAccount(context: Context, account: Account): T? { 19 | try { 20 | val manager = AccountManager.get(context) 21 | val accountData = Base64.decode(manager.getPassword(account), Base64.DEFAULT) 22 | return GsonBuilder().create().fromJson(String(accountData), T::class.java) 23 | }catch (_: Exception){ 24 | 25 | } 26 | 27 | return null 28 | } 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/enums/LoginType.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.enums 2 | 3 | enum class LoginType(val value: Int) { 4 | Authenticator (0), 5 | App(1) 6 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/enums/Operation.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.enums 2 | 3 | enum class Operation(val value: Int) { 4 | NewAccount(0), 5 | UpdateAccount(1) 6 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/exceptions/AuthenticatorException.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.exceptions 2 | 3 | import android.accounts.AccountManager 4 | import android.content.Context 5 | import android.os.Bundle 6 | 7 | open class AuthenticatorException(): Exception() { 8 | companion object{ 9 | private val serialVersionUUID: Long = 1L 10 | } 11 | 12 | private lateinit var mFailureBundle: Bundle 13 | val failureBundle: Bundle 14 | get() = this.mFailureBundle 15 | 16 | protected constructor(ctx: Context, errorCode: Int, errorMessageStringResourceId: Int) : this() { 17 | this.mFailureBundle = Bundle().apply { 18 | putInt(AccountManager.KEY_ERROR_CODE, errorCode) 19 | putString(AccountManager.KEY_ERROR_MESSAGE, ctx.getString(errorMessageStringResourceId)) 20 | } 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/exceptions/UnsupportedAccountTypeException.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.exceptions 2 | 3 | import android.accounts.AccountManager 4 | import android.content.Context 5 | 6 | 7 | class UnsupportedAccountTypeException(context: Context, unsupportedAccountTypeResourceId: Int) : 8 | AuthenticatorException( 9 | context, 10 | AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, 11 | unsupportedAccountTypeResourceId 12 | ) { 13 | companion object{ 14 | private val serialVersionUUID: Long = 2L 15 | } 16 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/exceptions/UnsupportedAuthTokenTypeException.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.exceptions 2 | 3 | import android.accounts.AccountManager 4 | import android.content.Context 5 | 6 | class UnsupportedAuthTokenTypeException(context: Context, unsupportedAuthTokenTypeResourceId: Int): AuthenticatorException( 7 | context, 8 | AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, 9 | unsupportedAuthTokenTypeResourceId 10 | ) { 11 | companion object{ 12 | val serialVersionUUID: Long = 3L 13 | } 14 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/authmanager/exceptions/UnsupportedFeaturesException.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.authmanager.exceptions 2 | 3 | import android.accounts.AccountManager 4 | import android.content.Context 5 | 6 | class UnsupportedFeaturesException(context: Context, unsupportedFeaturesResourceId: Int): AuthenticatorException( 7 | context, 8 | AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, 9 | unsupportedFeaturesResourceId 10 | ) { 11 | companion object{ 12 | val serialVersionUUID: Long = 4L 13 | } 14 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/networking/NetworkStatus.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.networking 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.map 5 | 6 | sealed class NetworkConnectionType { 7 | object Unknown: NetworkConnectionType() 8 | object ConnectionCellular: NetworkConnectionType() 9 | object Connection3G: NetworkConnectionType() 10 | object Connection4G: NetworkConnectionType() 11 | object Connection5G: NetworkConnectionType() 12 | object ConnectionWifi: NetworkConnectionType() 13 | } 14 | sealed class NetworkStatus{ 15 | object Available : NetworkStatus(){ 16 | var connectionType: NetworkConnectionType = NetworkConnectionType.Connection3G 17 | } 18 | object Unavailable : NetworkStatus() 19 | } 20 | 21 | inline fun Flow.map( 22 | crossinline onUnavailable: suspend () -> Result, 23 | crossinline onAvailable: suspend () -> Result, 24 | ): Flow = map { status -> 25 | when (status) { 26 | NetworkStatus.Unavailable -> onUnavailable() 27 | NetworkStatus.Available -> onAvailable() 28 | } 29 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/networking/NetworkStatusTracker.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.networking 2 | 3 | import android.Manifest 4 | import android.content.Context 5 | import android.content.pm.PackageManager 6 | import android.net.ConnectivityManager 7 | import android.net.Network 8 | import android.net.NetworkCapabilities 9 | import android.net.NetworkRequest 10 | import android.os.Build 11 | import android.telephony.TelephonyManager 12 | import android.telephony.TelephonyManager.NETWORK_TYPE_CDMA 13 | import android.telephony.TelephonyManager.NETWORK_TYPE_HSDPA 14 | import android.telephony.TelephonyManager.NETWORK_TYPE_HSPA 15 | import android.telephony.TelephonyManager.NETWORK_TYPE_HSUPA 16 | import android.telephony.TelephonyManager.NETWORK_TYPE_LTE 17 | import android.telephony.TelephonyManager.NETWORK_TYPE_NR 18 | import android.util.Log 19 | import androidx.core.app.ActivityCompat 20 | import kotlinx.coroutines.channels.awaitClose 21 | import kotlinx.coroutines.flow.callbackFlow 22 | import kotlinx.coroutines.launch 23 | 24 | class NetworkStatusTracker( 25 | context: Context 26 | ) { 27 | 28 | private val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager 29 | private val telephonyManager = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager 30 | 31 | val networkStatus = callbackFlow { 32 | val networkStatusCallback = object : ConnectivityManager.NetworkCallback() { 33 | override fun onUnavailable() { 34 | trySend(NetworkStatus.Unavailable).isSuccess 35 | } 36 | 37 | override fun onAvailable(network: Network) { 38 | val availableNetwork = NetworkStatus.Available 39 | trySend(availableNetwork).isSuccess 40 | } 41 | 42 | override fun onCapabilitiesChanged( 43 | network: Network, 44 | networkCapabilities: NetworkCapabilities 45 | ) { 46 | super.onCapabilitiesChanged(network, networkCapabilities) 47 | this@callbackFlow.launch { 48 | val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) 49 | 50 | val availableNetwork = NetworkStatus.Available.apply { 51 | this.connectionType = if (isWifi) 52 | NetworkConnectionType.ConnectionWifi 53 | else { 54 | if ( 55 | ActivityCompat.checkSelfPermission( 56 | context, 57 | Manifest.permission.READ_BASIC_PHONE_STATE 58 | ) != PackageManager.PERMISSION_GRANTED && 59 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 60 | NetworkConnectionType.ConnectionCellular 61 | }else { 62 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU && 63 | ActivityCompat.checkSelfPermission( 64 | context, 65 | Manifest.permission.READ_PHONE_STATE 66 | ) != PackageManager.PERMISSION_GRANTED){ 67 | NetworkConnectionType.ConnectionCellular 68 | }else { 69 | try { 70 | when (telephonyManager.dataNetworkType) { 71 | NETWORK_TYPE_CDMA, NETWORK_TYPE_HSDPA, NETWORK_TYPE_HSPA, NETWORK_TYPE_HSUPA -> NetworkConnectionType.Connection3G 72 | NETWORK_TYPE_LTE -> NetworkConnectionType.Connection4G 73 | NETWORK_TYPE_NR -> NetworkConnectionType.Connection5G 74 | else -> NetworkConnectionType.Unknown 75 | } 76 | } catch (ex: Exception) { 77 | Log.e("NetworkTracker", "${ex.message ?: ""}\n\t${ex.stackTraceToString()}") 78 | NetworkConnectionType.ConnectionCellular 79 | } 80 | } 81 | } 82 | } 83 | } 84 | 85 | trySend(availableNetwork).isSuccess 86 | } 87 | } 88 | 89 | override fun onLost(network: Network) { 90 | trySend(NetworkStatus.Unavailable).isSuccess 91 | } 92 | } 93 | 94 | val networkRequest = NetworkRequest.Builder() 95 | .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) 96 | .addTransportType(NetworkCapabilities.TRANSPORT_WIFI) 97 | .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) 98 | .build() 99 | 100 | connectivityManager.registerNetworkCallback(networkRequest, networkStatusCallback) 101 | 102 | awaitClose { 103 | connectivityManager.unregisterNetworkCallback(networkStatusCallback) 104 | } 105 | } 106 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/svg/AndroidResourceParser.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.svg 2 | 3 | import javax.xml.parsers.DocumentBuilderFactory 4 | 5 | typealias ResourceEntry = Pair 6 | 7 | class AndroidResourceParser(private val drawableResource: String) { 8 | private val factory = DocumentBuilderFactory.newInstance() 9 | private val builder = factory.newDocumentBuilder() 10 | private val drawable by lazy { 11 | builder.parse(drawableResource.byteInputStream(Charsets.UTF_8)).apply { documentElement.normalize() } 12 | } 13 | 14 | fun values(type: String): Iterable = drawable 15 | .getElementsByTagName(type).iterable.map { node -> 16 | ResourceEntry( 17 | node.attributes["name"]!!, 18 | node.textContent 19 | ) 20 | } 21 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/svg/ResourceCollector.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.svg 2 | 3 | class ResourceCollector { 4 | private val resources = mutableMapOf() 5 | 6 | fun addResources(values: Iterable) { 7 | resources.putAll(values) 8 | } 9 | 10 | fun getValue(name: String): String? { 11 | var curName = name 12 | 13 | do { 14 | val value = resources[curName] ?: return null 15 | 16 | if (!value.startsWith("@")) { 17 | return value 18 | } 19 | 20 | curName = value.split("/").last() 21 | } while (true) 22 | } 23 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/svg/Vector2SvgConverter.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.svg 2 | 3 | import org.w3c.dom.* 4 | import java.io.InputStream 5 | import java.io.OutputStream 6 | import java.util.* 7 | import javax.xml.parsers.DocumentBuilderFactory 8 | import javax.xml.transform.OutputKeys 9 | import javax.xml.transform.TransformerFactory 10 | import javax.xml.transform.dom.DOMSource 11 | import javax.xml.transform.stream.StreamResult 12 | import javax.xml.xpath.XPathConstants 13 | import javax.xml.xpath.XPathExpression 14 | import javax.xml.xpath.XPathFactory 15 | 16 | class Vector2SvgConverter(val colors: ResourceCollector) { 17 | 18 | private val builder by lazy { 19 | val factory = DocumentBuilderFactory.newInstance() 20 | factory.isNamespaceAware = true 21 | factory.newDocumentBuilder() 22 | } 23 | private val transformer by lazy { 24 | val factory = TransformerFactory.newInstance() 25 | try { 26 | factory.setAttribute("indent-number", 4) 27 | }catch (_: Exception){ 28 | 29 | } 30 | val transformer = factory.newTransformer() 31 | transformer.setOutputProperty(OutputKeys.INDENT, "yes") 32 | transformer 33 | } 34 | 35 | fun convert(input: InputStream, outputStream: OutputStream) { 36 | val doc = builder.parse(input) 37 | 38 | if (doc.documentElement.nodeName != "vector") { 39 | return 40 | } 41 | 42 | convert(doc) 43 | 44 | val source = DOMSource(doc) 45 | val result = StreamResult(outputStream) 46 | transformer.transform(source, result) 47 | } 48 | 49 | private fun convert(doc: Document) { 50 | with(doc.documentElement) { 51 | removeAttributeNS(ANDROID_NS, "width") 52 | removeAttributeNS(ANDROID_NS, "height") 53 | 54 | attributes.rename(ANDROID_NS, "viewportHeight", SVG_NS, "height") 55 | attributes.rename(ANDROID_NS, "viewportWidth", SVG_NS, "width") 56 | 57 | appendAttribute("viewBox", "0 0 ${attributes["width"]} ${attributes["height"]}") 58 | 59 | rename("svg", SVG_NS) 60 | } 61 | 62 | doc.getElementsByTagNameNS(null, "group").iterable.map { it as Element }.forEach { 63 | it.rename("g", null) 64 | it.fix() 65 | } 66 | 67 | doc.getElementsByTagNameNS(null, "path").iterable.map { it as Element }.forEach { 68 | it.attributes.rename(ANDROID_NS, "pathData", SVG_NS, "d") 69 | it.attributes.rename(ANDROID_NS, "fillType", SVG_NS, "fill-rule") 70 | it.fix() 71 | } 72 | 73 | doc.getElementsByTagNameNS(null, "clip-path").iterable.map { it as Element }.forEach { 74 | val parent = it.parentNode as? Element 75 | val clipPathId = convertClipPathElement(doc, it) 76 | parent?.setAttribute("clip-path", "url(#$clipPathId)") 77 | parent?.removeChild(it) 78 | } 79 | 80 | doc.getElementsByTagNameNS(AAPT_NS, "attr").iterable.map { it as Element }.forEach { 81 | val parent = it.parentNode as? Element 82 | try { 83 | val (attributeName, defId) = convertAaptAttributesElement(doc, it) 84 | parent?.setAttribute(attributeName, "url(#$defId)") 85 | } catch (e: Exception) { 86 | 87 | } 88 | 89 | parent?.removeChild(it) 90 | } 91 | 92 | fixEmptyNamespace(doc.documentElement) 93 | removeBlankNodes(doc) 94 | } 95 | 96 | private fun convertClipPathElement(doc: Document, element: Element): String { 97 | val index = clipPathCount(doc) 98 | val id = "_clippath_$index" 99 | val cp = createClipPath(doc, element, id) 100 | 101 | addElementToDefSection(doc, cp) 102 | 103 | return id 104 | } 105 | 106 | private fun convertAaptAttributesElement(doc: Document, element: Element): Pair { 107 | val svgAttributeName: String = when (val name = element.getAttribute("name")) { 108 | "android:fillColor" -> { 109 | "fill" 110 | } 111 | "android:strokeColor" -> { 112 | "stroke" 113 | } 114 | else -> { 115 | throw Exception("Unsupported aapt:attr name: $name") 116 | } 117 | } 118 | 119 | val firstChildElement = element.childNodes.iterable.filterIsInstance().firstOrNull() ?: throw Exception("Childless aapt:attr") 120 | 121 | if (firstChildElement.tagName == "gradient") { 122 | val gradient = convertGradientElement(doc, firstChildElement) 123 | addElementToDefSection(doc, gradient) 124 | 125 | return Pair(svgAttributeName, gradient.id()) 126 | } else { 127 | throw Exception("Unsupported aapt:attr child element tag: ${firstChildElement.tagName}") 128 | } 129 | } 130 | 131 | private fun convertGradientElement(doc: Document, element: Element): Element { 132 | var type = element.getAttributeNS(ANDROID_NS, "type") 133 | if (type.isEmpty()) { 134 | // by default type is linear 135 | type = "linear" 136 | } 137 | 138 | if (type == "linear") { 139 | val index = linearGradientCount(doc) 140 | val id = "_linear_gradient_$index" 141 | return createLinearGradient(doc, element, id) 142 | } else { 143 | throw Exception("Unsupported gradient android:type $type") 144 | } 145 | } 146 | 147 | private fun clipPathCount(doc: Document): Int { 148 | return doc.getElementsByTagName("clipPath").length 149 | } 150 | 151 | private fun linearGradientCount(doc: Document): Int { 152 | return doc.getElementsByTagName("linearGradient").length 153 | } 154 | 155 | private fun addElementToDefSection(doc: Document, element: Element) { 156 | val defSection = doc.getElementById(DEFS_SECTION) ?: createDefSection(doc) 157 | defSection.appendChild(element) 158 | } 159 | 160 | private fun createDefSection(doc: Document): Element { 161 | val element = doc.createElement("defs") 162 | element.setId(DEFS_SECTION) 163 | doc.documentElement.appendChild(element) 164 | 165 | return element 166 | } 167 | 168 | private fun createClipPath(doc: Document, androidClipPath: Element, id: String): Element { 169 | val pathData = androidClipPath.attributes.get(ANDROID_NS, "pathData") 170 | val pathElement = doc.createElement("path") 171 | pathElement.setAttribute("d", pathData) 172 | 173 | val clipPathElement = doc.createElement("clipPath") 174 | clipPathElement.setId(id) 175 | clipPathElement.appendChild(pathElement) 176 | 177 | return clipPathElement 178 | } 179 | 180 | private fun createLinearGradient(doc: Document, gradientElement: Element, id: String): Element { 181 | val element = doc.createElement("linearGradient") 182 | element.setId(id) 183 | element.setAttribute("gradientUnits", "userSpaceOnUse") 184 | 185 | val startX = gradientElement.getAttributeNS(ANDROID_NS, "startX") 186 | val startY = gradientElement.getAttributeNS(ANDROID_NS, "startY") 187 | val endX = gradientElement.getAttributeNS(ANDROID_NS, "endX") 188 | val endY = gradientElement.getAttributeNS(ANDROID_NS, "endY") 189 | if (startX.isNotEmpty() && startY.isNotEmpty()) { 190 | element.setAttribute("x1", startX) 191 | element.setAttribute("y1", startY) 192 | } 193 | if (endX.isNotEmpty() && endY.isNotEmpty()) { 194 | element.setAttribute("x2", endX) 195 | element.setAttribute("y2", endY) 196 | } 197 | 198 | when (gradientElement.getAttributeNS(ANDROID_NS, "tileMode").lowercase(Locale.getDefault())) { 199 | "clamp" -> 200 | element.setAttribute("spreadMethod", "pad") 201 | "mirror" -> 202 | element.setAttribute("spreadMethod", "reflect") 203 | "repeat" -> 204 | element.setAttribute("spreadMethod", "repeat") 205 | } 206 | 207 | val startColor = gradientElement.getAttributeNS(ANDROID_NS, "startColor") 208 | val endColor = gradientElement.getAttributeNS(ANDROID_NS, "endColor") 209 | val centerColor = gradientElement.getAttributeNS(ANDROID_NS, "centerColor") 210 | val stops: List 211 | if (startColor.isNotEmpty() && endColor.isNotEmpty()) { 212 | stops = if (centerColor.isNotEmpty()) { 213 | listOf( 214 | createStopFromColorAndOffset(doc, startColor, "0%"), 215 | createStopFromColorAndOffset(doc, centerColor, "50%"), 216 | createStopFromColorAndOffset(doc, endColor, "100%") 217 | ) 218 | } else { 219 | listOf( 220 | createStopFromColorAndOffset(doc, startColor, "0%"), 221 | createStopFromColorAndOffset(doc, endColor, "100%") 222 | ) 223 | } 224 | } else { 225 | stops = gradientElement.childNodes.iterable.mapNotNull { 226 | if (it is Element) createStopFromGradientItem(doc, it) else null 227 | } 228 | } 229 | 230 | element.removeAllChildNodes() 231 | element.appendChildNodes(stops) 232 | 233 | return element 234 | } 235 | 236 | private fun createStopFromColorAndOffset(doc: Document, colorString: String, offsetPercent: String): Element { 237 | val (color, opacity) = parseColor(colorString) 238 | val stopElement = doc.createElement("stop") 239 | stopElement.setAttribute("offset", offsetPercent) 240 | stopElement.setAttribute("stop-color", color) 241 | if (opacity != null) { 242 | stopElement.setAttribute("stop-opacity", opacity.toString()) 243 | } 244 | 245 | return stopElement 246 | } 247 | 248 | private fun createStopFromGradientItem(doc: Document, gradientItem: Element): Element? { 249 | if (gradientItem.tagName != "item") { 250 | return null 251 | } 252 | 253 | val colorStr = gradientItem.getAttributeNS(ANDROID_NS, "color") ?: return null 254 | val offset = gradientItem.getAttributeNS(ANDROID_NS, "offset") ?: return null 255 | 256 | val (color, opacity) = parseColor(colorStr) 257 | 258 | val stopElement = doc.createElement("stop") 259 | stopElement.setAttribute("offset", offset) 260 | stopElement.setAttribute("stop-color", color) 261 | 262 | if (opacity != null) { 263 | stopElement.setAttribute("stop-opacity", opacity.toString()) 264 | } 265 | 266 | return stopElement 267 | } 268 | 269 | private fun parseColor(color: String): Pair { 270 | var colorHex: String = color 271 | if (color.startsWith("@")) { 272 | val name = color.split("/").last() 273 | val resourceHex = colors.getValue(name) ?: throw IllegalArgumentException("Color $name does not exists") 274 | 275 | colorHex = resourceHex 276 | } 277 | 278 | return parseColorHex(colorHex) 279 | } 280 | 281 | private fun parseColorHex(colorHex: String): Pair { 282 | val color: String = if (colorHex.length < 6) { 283 | colorHex.takeLast(3) 284 | } else { 285 | colorHex.takeLast(6) 286 | } 287 | 288 | var opacity: Float? = null 289 | if (colorHex.length == 9) { 290 | opacity = colorHex.drop(1).take(2).toInt(16).toFloat() / 255.0f 291 | } 292 | 293 | return Pair("#$color", opacity) 294 | } 295 | 296 | private fun removeBlankNodes(doc: Document) { 297 | doc.documentElement.normalize() 298 | val xpathQuery = "//text()[normalize-space(.) = '']" 299 | val xpath: XPathExpression = XPathFactory.newInstance().newXPath().compile(xpathQuery) 300 | val blankTextNodes = xpath.evaluate(doc, XPathConstants.NODESET) as NodeList 301 | 302 | blankTextNodes.iterable.forEach { 303 | it.parentNode.removeChild(it) 304 | } 305 | } 306 | 307 | private fun fixEmptyNamespace(node: Node) { 308 | node.childNodes.iterable.forEach { 309 | if (it is Element) { 310 | it.rename(it.tagName, SVG_NS) 311 | fixEmptyNamespace(it) 312 | } 313 | } 314 | } 315 | 316 | private fun Element.fix() { 317 | fixTranslate() 318 | fixFill() 319 | fixRotate() 320 | fixScale() 321 | fixStroke() 322 | } 323 | 324 | private fun Element.fixStroke() { 325 | val strokeColor = attributes.get(ANDROID_NS, "strokeColor") 326 | val strokeWidth = attributes.get(ANDROID_NS, "strokeWidth") 327 | val strokeOpacity = attributes.get(ANDROID_NS, "strokeAlpha") 328 | val strokeLineCap = attributes.get(ANDROID_NS, "strokeLineCap") 329 | val strokeLineJoin = attributes.get(ANDROID_NS, "strokeLineJoin") 330 | 331 | if (strokeColor != null) { 332 | val (strokeColorHex, _) = parseColor(strokeColor) 333 | setAttribute("stroke", strokeColorHex) 334 | } 335 | 336 | if (strokeWidth != null) { 337 | setAttribute("stroke-width", strokeWidth) 338 | } 339 | 340 | if (strokeOpacity != null) { 341 | setAttribute("stroke-opacity", strokeOpacity) 342 | } 343 | 344 | if (strokeLineCap != null) { 345 | setAttribute("stroke-linecap", strokeLineCap) 346 | } 347 | 348 | if (strokeLineJoin != null) { 349 | setAttribute("stroke-linejoin", strokeLineJoin) 350 | } 351 | 352 | removeAttributeNS(ANDROID_NS, "strokeColor") 353 | removeAttributeNS(ANDROID_NS, "strokeWidth") 354 | removeAttributeNS(ANDROID_NS, "strokeAlpha") 355 | removeAttributeNS(ANDROID_NS, "strokeLineCap") 356 | removeAttributeNS(ANDROID_NS, "strokeLineJoin") 357 | } 358 | 359 | private fun Element.fixTranslate() { 360 | val translateX = attributes.get(ANDROID_NS, "translateX") 361 | val translateY = attributes.get(ANDROID_NS, "translateY") 362 | 363 | if (translateX != null) { 364 | val translate = translateY?.let { y -> "translate($translateX, $y)" } ?: "translate($translateX)" 365 | appendAttribute("transform", translate) 366 | } 367 | 368 | removeAttributeNS(ANDROID_NS, "translateX") 369 | removeAttributeNS(ANDROID_NS, "translateY") 370 | } 371 | 372 | private fun Element.fixScale() { 373 | val scaleX = attributes.get(ANDROID_NS, "scaleX") 374 | val scaleY = attributes.get(ANDROID_NS, "scaleY") 375 | 376 | if (scaleX != null || scaleY != null) { 377 | val scaleX1 = scaleX ?: "1" 378 | val scaleY1 = scaleY ?: "1" 379 | 380 | appendAttribute("transform", "scale($scaleX1, $scaleY1)") 381 | } 382 | 383 | removeAttributeNS(ANDROID_NS, "scaleX") 384 | removeAttributeNS(ANDROID_NS, "scaleY") 385 | } 386 | 387 | private fun Element.fixFill() { 388 | val fillColorName = attributes.get(ANDROID_NS, "fillColor") 389 | var fillColorHex: String? = null 390 | var fillAlpha: Float = attributes.get(ANDROID_NS, "fillAlpha")?.toFloatOrNull() ?: 1.0f 391 | 392 | if (fillColorName != null) { 393 | val (colorHex, androidAlpha) = parseColor(fillColorName) 394 | if (androidAlpha != null) { 395 | fillAlpha *= androidAlpha 396 | } 397 | fillColorHex = colorHex 398 | } 399 | 400 | if (fillAlpha != 1.0f) { 401 | setAttribute("fill-opacity", fillAlpha.toString()) 402 | } 403 | 404 | if (fillColorHex != null) { 405 | setAttribute("fill", fillColorHex) 406 | } 407 | 408 | removeAttributeNS(ANDROID_NS, "fillColor") 409 | removeAttributeNS(ANDROID_NS, "fillAlpha") 410 | } 411 | 412 | private fun Element.fixRotate() { 413 | val pivotX = attributes.get(ANDROID_NS, "pivotX") 414 | val pivotY = attributes.get(ANDROID_NS, "pivotY") 415 | val rotation = attributes.get(ANDROID_NS, "rotation") ?: return 416 | 417 | if (pivotX != null || pivotY != null) { 418 | appendAttribute("transform", "rotation($rotation, ${pivotX!!} ${pivotY!!})") 419 | } else { 420 | appendAttribute("transform", "rotation($rotation)") 421 | } 422 | 423 | removeAttributeNS(ANDROID_NS, "pivotX") 424 | removeAttributeNS(ANDROID_NS, "pivotY") 425 | removeAttributeNS(ANDROID_NS, "rotation") 426 | } 427 | 428 | private fun Element.appendAttribute(name: String, value: String, delimiter: String = " ") { 429 | val current = attributes.get(null, name) 430 | 431 | if (current == null) { 432 | setAttribute(name, value) 433 | } else { 434 | setAttribute(name, "$current$delimiter$value") 435 | } 436 | } 437 | 438 | private fun Element.appendChildNodes(nodes: List) { 439 | nodes.forEach { 440 | appendChild(it) 441 | } 442 | } 443 | 444 | private fun Element.removeAllChildNodes() { 445 | childNodes.iterable.forEach { 446 | removeChild(it) 447 | } 448 | } 449 | 450 | private fun Element.id(): String { 451 | return getAttribute("id") 452 | } 453 | 454 | private fun Element.setId(id: String) { 455 | setAttribute("id", id) 456 | setIdAttribute("id", true) 457 | } 458 | 459 | private fun NamedNodeMap.rename(nameSpaceUri: String, old: String, newNameSpaceUri: String?, new: String) { 460 | val node = getNamedItemNS(nameSpaceUri, old) as Attr? ?: return 461 | with(node.ownerElement) { 462 | removeAttributeNS(nameSpaceUri, old) 463 | setAttribute(new, node.value) 464 | } 465 | } 466 | 467 | private fun Node.rename(new: String, namespaceUri: String?) { 468 | ownerDocument.renameNode(this, namespaceUri, new) 469 | } 470 | 471 | companion object { 472 | const val ANDROID_NS = "http://schemas.android.com/apk/res/android" 473 | const val AAPT_NS = "http://schemas.android.com/aapt" 474 | const val SVG_NS = "http://www.w3.org/2000/svg" 475 | 476 | const val DEFS_SECTION = "svg-definitions" 477 | } 478 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/svg/XmlUtilities.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.svg 2 | 3 | import org.w3c.dom.NamedNodeMap 4 | import org.w3c.dom.Node 5 | import org.w3c.dom.NodeList 6 | 7 | operator fun NamedNodeMap.get(name: String): String? = getNamedItem(name)?.nodeValue 8 | 9 | fun NamedNodeMap.get(namespaceUri: String?, name: String): String? = getNamedItemNS(namespaceUri, name)?.nodeValue 10 | 11 | class NodeListIterator(private val nodeList: NodeList) : Iterator { 12 | private var position = 0 13 | 14 | override fun hasNext(): Boolean { 15 | return position < nodeList.length 16 | } 17 | 18 | override fun next() = nodeList.item(position++)!! 19 | } 20 | 21 | val NodeList.iterable: Iterable 22 | get() { 23 | return object : Iterable { 24 | override fun iterator(): Iterator { 25 | return iterator 26 | } 27 | } 28 | } 29 | 30 | val NodeList.iterator: Iterator get() = NodeListIterator(this) -------------------------------------------------------------------------------- /jetpackcompose/src/main/java/io/github/afalabarce/jetpackcompose/utilities/Extensions.kt: -------------------------------------------------------------------------------- 1 | package io.github.afalabarce.jetpackcompose.utilities 2 | 3 | import io.github.afalabarce.jetpackcompose.svg.AndroidResourceParser 4 | import io.github.afalabarce.jetpackcompose.svg.ResourceCollector 5 | import io.github.afalabarce.jetpackcompose.svg.Vector2SvgConverter 6 | import java.io.ByteArrayOutputStream 7 | import java.nio.charset.Charset 8 | import java.text.DecimalFormat 9 | import java.text.SimpleDateFormat 10 | import java.util.* 11 | 12 | fun Date.format(strFormat: String = "dd/MM/yyyy"):String = SimpleDateFormat(strFormat, Locale.getDefault()).format(this) 13 | fun Int.format(strFormat: String = "#,##0"):String = DecimalFormat(strFormat).format(this) 14 | fun Long.format(strFormat: String = "#,##0"):String = DecimalFormat(strFormat).format(this) 15 | fun Float.format(strFormat: String = "#,##0.00"):String = DecimalFormat(strFormat).format(this) 16 | fun Double.format(strFormat: String = "#,##0.00"):String = DecimalFormat(strFormat).format(this) 17 | fun Boolean.iif(ifTrue: T, ifFalse: T): T = if (this) ifTrue else ifFalse 18 | fun Calendar.today(): Date? = SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).let { f -> 19 | val dateStr = f.format(this.time) 20 | f.parse(dateStr) 21 | } 22 | 23 | fun String.toDate(format: String = "yyyy-MM-dd"): Date? = try{ 24 | SimpleDateFormat(format, Locale.getDefault()).parse(this) 25 | }catch (ex: Exception){ 26 | null 27 | } 28 | 29 | fun CharSequence.toDate(format: String = "yyyy-MM-dd"): Date? = try{ 30 | SimpleDateFormat(format, Locale.getDefault()).parse(this.toString()) 31 | }catch (ex: Exception){ 32 | null 33 | } 34 | 35 | fun String.toSvg(charset: Charset = Charsets.UTF_8): String{ 36 | try { 37 | val resourceParser = AndroidResourceParser(this) 38 | val colorCollector = 39 | ResourceCollector().apply { addResources(resourceParser.values("color")) } 40 | val svgConverter = Vector2SvgConverter(colorCollector) 41 | val outputStream = ByteArrayOutputStream(0) 42 | 43 | svgConverter.convert(this.byteInputStream(Charsets.UTF_8), outputStream) 44 | 45 | return String(outputStream.toByteArray(), charset) 46 | }catch (_: Exception){ 47 | return this 48 | } 49 | } -------------------------------------------------------------------------------- /jetpackcompose/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Give me more icons! 4 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 11 | repositories { 12 | google() 13 | mavenCentral() 14 | } 15 | } 16 | rootProject.name = "JetpackComposeComponents" 17 | include ':jetpackcompose' 18 | --------------------------------------------------------------------------------