├── .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 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
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 |
--------------------------------------------------------------------------------