├── common ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── common │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mutualmobile │ │ └── praxis │ │ └── common │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── data ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── data │ │ │ ├── AppConstants.kt │ │ │ ├── mapper │ │ │ └── EntityMapper.kt │ │ │ ├── sources │ │ │ ├── IJokesRemoteSource.kt │ │ │ └── JokesRemoteSource.kt │ │ │ ├── remote │ │ │ ├── JokeApiService.kt │ │ │ ├── SafeApiCall.kt │ │ │ ├── model │ │ │ │ └── NETJokeListData.kt │ │ │ └── RetrofitHelper.kt │ │ │ ├── injection │ │ │ ├── SourcesModule.kt │ │ │ ├── RepositoryModule.kt │ │ │ └── NetworkModule.kt │ │ │ └── repository │ │ │ └── JokesRepoImpl.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── data │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mutualmobile │ │ └── praxis │ │ └── data │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── domain ├── .gitignore ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── domain │ │ │ ├── repository │ │ │ └── IJokesRepo.kt │ │ │ ├── mappers │ │ │ └── DomainModel.kt │ │ │ ├── model │ │ │ └── DOMJoke.kt │ │ │ ├── injection │ │ │ └── UseCaseModule.kt │ │ │ ├── usecases │ │ │ ├── BaseUseCase.kt │ │ │ └── GetFiveRandomJokesUseCase.kt │ │ │ └── SafeResult.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── domain │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mutualmobile │ │ └── praxis │ │ └── domain │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── app ├── .gitignore ├── keystore │ ├── praxis-debug.jks │ └── praxis-release.jks ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ └── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ ├── java │ │ │ └── com │ │ │ │ └── mutualmobile │ │ │ │ └── praxis │ │ │ │ ├── PraxisApp.kt │ │ │ │ ├── injection │ │ │ │ └── module │ │ │ │ │ └── NavigationModule.kt │ │ │ │ └── root │ │ │ │ ├── MainActivity.kt │ │ │ │ └── PraxisNavigation.kt │ │ └── AndroidManifest.xml │ └── test │ │ ├── java │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ ├── utils │ │ │ ├── FileUtil.kt │ │ │ └── TestKotlinExtensions.kt │ │ │ ├── TestApplication.kt │ │ │ ├── useCaseTest │ │ │ └── GetFiveRandomJokesUseCaseTest.kt │ │ │ ├── base │ │ │ └── BaseTest.kt │ │ │ └── injection │ │ │ └── module │ │ │ └── FakeNetworkModule.kt │ │ └── resources │ │ └── responses │ │ └── jokes_response.json ├── fabric.properties ├── signing.gradle.kts ├── proguard-rules.pro ├── variants.gradle.kts ├── proguard-specific.txt ├── build.gradle.kts └── proguard-common.txt ├── commonui ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── integer.xml │ │ │ │ ├── dimens.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ ├── anim │ │ │ │ ├── slide_left_in.xml │ │ │ │ ├── slide_left_out.xml │ │ │ │ ├── slide_right_in.xml │ │ │ │ └── slide_right_out.xml │ │ │ ├── values-v21 │ │ │ │ └── styles.xml │ │ │ └── drawable │ │ │ │ ├── ic_email.xml │ │ │ │ └── ic_eye.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── commonui │ │ │ ├── theme │ │ │ ├── Shape.kt │ │ │ ├── Type.kt │ │ │ ├── Color.kt │ │ │ ├── PraxisSurface.kt │ │ │ ├── SystemUiController.kt │ │ │ └── Theme.kt │ │ │ └── material │ │ │ ├── CommonTopAppBar.kt │ │ │ └── DefaultSnackbar.kt │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── commonui │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mutualmobile │ │ └── praxis │ │ └── commonui │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── navigator ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── java │ │ │ └── com │ │ │ │ └── mutualmobile │ │ │ │ └── praxis │ │ │ │ └── navigator │ │ │ │ ├── NavigationKeys.kt │ │ │ │ ├── NavigationCommand.kt │ │ │ │ ├── Screens.kt │ │ │ │ ├── Navigator.kt │ │ │ │ └── PraxisNavigator.kt │ │ └── AndroidManifest.xml │ ├── test │ │ └── java │ │ │ └── com │ │ │ └── mutualmobile │ │ │ └── praxis │ │ │ └── navigator │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── com │ │ └── mutualmobile │ │ └── praxis │ │ └── navigator │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── featcalendarview ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── com │ │ │ └── praxis │ │ │ └── feat │ │ │ └── calendarview │ │ │ └── ui │ │ │ ├── CalendarYearVM.kt │ │ │ ├── CalendarMonthVM.kt │ │ │ ├── CalendarYearView.kt │ │ │ └── CalendarMonthlyView.kt │ └── test │ │ └── java │ │ └── com │ │ └── praxis │ │ └── feat │ │ └── calendarview │ │ ├── MainCoroutineRule.kt │ │ └── vm │ │ └── AuthVMTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── libjetcalendar ├── .gitignore ├── src │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── dev │ │ └── baseio │ │ └── libjetcalendar │ │ ├── data │ │ ├── JetDay.kt │ │ ├── JetViewType.kt │ │ ├── JetYear.kt │ │ ├── JetMonth.kt │ │ └── JetWeek.kt │ │ ├── weekly │ │ └── JetCalendarWeekView.kt │ │ ├── monthly │ │ └── JetCalendarMonthlyView.kt │ │ └── yearly │ │ └── JetCalendarYearlyView.kt └── build.gradle.kts ├── art ├── art1.png └── art2.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── settings.gradle.kts ├── .gitignore ├── team-props ├── git-hooks │ └── pre-commit.sh └── git-hooks.gradle.kts ├── gradle.properties ├── README.md ├── gradlew.bat ├── gradlew └── LICENSE /common/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /domain/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /common/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /commonui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /commonui/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /navigator/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /navigator/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /featcalendarview/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /libjetcalendar/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /featcalendarview/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /art/art1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/art/art1.png -------------------------------------------------------------------------------- /art/art2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/art/art2.png -------------------------------------------------------------------------------- /app/keystore/praxis-debug.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/app/keystore/praxis-debug.jks -------------------------------------------------------------------------------- /app/keystore/praxis-release.jks: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/app/keystore/praxis-release.jks -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MainActivity 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /commonui/src/main/res/values/integer.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 300 4 | -------------------------------------------------------------------------------- /commonui/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/commonui/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /commonui/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/commonui/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /commonui/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/commonui/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /commonui/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/commonui/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /commonui/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oianmol/JetCalendarView/master/commonui/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /data/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /navigator/src/main/java/com/mutualmobile/praxis/navigator/NavigationKeys.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.navigator 2 | 3 | object NavigationKeys { 4 | 5 | const val ForgotPassword = "forgotPassword" 6 | } -------------------------------------------------------------------------------- /common/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /domain/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /commonui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /libjetcalendar/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /navigator/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":domain") 2 | include(":data") 3 | include(":common") 4 | include(":commonui") 5 | include(":navigator") 6 | include(":app") 7 | include(":featcalendarview") 8 | include(":libjetcalendar") 9 | -------------------------------------------------------------------------------- /featcalendarview/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /captures 8 | .externalNativeBuild 9 | .idea 10 | *.aab 11 | .cxx 12 | */build 13 | */.gradle 14 | /buildSrc/build -------------------------------------------------------------------------------- /app/fabric.properties: -------------------------------------------------------------------------------- 1 | #Contains API Secret used to validate your application. Commit to internal source control; avoid making secret public. 2 | #Wed Mar 08 18:34:26 IST 2017 3 | apiSecret=9cb50ff88ceece358871f08a6424267dd88f84ca25ecce34d12d8c1d9ff12367 4 | -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/data/JetDay.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.data 2 | 3 | import java.time.LocalDate 4 | 5 | open class JetCalendarType 6 | 7 | data class JetDay(val date: LocalDate, val isPartOfMonth: Boolean) : JetCalendarType() -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jul 14 15:56:55 IST 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip 7 | -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/AppConstants.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data 2 | 3 | object AppConstants { 4 | const val BASE_URL = "https://api.icndb.com" 5 | 6 | const val COROUTINE_RETROFIT = "COROUTINE_RETROFIT" 7 | const val RX_RETROFIT = "RX_RETROFIT" 8 | } 9 | -------------------------------------------------------------------------------- /commonui/src/main/res/anim/slide_left_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /commonui/src/main/res/anim/slide_left_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /commonui/src/main/res/anim/slide_right_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /commonui/src/main/res/anim/slide_right_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | -------------------------------------------------------------------------------- /commonui/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 20dp 4 | 16sp 5 | 10sp 6 | 26sp 7 | 8 | -------------------------------------------------------------------------------- /domain/src/main/java/com/mutualmobile/praxis/domain/repository/IJokesRepo.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain.repository 2 | 3 | import com.mutualmobile.praxis.domain.SafeResult 4 | import com.mutualmobile.praxis.domain.model.DOMJokeList 5 | 6 | interface IJokesRepo { 7 | suspend fun getFiveRandomJokes(): SafeResult 8 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mutualmobile/praxis/domain/mappers/DomainModel.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain.mappers 2 | 3 | open class DomainModel 4 | 5 | open class UIModel 6 | 7 | interface UiModelMapper { 8 | fun mapToPresentation(model: M): UIModel 9 | 10 | fun mapToDomain(modelItem: MI): DomainModel 11 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/mapper/EntityMapper.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.mapper 2 | 3 | import com.mutualmobile.praxis.domain.mappers.DomainModel 4 | 5 | interface EntityMapper { 6 | fun mapToDomain(entity: ME): M 7 | 8 | fun mapToEntity(model: M): ME 9 | } 10 | 11 | open class DataModel 12 | -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/data/JetViewType.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.data 2 | 3 | enum class JetViewType { 4 | MONTHLY, 5 | WEEKLY, 6 | YEARLY; 7 | 8 | fun next(): JetViewType { 9 | if (ordinal == values().size.minus(1)) { 10 | return MONTHLY 11 | } 12 | return values()[ordinal + 1] 13 | } 14 | } -------------------------------------------------------------------------------- /commonui/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | -------------------------------------------------------------------------------- /team-props/git-hooks/pre-commit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Running static analysis using ktlint..." 4 | 5 | # ktlintcheck 6 | #./gradlew ktlintcheck --daemon 7 | 8 | status=$? 9 | 10 | if [ "$status" = 0 ] ; then 11 | echo "Static analysis found no problems." 12 | exit 0 13 | else 14 | echo 1>&2 "Static analysis found violations it could not fix." 15 | exit 1 16 | fi -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /commonui/src/main/res/drawable/ic_email.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /domain/src/main/java/com/mutualmobile/praxis/domain/model/DOMJoke.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain.model 2 | 3 | import android.os.Parcelable 4 | import com.mutualmobile.praxis.domain.mappers.DomainModel 5 | import kotlinx.parcelize.Parcelize 6 | 7 | /** 8 | * Created by Vipul Asri on 18/01/21. 9 | */ 10 | 11 | @Parcelize 12 | data class DOMJoke( 13 | val id: Int, 14 | val joke: String 15 | ) : DomainModel(), Parcelable 16 | 17 | data class DOMJokeList(val type: String, val DOMJokes: List) : DomainModel() -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/remote/JokeApiService.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.remote 2 | 3 | import com.mutualmobile.praxis.data.remote.model.NETJokeListData 4 | import retrofit2.Retrofit 5 | import retrofit2.http.GET 6 | 7 | interface JokeApiService { 8 | 9 | companion object { 10 | fun createRetrofitService(retrofit: Retrofit): JokeApiService { 11 | return retrofit.create(JokeApiService::class.java) 12 | } 13 | } 14 | 15 | @GET("/jokes/random/5") 16 | suspend fun getFiveRandomJokes(): NETJokeListData 17 | } -------------------------------------------------------------------------------- /app/signing.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_APPLICATION_PLUGIN) 3 | } 4 | 5 | android { 6 | signingConfigs { 7 | getByName("release") { 8 | keyAlias = "praxis-release" 9 | keyPassword = "ITHOmptI" 10 | storeFile(file("keystore/praxis-release.jks")) 11 | storePassword = "PoTHatHR" 12 | } 13 | 14 | getByName("debug") { 15 | keyAlias = "praxis-debug" 16 | keyPassword = "utherNiC" 17 | storeFile(file("keystore/praxis-debug.jks")) 18 | storePassword = "uRgeSCIt" 19 | } 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mutualmobile/praxis/TestApplication.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis 2 | 3 | import com.mutualmobile.praxis.injection.component.DaggerTestAppComponent 4 | import com.mutualmobile.praxis.injection.component.TestAppComponent 5 | import dagger.android.DaggerApplication 6 | 7 | class TestApplication : DaggerApplication() { 8 | 9 | private val component: TestAppComponent by lazy { 10 | DaggerTestAppComponent.factory() 11 | .create(this) as TestAppComponent 12 | } 13 | 14 | override fun applicationInjector() = component 15 | 16 | fun provideComponent() = component 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mutualmobile/praxis/injection/module/NavigationModule.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.injection.module 2 | 3 | import com.mutualmobile.praxis.navigator.Navigator 4 | import com.mutualmobile.praxis.navigator.PraxisNavigator 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(SingletonComponent::class) 13 | class NavigationModule { 14 | 15 | @Provides 16 | @Singleton 17 | fun provideNavigator(): Navigator = PraxisNavigator() 18 | 19 | } 20 | -------------------------------------------------------------------------------- /commonui/src/main/res/drawable/ic_eye.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /navigator/src/main/java/com/mutualmobile/praxis/navigator/NavigationCommand.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.navigator 2 | 3 | import androidx.navigation.NavOptions 4 | 5 | sealed class NavigationCommand { 6 | object NavigateUp : NavigationCommand() 7 | data class NavigateToRoute(val route: String, val options: NavOptions? = null) : 8 | NavigationCommand() 9 | 10 | data class NavigateUpWithResult( 11 | val key: String, 12 | val result: T, 13 | val destination: String? = null 14 | ) : NavigationCommand() 15 | 16 | data class PopUpToRoute(val route: String, val inclusive: Boolean) : NavigationCommand() 17 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mutualmobile/praxis/domain/injection/UseCaseModule.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain.injection 2 | 3 | import com.mutualmobile.praxis.domain.repository.IJokesRepo 4 | import com.mutualmobile.praxis.domain.usecases.GetFiveRandomJokesUseCase 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.components.SingletonComponent 9 | /** 10 | * Created by Vipul Asri on 13/01/21. 11 | */ 12 | @Module 13 | @InstallIn(SingletonComponent::class) 14 | object UseCaseModule { 15 | 16 | @Provides 17 | fun provideGetFiveRandomJokes(repo: IJokesRepo): GetFiveRandomJokesUseCase { 18 | return GetFiveRandomJokesUseCase(repo) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Development/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /domain/src/main/java/com/mutualmobile/praxis/domain/usecases/BaseUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain.usecases 2 | 3 | /** 4 | * Created by Vipul Asri on 13/01/21. 5 | */ 6 | 7 | interface BaseUseCase { 8 | 9 | /** 10 | * Perform an operation with no input parameters. 11 | * Will throw an exception by default, if not implemented but invoked. 12 | * 13 | * @return 14 | */ 15 | suspend fun perform(): Result = throw NotImplementedError() 16 | 17 | /** 18 | * Perform an operation. 19 | * Will throw an exception by default, if not implemented but invoked. 20 | * 21 | * @param params 22 | * @return 23 | */ 24 | suspend fun perform(params: ExecutableParam): Result = throw NotImplementedError() 25 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/injection/SourcesModule.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.injection 2 | 3 | import com.mutualmobile.praxis.data.remote.JokeApiService 4 | import com.mutualmobile.praxis.data.sources.IJokesRemoteSource 5 | import com.mutualmobile.praxis.data.sources.JokesRemoteSource 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | /** 11 | * Created by Vipul Asri on 13/01/21. 12 | */ 13 | @Module 14 | @InstallIn(SingletonComponent::class) 15 | object SourcesModule { 16 | 17 | @Provides 18 | fun provideJokesNetworkSource(apiService: JokeApiService): IJokesRemoteSource { 19 | return JokesRemoteSource( 20 | apiService 21 | ) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mutualmobile/praxis/utils/TestKotlinExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.utils 2 | 3 | import kotlinx.coroutines.runBlocking 4 | import okhttp3.mockwebserver.MockResponse 5 | import okhttp3.mockwebserver.MockWebServer 6 | import org.junit.Assert.assertThrows 7 | 8 | fun MockWebServer.enqueueResponse( 9 | responsePath: String? = null, 10 | responseCode: Int = 200 11 | ) { 12 | val mockResponse = MockResponse() 13 | .setBody(FileUtil.loadText("responses/$responsePath")) 14 | .setResponseCode(responseCode) 15 | enqueue(mockResponse) 16 | } 17 | 18 | inline fun assertThrows( 19 | noinline executable: suspend () -> Unit 20 | ) { 21 | assertThrows(T::class.java) { 22 | runBlocking { 23 | executable() 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /data/src/androidTest/java/com/mutualmobile/praxis/data/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mutualmobile.praxis.data.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mutualmobile/praxis/domain/usecases/GetFiveRandomJokesUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain.usecases 2 | 3 | import com.mutualmobile.praxis.domain.SafeResult 4 | import com.mutualmobile.praxis.domain.model.DOMJoke 5 | import com.mutualmobile.praxis.domain.repository.IJokesRepo 6 | 7 | /** 8 | * Created by Vipul Asri on 13/01/21. 9 | */ 10 | 11 | class GetFiveRandomJokesUseCase(private val jokesRepo: IJokesRepo) : 12 | BaseUseCase>, Unit> { 13 | 14 | override suspend fun perform(): SafeResult> { 15 | return when (val result = jokesRepo.getFiveRandomJokes()) { 16 | is SafeResult.Success -> SafeResult.Success(result.data.DOMJokes) 17 | is SafeResult.NetworkError -> result 18 | is SafeResult.Failure -> result 19 | } 20 | } 21 | 22 | } -------------------------------------------------------------------------------- /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 | android.enableJetifier=true 10 | android.useAndroidX=true 11 | org.gradle.jvmargs=-Xmx1536m 12 | # When configured, Gradle will run in incubating parallel mode. 13 | # This option should only be used with decoupled projects. More details, visit 14 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 15 | # org.gradle.parallel=true 16 | -------------------------------------------------------------------------------- /common/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 -------------------------------------------------------------------------------- /common/src/androidTest/java/com/mutualmobile/praxis/common/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.common 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mutualmobile.praxis.common.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /commonui/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 -------------------------------------------------------------------------------- /data/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.kts. 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 -------------------------------------------------------------------------------- /domain/src/androidTest/java/com/mutualmobile/praxis/domain/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mutualmobile.praxis.domain.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /commonui/src/androidTest/java/com/mutualmobile/praxis/commonui/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mutualmobile.praxis.commonui.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /domain/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.kts. 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 -------------------------------------------------------------------------------- /navigator/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 -------------------------------------------------------------------------------- /featcalendarview/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 -------------------------------------------------------------------------------- /navigator/src/androidTest/java/com/mutualmobile/praxis/navigator/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.navigator 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("com.mutualmobile.praxis.navigator.test", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/injection/RepositoryModule.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.injection 2 | 3 | import com.mutualmobile.praxis.data.remote.model.JokesListResponseMapper 4 | import com.mutualmobile.praxis.data.repository.JokesRepoImpl 5 | import com.mutualmobile.praxis.data.sources.IJokesRemoteSource 6 | import com.mutualmobile.praxis.domain.repository.IJokesRepo 7 | import dagger.Module 8 | import dagger.Provides 9 | import dagger.hilt.InstallIn 10 | import dagger.hilt.components.SingletonComponent 11 | 12 | /** 13 | * Created by Vipul Asri on 13/01/21. 14 | */ 15 | @Module 16 | @InstallIn(SingletonComponent::class) 17 | object RepositoryModule { 18 | 19 | @Provides 20 | fun provideJokesRepository(networkSource: IJokesRemoteSource): IJokesRepo { 21 | return JokesRepoImpl(networkSource, JokesListResponseMapper()) 22 | } 23 | 24 | } -------------------------------------------------------------------------------- /commonui/src/main/java/com/mutualmobile/praxis/commonui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui.theme 2 | 3 | import androidx.compose.material.Typography 4 | import androidx.compose.ui.text.TextStyle 5 | import androidx.compose.ui.text.font.FontFamily 6 | import androidx.compose.ui.text.font.FontWeight 7 | import androidx.compose.ui.unit.sp 8 | 9 | // Set of Material typography styles to start with 10 | val PraxisTypography = Typography( 11 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ), 16 | button = TextStyle( 17 | fontFamily = FontFamily.Default, 18 | fontWeight = FontWeight.W500, 19 | fontSize = 14.sp 20 | ), 21 | caption = TextStyle( 22 | fontFamily = FontFamily.Default, 23 | fontWeight = FontWeight.Normal, 24 | fontSize = 12.sp 25 | ) 26 | 27 | 28 | ) -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/sources/JokesRemoteSource.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.sources 2 | 3 | import com.mutualmobile.praxis.data.remote.JokeApiService 4 | import com.mutualmobile.praxis.data.remote.model.NETJokeListData 5 | import com.mutualmobile.praxis.data.remote.safeApiCall 6 | import com.mutualmobile.praxis.domain.SafeResult 7 | import kotlinx.coroutines.CoroutineDispatcher 8 | import kotlinx.coroutines.Dispatchers 9 | 10 | /** 11 | * Created by Vipul Asri on 13/01/21. 12 | */ 13 | 14 | class JokesRemoteSource( 15 | private val jokeApiService: JokeApiService, 16 | private val dispatcher: CoroutineDispatcher = Dispatchers.IO 17 | ) : IJokesRemoteSource { 18 | 19 | override suspend fun getFiveRandomJokes(): SafeResult { 20 | return safeApiCall(dispatcher) { 21 | jokeApiService.getFiveRandomJokes() 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mutualmobile/praxis/root/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.root 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.core.view.WindowCompat 7 | import com.mutualmobile.praxis.navigator.Navigator 8 | import com.mutualmobile.praxis.commonui.theme.PraxisTheme 9 | import dagger.hilt.android.AndroidEntryPoint 10 | import javax.inject.Inject 11 | 12 | @AndroidEntryPoint 13 | class MainActivity : ComponentActivity() { 14 | 15 | @Inject 16 | lateinit var navigator: Navigator 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | WindowCompat.setDecorFitsSystemWindows(window, false) 21 | 22 | setContent { 23 | PraxisTheme { 24 | PraxisNavigation(navigator) 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /commonui/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Praxis 3 | Show 5 Random Jokes (Coroutine) 4 | Show 5 Random Jokes (Rx) 5 | About 6 | Chuck Norris Random Joke Generator 7 | Praxis is a sample Android app which can be used as a base project for other projects written in Kotlin language. The app uses MVVM architecture, Architecture Components, Dagger 2 with AndroidX to provide a robust base to the app.\n\nThe Retrofit library is used to fetch jokes along with the RxJava2/Coroutines which handles connections asynchronously and makes the app more reliable. 8 | MutualMobile 9 | 10 | -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/remote/SafeApiCall.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.remote 2 | 3 | import android.util.Log 4 | import com.mutualmobile.praxis.domain.SafeResult 5 | import kotlinx.coroutines.CoroutineDispatcher 6 | import kotlinx.coroutines.withContext 7 | import retrofit2.HttpException 8 | import java.io.IOException 9 | 10 | internal suspend fun safeApiCall( 11 | dispatcher: CoroutineDispatcher, 12 | apiCall: suspend () -> T 13 | ): SafeResult { 14 | return withContext(dispatcher) { 15 | try { 16 | SafeResult.Success(apiCall.invoke()) 17 | } catch (throwable: Throwable) { 18 | Log.e("safeApiCall", throwable.message.toString()) 19 | when (throwable) { 20 | is IOException -> SafeResult.NetworkError 21 | is HttpException -> SafeResult.Failure(throwable) 22 | else -> SafeResult.Failure(Exception(throwable)) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/test/resources/responses/jokes_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "success", 3 | "value": [ 4 | { 5 | "id": 427, 6 | "joke": "Chuck Norris' favorite cereal is Kellogg's Nails 'N' Gravel.", 7 | "categories": [] 8 | }, 9 | { 10 | "id": 75, 11 | "joke": "Chuck Norris can believe it's not butter.", 12 | "categories": [] 13 | }, 14 | { 15 | "id": 302, 16 | "joke": "Chuck Norris doesn't go on the internet, he has every internet site stored in his memory. He refreshes webpages by blinking.", 17 | "categories": [] 18 | }, 19 | { 20 | "id": 275, 21 | "joke": "Little Miss Muffet sat on her tuffet, until Chuck Norris roundhouse kicked her into a glacier.", 22 | "categories": [] 23 | }, 24 | { 25 | "id": 76, 26 | "joke": "If tapped, a Chuck Norris roundhouse kick could power the country of Australia for 44 minutes.", 27 | "categories": [] 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /app/variants.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_APPLICATION_PLUGIN) 3 | } 4 | subprojects { 5 | apply { 6 | from("signing.gradle.kts") 7 | } 8 | } 9 | 10 | android { 11 | buildTypes { 12 | getByName("release") { 13 | debuggable = false 14 | versionNameSuffix = "-release" 15 | 16 | isMinifyEnabled = false 17 | shrinkResources = true 18 | 19 | proguardFiles( 20 | getDefaultProguardFile("proguard-android.txt"), "proguard-common.txt", 21 | "proguard-specific.txt" 22 | ) 23 | signingConfig( 24 | signingConfigs.release 25 | // buildConfigField "boolean", "ENABLE_LOGGING", "false" 26 | ) 27 | } 28 | getByName("debug") { 29 | debuggable = true 30 | versionNameSuffix = "-debug" 31 | applicationIdSuffix = ".debug" 32 | signingConfig( 33 | signingConfigs.debug 34 | // buildConfigField "boolean", "ENABLE_LOGGING", "true" 35 | ) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /featcalendarview/src/test/java/com/praxis/feat/calendarview/MainCoroutineRule.kt: -------------------------------------------------------------------------------- 1 | package com.praxis.feat.calendarview 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.TestCoroutineDispatcher 6 | import kotlinx.coroutines.test.TestCoroutineScope 7 | import kotlinx.coroutines.test.resetMain 8 | import kotlinx.coroutines.test.setMain 9 | import org.junit.rules.TestWatcher 10 | import org.junit.runner.Description 11 | 12 | @ExperimentalCoroutinesApi 13 | class MainCoroutineRule( 14 | private val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() 15 | ) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) { 16 | override fun starting(description: Description?) { 17 | super.starting(description) 18 | Dispatchers.setMain(dispatcher) 19 | } 20 | 21 | override fun finished(description: Description?) { 22 | super.finished(description) 23 | cleanupTestCoroutines() 24 | Dispatchers.resetMain() 25 | } 26 | } -------------------------------------------------------------------------------- /domain/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 5 | id(BuildPlugins.KOTLIN_KAPT) 6 | id(BuildPlugins.DAGGER_HILT) 7 | } 8 | 9 | android { 10 | compileSdk = (ProjectProperties.COMPILE_SDK) 11 | 12 | defaultConfig { 13 | minSdk = (ProjectProperties.MIN_SDK) 14 | targetSdk = (ProjectProperties.TARGET_SDK) 15 | 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | } 26 | 27 | // Required for annotation processing plugins like Dagger 28 | kapt { 29 | generateStubs = true 30 | correctErrorTypes = true 31 | } 32 | 33 | dependencies { 34 | 35 | /*Kotlin*/ 36 | api(Lib.Kotlin.KT_STD) 37 | api(Lib.Async.COROUTINES) 38 | 39 | /* Dependency Injection */ 40 | api(Lib.Di.hilt) 41 | kapt(Lib.Di.hiltAndroidCompiler) 42 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/injection/NetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.injection 2 | 3 | import com.mutualmobile.praxis.data.AppConstants 4 | import com.mutualmobile.praxis.data.remote.JokeApiService 5 | import com.mutualmobile.praxis.data.remote.RetrofitHelper 6 | import dagger.Module 7 | import dagger.Provides 8 | import dagger.hilt.InstallIn 9 | import dagger.hilt.components.SingletonComponent 10 | import okhttp3.OkHttpClient 11 | import retrofit2.Retrofit 12 | /** 13 | * Created by Vipul Asri on 13/01/21. 14 | */ 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | object NetworkModule { 19 | 20 | @Provides 21 | fun provideHttpClient(): OkHttpClient { 22 | return RetrofitHelper.createOkHttpClient() 23 | } 24 | 25 | @Provides 26 | fun provideRetrofit( 27 | okHttpClient: OkHttpClient 28 | ): Retrofit { 29 | return RetrofitHelper.createRetrofitClient(okHttpClient, AppConstants.BASE_URL) 30 | } 31 | 32 | @Provides 33 | fun provideJokesApiService(retrofit: Retrofit): JokeApiService { 34 | return JokeApiService.createRetrofitService(retrofit) 35 | } 36 | 37 | } -------------------------------------------------------------------------------- /team-props/git-hooks.gradle.kts: -------------------------------------------------------------------------------- 1 | fun isLinuxOrMacOs(): Boolean { 2 | val osName = System.getProperty("os.name") 3 | .toLowerCase() 4 | return osName.contains("linux") || osName.contains("mac os") || osName.contains("macos") 5 | } 6 | 7 | tasks.create("copyGitHooks") { 8 | description = "Copies the git hooks from team-props/git-hooks to the .git folder." 9 | from("$rootDir/team-props/git-hooks/") { 10 | include("**/*.sh") 11 | rename("(.*).sh", "$1") 12 | } 13 | into("$rootDir/.git/hooks") 14 | onlyIf { isLinuxOrMacOs() } 15 | } 16 | 17 | tasks.create("installGitHooks") { 18 | description = "Installs the pre-commit git hooks from team-props/git-hooks." 19 | group = "git hooks" 20 | workingDir(rootDir) 21 | commandLine("chmod") 22 | args("-R", "+x", ".git/hooks/") 23 | dependsOn("copyGitHooks") 24 | onlyIf { isLinuxOrMacOs() } 25 | doLast { 26 | logger.info("Git hook installed successfully.") 27 | } 28 | } 29 | 30 | tasks.getByName("installGitHooks") 31 | .dependsOn(getTasksByName("copyGitHooks", true)) 32 | tasks.getByPath("app:preBuild") 33 | .dependsOn(getTasksByName("installGitHooks", true)) 34 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/remote/model/NETJokeListData.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.remote.model 2 | 3 | import com.google.gson.annotations.SerializedName 4 | import com.mutualmobile.praxis.data.mapper.EntityMapper 5 | import com.mutualmobile.praxis.data.mapper.DataModel 6 | import com.mutualmobile.praxis.domain.model.DOMJoke 7 | import com.mutualmobile.praxis.domain.model.DOMJokeList 8 | 9 | data class NETJokeListData( 10 | @SerializedName("type") 11 | val type: String, 12 | @SerializedName("value") 13 | val value: List 14 | ) : DataModel() 15 | 16 | data class NETJokeData( 17 | @SerializedName("id") 18 | val id: Int, 19 | @SerializedName("joke") 20 | val joke: String 21 | ) : DataModel() 22 | 23 | class JokesListResponseMapper : EntityMapper { 24 | override fun mapToDomain(entity: NETJokeListData): DOMJokeList { 25 | return DOMJokeList(entity.type, entity.value.map { DOMJoke(it.id, it.joke) }) 26 | } 27 | 28 | override fun mapToEntity(model: DOMJokeList): NETJokeListData { 29 | return NETJokeListData(model.type, model.DOMJokes.map { NETJokeData(it.id, it.joke) }) 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/remote/RetrofitHelper.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.remote 2 | 3 | import com.mutualmobile.praxis.data.BuildConfig 4 | import okhttp3.OkHttpClient 5 | import okhttp3.logging.HttpLoggingInterceptor 6 | import retrofit2.Retrofit 7 | import retrofit2.converter.gson.GsonConverterFactory 8 | 9 | object RetrofitHelper { 10 | 11 | private val okHttpLoggingInterceptor by lazy { 12 | HttpLoggingInterceptor().apply { 13 | level = 14 | if (BuildConfig.DEBUG) 15 | HttpLoggingInterceptor.Level.BODY 16 | else 17 | HttpLoggingInterceptor.Level.NONE 18 | } 19 | } 20 | 21 | fun createOkHttpClient(): OkHttpClient { 22 | return OkHttpClient.Builder() 23 | .addInterceptor(okHttpLoggingInterceptor) 24 | .retryOnConnectionFailure(true) 25 | .build() 26 | } 27 | 28 | fun createRetrofitClient( 29 | okHttpClient: OkHttpClient, 30 | baseUrl: String 31 | ): Retrofit { 32 | return Retrofit.Builder() 33 | .baseUrl(baseUrl) 34 | .addConverterFactory(GsonConverterFactory.create()) 35 | .client(okHttpClient) 36 | .build() 37 | } 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JetCalendarView 2 | 3 | Hit Refresh! Calendar view ❤️ Jetpack Compose 4 | 5 | This project is an experimental calendar view. Please raise issues if you find any, thanks! 6 | 7 | ## Features 8 | 9 | - Year View (List and Grid View) with optional Navigation buttons 10 | - Month View with optional Navigation buttons 11 | - Modular and built with a week as a simplest view 12 | 13 | 14 | drawing 15 | drawing 16 | 17 | 18 | License 19 | ======= 20 | Copyright 2022 Anmol Verma 21 | 22 | Licensed under the Apache License, Version 2.0 (the "License"); 23 | you may not use this file except in compliance with the License. 24 | You may obtain a copy of the License at 25 | 26 | http://www.apache.org/licenses/LICENSE-2.0 27 | 28 | Unless required by applicable law or agreed to in writing, software 29 | distributed under the License is distributed on an "AS IS" BASIS, 30 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | See the License for the specific language governing permissions and 32 | limitations under the License. 33 | -------------------------------------------------------------------------------- /commonui/src/main/java/com/mutualmobile/praxis/commonui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val White = Color(0xffffffff) 6 | val PraxisColor = Color(0xff00ACEE) 7 | 8 | val VeryLightGrey = Color(0xffdadce0) 9 | val LightGrey = Color(0xffe8eaed) 10 | val Grey = Color(0xff5f6368) 11 | val TextPrimary = Color(0xff202124) 12 | val TextSecondaryDark = Color(0xff212121) 13 | val TextSecondary = Color(0xff5f6368) 14 | val DarkGreen = Color(0xff056449) 15 | val GreyBg = Color(0xff15202b) 16 | val SearchBarDarkColor = Color(0xff10171e) 17 | 18 | val Shadow1 = Color(0xffded6fe) 19 | 20 | val Ocean11 = Color(0xff005687) 21 | val Ocean2 = Color(0xffbbfdfd) 22 | 23 | val Neutral7 = Color(0xff000000) 24 | val Neutral6 = Color(0x99000000) 25 | val Neutral3 = Color(0x1fffffff) 26 | val Neutral2 = Color(0x61ffffff) 27 | val Neutral1 = Color(0xbdffffff) 28 | val Neutral0 = Color(0xffffffff) 29 | 30 | val FunctionalRed = Color(0xffd00036) 31 | val FunctionalRedDark = Color(0xffea6d7e) 32 | val FunctionalGrey = Color(0xfff6f6f6) 33 | val FunctionalDarkGrey = Color(0xff2e2e2e) 34 | 35 | const val AlphaNearOpaque = 0.95f 36 | const val AlphaNearTransparent = 0.15f -------------------------------------------------------------------------------- /data/src/main/java/com/mutualmobile/praxis/data/repository/JokesRepoImpl.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.data.repository 2 | 3 | import com.mutualmobile.praxis.data.mapper.EntityMapper 4 | import com.mutualmobile.praxis.data.remote.model.NETJokeListData 5 | import com.mutualmobile.praxis.data.sources.IJokesRemoteSource 6 | import com.mutualmobile.praxis.domain.SafeResult 7 | import com.mutualmobile.praxis.domain.model.DOMJokeList 8 | import com.mutualmobile.praxis.domain.repository.IJokesRepo 9 | import java.lang.RuntimeException 10 | 11 | /** 12 | * Created by Vipul Asri on 13/01/21. 13 | */ 14 | 15 | class JokesRepoImpl( 16 | private val remoteSource: IJokesRemoteSource, 17 | private val DOMJokeListResponseMapper: EntityMapper 18 | ) : IJokesRepo { 19 | 20 | override suspend fun getFiveRandomJokes(): SafeResult { 21 | return when (val result = remoteSource.getFiveRandomJokes()) { 22 | is SafeResult.Success<*> -> SafeResult.Success(DOMJokeListResponseMapper.mapToDomain(result.data as NETJokeListData)) 23 | is SafeResult.Failure -> SafeResult.Failure(result.exception) 24 | SafeResult.NetworkError -> SafeResult.NetworkError 25 | else -> { throw RuntimeException()} 26 | } 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /data/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KAPT) 5 | id(BuildPlugins.DAGGER_HILT) 6 | } 7 | 8 | android { 9 | compileSdk = ProjectProperties.COMPILE_SDK 10 | 11 | defaultConfig { 12 | minSdk = (ProjectProperties.MIN_SDK) 13 | targetSdk = (ProjectProperties.TARGET_SDK) 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | getByName("release") { 19 | isMinifyEnabled = false 20 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 21 | } 22 | } 23 | } 24 | 25 | // Required for annotation processing plugins like Dagger 26 | kapt { 27 | generateStubs = true 28 | correctErrorTypes = true 29 | } 30 | 31 | dependencies { 32 | 33 | implementation(project(":domain")) 34 | /*Kotlin*/ 35 | api(Lib.Kotlin.KT_STD) 36 | api(Lib.Async.COROUTINES) 37 | 38 | /* Networking */ 39 | api(Lib.Networking.RETROFIT) 40 | api(Lib.Networking.RETROFIT_GSON) 41 | api(Lib.Networking.LOGGING) 42 | 43 | api(Lib.Serialization.GSON) 44 | 45 | /* Dependency Injection */ 46 | api(Lib.Di.hilt) 47 | kapt(Lib.Di.hiltAndroidCompiler) 48 | } -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KAPT) 5 | id(BuildPlugins.DAGGER_HILT) 6 | } 7 | 8 | android { 9 | compileSdk = ProjectProperties.COMPILE_SDK 10 | 11 | defaultConfig { 12 | minSdk = (ProjectProperties.MIN_SDK) 13 | targetSdk = (ProjectProperties.TARGET_SDK) 14 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | 17 | buildTypes { 18 | getByName("release") { 19 | isMinifyEnabled = false 20 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 21 | } 22 | } 23 | } 24 | 25 | // Required for annotation processing plugins like Dagger 26 | kapt { 27 | generateStubs = true 28 | correctErrorTypes = true 29 | } 30 | 31 | dependencies { 32 | /*Kotlin*/ 33 | api(Lib.Kotlin.KT_STD) 34 | 35 | /* Dependency Injection */ 36 | api(Lib.Di.hilt) 37 | api(Lib.Di.hiltNavigationCompose) 38 | api(Lib.Di.viewmodel) 39 | 40 | kapt(Lib.Di.hiltCompiler) 41 | kaptTest(Lib.Di.hiltCompiler) 42 | kapt(Lib.Di.hiltAndroidCompiler) 43 | kaptTest(Lib.Di.hiltAndroidCompiler) 44 | 45 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mutualmobile/praxis/useCaseTest/GetFiveRandomJokesUseCaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.useCaseTest 2 | 3 | import com.mutualmobile.praxis.base.BaseTest 4 | import com.mutualmobile.praxis.data.SafeResult 5 | import com.mutualmobile.praxis.domain.usecases.GetFiveRandomJokesUseCase 6 | import com.mutualmobile.praxis.injection.component.TestAppComponent 7 | import com.mutualmobile.praxis.utils.enqueueResponse 8 | import junit.framework.TestCase.assertEquals 9 | import kotlinx.coroutines.runBlocking 10 | import org.junit.Test 11 | import javax.inject.Inject 12 | 13 | class GetFiveRandomJokesUseCaseTest : BaseTest() { 14 | 15 | override fun injectIntoDagger(testAppComponent: TestAppComponent) { 16 | testAppComponent.inject(this) 17 | } 18 | 19 | @Inject 20 | lateinit var getFiveRandomJokesUseCase: GetFiveRandomJokesUseCase 21 | 22 | @Test 23 | fun `when api returns success- assert result data contains Jokes`() = 24 | runBlocking { 25 | mockWebServer.enqueueResponse("jokes_response.json") 26 | 27 | val result = getFiveRandomJokesUseCase.perform() 28 | assertEquals(1, mockWebServer.requestCount) 29 | assert(result is SafeResult.Success) 30 | assert((result as SafeResult.Success).data.isNotEmpty()) 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mutualmobile/praxis/base/BaseTest.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.base 2 | 3 | import android.os.Build 4 | import androidx.test.core.app.ApplicationProvider 5 | import androidx.test.ext.junit.runners.AndroidJUnit4 6 | import com.mutualmobile.praxis.TestApplication 7 | import com.mutualmobile.praxis.injection.component.TestAppComponent 8 | import okhttp3.mockwebserver.MockWebServer 9 | import org.junit.After 10 | import org.junit.Before 11 | import org.junit.runner.RunWith 12 | import org.robolectric.annotation.Config 13 | import javax.inject.Inject 14 | 15 | /** 16 | * Created by Ashish Suman on 09/03/21 17 | */ 18 | 19 | @RunWith(AndroidJUnit4::class) 20 | @Config(application = TestApplication::class, sdk = [Build.VERSION_CODES.O_MR1]) 21 | abstract class BaseTest { 22 | 23 | @Inject 24 | lateinit var mockWebServer: MockWebServer 25 | 26 | private lateinit var application: TestApplication 27 | 28 | @Before 29 | open fun setup() { 30 | application = ApplicationProvider.getApplicationContext() as TestApplication 31 | injectIntoDagger(application.provideComponent()) 32 | } 33 | 34 | @After 35 | open fun tearDown() { 36 | mockWebServer.shutdown() 37 | } 38 | 39 | abstract fun injectIntoDagger(testAppComponent: TestAppComponent) 40 | 41 | } -------------------------------------------------------------------------------- /domain/src/main/java/com/mutualmobile/praxis/domain/SafeResult.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.domain 2 | 3 | /** 4 | * A generic class that holds a value with its loading status. 5 | * @param 6 | */ 7 | sealed class SafeResult { 8 | 9 | data class Success(val data: T) : SafeResult() 10 | data class Failure( 11 | val exception: Exception? = Exception("Unknown Error"), 12 | val message: String = exception?.localizedMessage ?: "" 13 | ) : SafeResult() 14 | 15 | object NetworkError : SafeResult() 16 | 17 | override fun toString(): String { 18 | return when (this) { 19 | is Success -> "Success[data=$data]" 20 | is Failure -> "Failure[exception=$exception]" 21 | is NetworkError -> "NetworkError" 22 | } 23 | } 24 | } 25 | 26 | /** 27 | * `true` if [SafeResult] is of type [Success] & holds non-null [Success.data]. 28 | */ 29 | val SafeResult<*>.succeeded 30 | get() = this is SafeResult.Success && data != null 31 | 32 | fun SafeResult.getSuccessOrNull(): T? { 33 | return when (this) { 34 | is SafeResult.Success -> this.data 35 | else -> null 36 | } 37 | } 38 | 39 | fun SafeResult.getErrorOrNull(): SafeResult.Failure? { 40 | return when (this) { 41 | is SafeResult.Failure -> this 42 | else -> null 43 | } 44 | } -------------------------------------------------------------------------------- /navigator/src/main/java/com/mutualmobile/praxis/navigator/Screens.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.navigator 2 | 3 | import androidx.navigation.NamedNavArgument 4 | import androidx.navigation.NavType 5 | import androidx.navigation.navArgument 6 | 7 | sealed class Screen( 8 | baseRoute: String, 9 | val navArguments: List = emptyList() 10 | ) { 11 | val route: String = baseRoute.appendArguments(navArguments) 12 | 13 | object CalendarYearRoute : 14 | Screen("yearView") 15 | 16 | object CalendarMonthRoute : 17 | Screen( 18 | "monthView", 19 | navArguments = listOf(navArgument("date") { type = NavType.LongType }) 20 | ) { 21 | fun createRoute(date: Long) = 22 | route.replace("{${navArguments.first().name}}","$date") 23 | } 24 | } 25 | 26 | private fun String.appendArguments(navArguments: List): String { 27 | val mandatoryArguments = navArguments.filter { it.argument.defaultValue == null } 28 | .takeIf { it.isNotEmpty() } 29 | ?.joinToString(separator = "/", prefix = "/") { "{${it.name}}" } 30 | .orEmpty() 31 | val optionalArguments = navArguments.filter { it.argument.defaultValue != null } 32 | .takeIf { it.isNotEmpty() } 33 | ?.joinToString(separator = "&", prefix = "?") { "${it.name}={${it.name}}" } 34 | .orEmpty() 35 | return "$this$mandatoryArguments$optionalArguments" 36 | } -------------------------------------------------------------------------------- /commonui/src/main/java/com/mutualmobile/praxis/commonui/material/CommonTopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui.material 2 | 3 | import androidx.compose.foundation.layout.RowScope 4 | import androidx.compose.material.* 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.unit.Dp 9 | import androidx.compose.ui.unit.dp 10 | import com.mutualmobile.praxis.commonui.theme.PraxisSurface 11 | import com.mutualmobile.praxis.commonui.theme.PraxisTheme 12 | 13 | @Composable 14 | fun CommonTopAppBar( 15 | title: @Composable () -> Unit, 16 | modifier: Modifier = Modifier, 17 | navigationIcon: @Composable (() -> Unit)? = null, 18 | actions: @Composable RowScope.() -> Unit = {}, 19 | backgroundColor: Color = PraxisTheme.colors.uiBackground, 20 | contentColor: Color = contentColorFor(backgroundColor), 21 | elevation: Dp = AppBarDefaults.TopAppBarElevation 22 | ) { 23 | PraxisSurface( 24 | color = PraxisTheme.colors.uiBackground, 25 | contentColor = PraxisTheme.colors.accent, 26 | elevation = 4.dp 27 | ) { 28 | TopAppBar( 29 | modifier = modifier, 30 | contentColor = contentColor, 31 | elevation = elevation, 32 | title = title, 33 | navigationIcon = navigationIcon, 34 | actions = actions, 35 | backgroundColor = backgroundColor, 36 | ) 37 | } 38 | } -------------------------------------------------------------------------------- /navigator/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KAPT) 5 | } 6 | 7 | android { 8 | compileSdk = ProjectProperties.COMPILE_SDK 9 | 10 | defaultConfig { 11 | minSdk = (ProjectProperties.MIN_SDK) 12 | targetSdk = (ProjectProperties.TARGET_SDK) 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | getByName("release") { 18 | isMinifyEnabled = false 19 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 20 | } 21 | } 22 | 23 | 24 | compileOptions { 25 | sourceCompatibility = JavaVersion.VERSION_1_8 26 | targetCompatibility = JavaVersion.VERSION_1_8 27 | } 28 | 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | } 32 | 33 | 34 | composeOptions { 35 | kotlinCompilerExtensionVersion = Lib.Android.COMPOSE_COMPILER 36 | } 37 | } 38 | 39 | // Required for annotation processing plugins like Dagger 40 | kapt { 41 | generateStubs = true 42 | correctErrorTypes = true 43 | } 44 | 45 | dependencies { 46 | /*Kotlin*/ 47 | implementation(Lib.Android.appCompat) 48 | implementation(Lib.Kotlin.KTX_CORE) 49 | api(Lib.Async.COROUTINES) 50 | api(Lib.Async.COROUTINES_ANDROID) 51 | 52 | 53 | implementation(Lib.Kotlin.KT_STD) 54 | implementation(Lib.Android.navigationCompose) 55 | 56 | implementation(Lib.Android.navigationCompose) 57 | implementation(Lib.Di.hiltNavigationCompose) 58 | 59 | } -------------------------------------------------------------------------------- /app/src/test/java/com/mutualmobile/praxis/injection/module/FakeNetworkModule.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.injection.module 2 | 3 | import com.mutualmobile.praxis.data.remote.JokeApiService 4 | import com.mutualmobile.praxis.data.remote.RetrofitHelper 5 | import dagger.Module 6 | import dagger.Provides 7 | import okhttp3.OkHttpClient 8 | import okhttp3.mockwebserver.MockWebServer 9 | import retrofit2.Retrofit 10 | import javax.inject.Named 11 | import javax.inject.Singleton 12 | 13 | @Module 14 | class FakeNetworkModule { 15 | 16 | @Provides 17 | @Singleton 18 | internal fun provideMockWebServer(): MockWebServer { 19 | var mockWebServer: MockWebServer? = null 20 | val thread = Thread { 21 | mockWebServer = MockWebServer() 22 | mockWebServer?.start() 23 | } 24 | thread.start() 25 | thread.join() 26 | return mockWebServer!! 27 | } 28 | 29 | @Provides 30 | @Singleton 31 | @Named("mockRootUrl") 32 | internal fun provideBaseUrl(mockWebServer: MockWebServer): String { 33 | return mockWebServer.url("/") 34 | .toString() 35 | } 36 | 37 | @Provides 38 | @Singleton 39 | internal fun provideHttpClient(): OkHttpClient { 40 | return RetrofitHelper.createOkHttpClient() 41 | } 42 | 43 | @Provides 44 | @Singleton 45 | internal fun provideRetrofit( 46 | okHttpClient: OkHttpClient, 47 | @Named("mockRootUrl") rootUrl: String 48 | ): Retrofit { 49 | return RetrofitHelper.createRetrofitClient(okHttpClient, rootUrl) 50 | } 51 | 52 | @Provides 53 | @Singleton 54 | internal fun provideJokesApiService(retrofit: Retrofit): JokeApiService { 55 | return JokeApiService.createRetrofitService(retrofit) 56 | } 57 | 58 | } -------------------------------------------------------------------------------- /commonui/src/main/java/com/mutualmobile/praxis/commonui/material/DefaultSnackbar.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui.material 2 | 3 | import androidx.compose.foundation.layout.fillMaxWidth 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.foundation.layout.wrapContentHeight 6 | import androidx.compose.material.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | import com.mutualmobile.praxis.commonui.theme.PraxisTheme 12 | import com.mutualmobile.praxis.commonui.theme.PraxisTypography 13 | 14 | @Composable 15 | fun DefaultSnackbar( 16 | snackbarHostState: SnackbarHostState, 17 | modifier: Modifier = Modifier, 18 | onDismiss: () -> Unit = { } 19 | ) { 20 | SnackbarHost( 21 | hostState = snackbarHostState, 22 | snackbar = { data -> 23 | Snackbar( 24 | content = { 25 | Text( 26 | text = data.message, 27 | style = PraxisTypography.body1, 28 | color = PraxisTheme.colors.textPrimary, 29 | ) 30 | }, 31 | action = { 32 | data.actionLabel?.let { actionLabel -> 33 | TextButton(onClick = onDismiss) { 34 | Text( 35 | text = actionLabel, 36 | color = PraxisTheme.colors.textPrimary, 37 | style = PraxisTypography.body2 38 | ) 39 | } 40 | } 41 | }, 42 | backgroundColor = PraxisTheme.colors.accent 43 | ) 44 | }, 45 | modifier = modifier 46 | .fillMaxWidth() 47 | .wrapContentHeight(Alignment.Bottom) 48 | ) 49 | } -------------------------------------------------------------------------------- /commonui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KAPT) 5 | } 6 | 7 | android { 8 | compileSdk = ProjectProperties.COMPILE_SDK 9 | 10 | defaultConfig { 11 | minSdk = (ProjectProperties.MIN_SDK) 12 | targetSdk = (ProjectProperties.TARGET_SDK) 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | getByName("release") { 18 | isMinifyEnabled = false 19 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 20 | } 21 | } 22 | buildFeatures { 23 | compose = true 24 | } 25 | 26 | composeOptions { 27 | kotlinCompilerExtensionVersion = Lib.Android.COMPOSE_COMPILER 28 | } 29 | packagingOptions { 30 | resources { 31 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 32 | } 33 | } 34 | 35 | } 36 | 37 | // Required for annotation processing plugins like Dagger 38 | kapt { 39 | generateStubs = true 40 | correctErrorTypes = true 41 | } 42 | 43 | dependencies { 44 | /*Kotlin*/ 45 | api(Lib.Kotlin.KT_STD) 46 | api(Lib.Kotlin.KTX_CORE) 47 | /* Android Designing and layout */ 48 | api(Lib.Android.MATERIAL_DESIGN) 49 | api(Lib.Android.COMPOSE_UI) 50 | api(Lib.Android.COIL_COMPOSE) 51 | api(Lib.Android.COMPOSE_MATERIAL) 52 | api(Lib.Android.COMPOSE_TOOLING) 53 | debugApi(Lib.Android.DEBUG_TOOLING) 54 | api(Lib.Android.ACT_COMPOSE) 55 | 56 | /* Dependency Injection */ 57 | api(Lib.Di.hilt) 58 | kapt(Lib.Di.hiltAndroidCompiler) 59 | 60 | 61 | } -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/data/JetYear.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.data 2 | 3 | import java.time.DayOfWeek 4 | import java.time.LocalDate 5 | import java.time.temporal.TemporalAdjusters 6 | import java.time.temporal.WeekFields 7 | import java.util.* 8 | 9 | 10 | class JetYear private constructor( 11 | val startDate: LocalDate, 12 | val endDate: LocalDate, 13 | ) : JetCalendarType() { 14 | lateinit var yearMonths: List 15 | 16 | companion object { 17 | fun current( 18 | date: LocalDate = LocalDate.now(), 19 | firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek 20 | ): JetYear { 21 | val day: LocalDate = date.with(TemporalAdjusters.firstDayOfYear()) 22 | val last: LocalDate = date.with(TemporalAdjusters.lastDayOfYear()) 23 | val year = JetYear(day, last) 24 | year.yearMonths = year.months(firstDayOfWeek) 25 | return year 26 | } 27 | } 28 | 29 | private fun months(firstDayOfWeek: DayOfWeek): List { 30 | val months = mutableListOf() 31 | 32 | var startDateMonth = this.startDate.withDayOfMonth(1) 33 | var endDateMonth = this.startDate.withDayOfMonth(this.startDate.lengthOfMonth()) 34 | 35 | var currentYear = this.startDate.year 36 | while (true) { 37 | months.add(JetMonth.current(startDateMonth, firstDayOfWeek)) 38 | 39 | startDateMonth = endDateMonth.plusDays(1) 40 | endDateMonth = startDateMonth.withDayOfMonth(startDateMonth.lengthOfMonth()) 41 | if (endDateMonth.year > currentYear) { 42 | break 43 | } 44 | currentYear = endDateMonth.year 45 | } 46 | return months 47 | } 48 | 49 | fun year(): String { 50 | return this.startDate.year.toString() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /libjetcalendar/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KAPT) 5 | } 6 | 7 | android { 8 | compileSdk = ProjectProperties.COMPILE_SDK 9 | 10 | defaultConfig { 11 | minSdk = (ProjectProperties.MIN_SDK) 12 | targetSdk = (ProjectProperties.TARGET_SDK) 13 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildFeatures { 17 | compose = true 18 | } 19 | 20 | composeOptions { 21 | kotlinCompilerExtensionVersion = Lib.Android.COMPOSE_COMPILER 22 | } 23 | packagingOptions { 24 | resources.excludes.add("META-INF/LICENSE.txt") 25 | resources.excludes.add("META-INF/NOTICE.txt") 26 | resources.excludes.add("LICENSE.txt") 27 | resources.excludes.add( "/META-INF/{AL2.0,LGPL2.1}") 28 | } 29 | 30 | compileOptions { 31 | isCoreLibraryDesugaringEnabled = true 32 | sourceCompatibility = JavaVersion.VERSION_1_8 33 | targetCompatibility = JavaVersion.VERSION_1_8 34 | } 35 | 36 | kotlinOptions { 37 | jvmTarget = "1.8" 38 | } 39 | 40 | } 41 | 42 | // Required for annotation processing plugins like Dagger 43 | kapt { 44 | generateStubs = true 45 | correctErrorTypes = true 46 | } 47 | 48 | dependencies { 49 | /*Kotlin*/ 50 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") 51 | 52 | api(Lib.Android.COMPOSE_UI) 53 | api(Lib.Android.COMPOSE_MATERIAL) 54 | api(Lib.Android.COMPOSE_TOOLING) 55 | debugApi(Lib.Android.DEBUG_TOOLING) 56 | api(Lib.Android.ACT_COMPOSE) 57 | api(Lib.Android.ACCOMPANIST_INSETS) 58 | 59 | api(Lib.Android.appCompat) 60 | api(Lib.Kotlin.KTX_CORE) 61 | 62 | } -------------------------------------------------------------------------------- /app/src/main/java/com/mutualmobile/praxis/root/PraxisNavigation.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.root 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.ui.Modifier 7 | import androidx.navigation.compose.NavHost 8 | import androidx.navigation.compose.composable 9 | import androidx.navigation.compose.rememberNavController 10 | import com.google.accompanist.insets.ProvideWindowInsets 11 | import com.mutualmobile.praxis.commonui.theme.AlphaNearOpaque 12 | import com.mutualmobile.praxis.commonui.theme.PraxisSurface 13 | import com.mutualmobile.praxis.commonui.theme.PraxisTheme 14 | import com.mutualmobile.praxis.navigator.Navigator 15 | import com.mutualmobile.praxis.navigator.Screen 16 | import com.praxis.feat.calendarview.ui.CalendarMonthlyView 17 | import com.praxis.feat.calendarview.ui.CalendarYearView 18 | 19 | @Composable 20 | fun PraxisNavigation(navigator: Navigator) { 21 | ProvideWindowInsets { 22 | PraxisSurface( 23 | color = PraxisTheme.colors.statusBarColor.copy(alpha = AlphaNearOpaque), 24 | modifier = Modifier.fillMaxSize() 25 | ) { 26 | val navController = rememberNavController() 27 | 28 | LaunchedEffect(Unit) { 29 | navigator.handleNavigationCommands(navController) 30 | } 31 | 32 | NavHost( 33 | navController = navController, 34 | startDestination = Screen.CalendarYearRoute.route 35 | ) { 36 | composable(Screen.CalendarYearRoute.route) { 37 | CalendarYearView() 38 | } 39 | composable( 40 | Screen.CalendarMonthRoute.route, arguments = Screen.CalendarMonthRoute.navArguments 41 | ) { 42 | CalendarMonthlyView() 43 | } 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | -------------------------------------------------------------------------------- /navigator/src/main/java/com/mutualmobile/praxis/navigator/Navigator.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.navigator 2 | 3 | import androidx.lifecycle.LiveData 4 | import androidx.lifecycle.Observer 5 | import androidx.navigation.NavController 6 | import androidx.navigation.NavOptions 7 | import androidx.navigation.NavOptionsBuilder 8 | import androidx.navigation.navOptions 9 | import kotlinx.coroutines.Dispatchers 10 | import kotlinx.coroutines.GlobalScope 11 | import kotlinx.coroutines.channels.Channel 12 | import kotlinx.coroutines.flow.* 13 | import kotlinx.coroutines.launch 14 | import kotlinx.coroutines.withContext 15 | 16 | abstract class Navigator { 17 | // We use a StateFlow here to allow ViewModels to start observing navigation results before the initial composition, 18 | // and still get the navigation result later 19 | val navControllerFlow = MutableStateFlow(null) 20 | 21 | val navigationCommands = 22 | MutableSharedFlow(extraBufferCapacity = Int.MAX_VALUE) 23 | 24 | 25 | abstract suspend fun handleNavigationCommands(navController: NavController) 26 | abstract fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)? = null) 27 | abstract fun navigateAndClearBackStack(route: String) 28 | abstract fun navigateUp() 29 | abstract fun popUpTo(route: String, inclusive: Boolean) 30 | abstract fun navigateBackWithResult(key: String, result: T, destination: String?) 31 | abstract fun observeResult(key: String, route: String? = null): Flow 32 | } 33 | 34 | 35 | 36 | fun LiveData.asFlow(): Flow = flow { 37 | val channel = Channel(Channel.CONFLATED) 38 | val observer = Observer { 39 | channel.trySend(it) 40 | } 41 | withContext(Dispatchers.Main.immediate) { 42 | observeForever(observer) 43 | } 44 | try { 45 | for (value in channel) { 46 | emit(value) 47 | } 48 | } finally { 49 | GlobalScope.launch(Dispatchers.Main.immediate) { 50 | removeObserver(observer) 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /featcalendarview/src/test/java/com/praxis/feat/calendarview/vm/AuthVMTest.kt: -------------------------------------------------------------------------------- 1 | package com.praxis.feat.calendarview.vm 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import com.mutualmobile.praxis.navigator.Navigator 5 | import com.mutualmobile.praxis.navigator.PraxisNavigator 6 | import com.praxis.feat.calendarview.MainCoroutineRule 7 | import com.praxis.feat.calendarview.ui.model.LoginForm 8 | import io.mockk.coEvery 9 | import io.mockk.mockk 10 | import kotlinx.coroutines.runBlocking 11 | import org.junit.Before 12 | import org.junit.Rule 13 | import org.junit.Test 14 | 15 | class AuthVMTest { 16 | 17 | private var navigator: Navigator = PraxisNavigator() 18 | private lateinit var savedStateHandle: SavedStateHandle 19 | private lateinit var authVM: AuthVM 20 | 21 | @get:Rule 22 | val coroutineRule = MainCoroutineRule() 23 | 24 | 25 | @Before 26 | fun setUp() { 27 | savedStateHandle = mockk() 28 | } 29 | 30 | @Test 31 | fun `test that login now fails with validation exception`() { 32 | runBlocking { 33 | 34 | coEvery { 35 | savedStateHandle.get(any()) 36 | } returns "" 37 | 38 | authVM = AuthVM(savedStateHandle, navigator) 39 | 40 | assert(authVM.uiState.value is AuthVM.UiState.Empty) 41 | authVM.loginNow() 42 | assert(authVM.uiState.value is AuthVM.UiState.ErrorState) 43 | } 44 | } 45 | 46 | @Test 47 | fun `test that loginNow() completes with no exception`() { 48 | runBlocking { 49 | 50 | coEvery { 51 | savedStateHandle.get(any()) 52 | } returns "" 53 | 54 | authVM = AuthVM(savedStateHandle, navigator) 55 | authVM.credentials.value = LoginForm("anmol@gmail.com","sdkfkjkjfdsjkfds") 56 | assert(authVM.uiState.value is AuthVM.UiState.Empty) 57 | authVM.loginNow() 58 | assert(authVM.uiState.value is AuthVM.UiState.SuccessState) 59 | } 60 | } 61 | 62 | @Test 63 | fun `navigateForgotPassword how ?`() { 64 | TODO("how do we test navigation controller in unit test ?") 65 | } 66 | } -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/data/JetMonth.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.data 2 | 3 | import java.time.DayOfWeek 4 | import java.time.LocalDate 5 | import java.time.YearMonth 6 | import java.time.format.TextStyle 7 | import java.time.temporal.TemporalAdjusters 8 | import java.time.temporal.WeekFields 9 | import java.util.* 10 | 11 | 12 | class JetMonth private constructor( 13 | val startDate: LocalDate, 14 | val endDate: LocalDate, 15 | var firstDayOfWeek: DayOfWeek, 16 | ) : JetCalendarType() { 17 | lateinit var monthWeeks: List 18 | 19 | fun name(): String { 20 | return startDate.month.getDisplayName( 21 | TextStyle.SHORT, 22 | Locale.getDefault() 23 | ) 24 | } 25 | 26 | fun monthYear(): String { 27 | return name() + " " + year() 28 | } 29 | 30 | fun year(): String { 31 | return startDate.year.toString() 32 | } 33 | 34 | companion object { 35 | fun current( 36 | date: LocalDate = LocalDate.now(), 37 | firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek 38 | ): JetMonth { 39 | val startOfMonth = date.with(TemporalAdjusters.firstDayOfMonth()) 40 | val endOfMonth = date.with(TemporalAdjusters.lastDayOfMonth()) 41 | val month = JetMonth(startOfMonth, endOfMonth, firstDayOfWeek = firstDayOfWeek) 42 | month.monthWeeks = month.weeks(firstDayOfWeek) 43 | return month 44 | } 45 | } 46 | 47 | private fun weeks(firstDayOfWeek: DayOfWeek): List { 48 | val currentYearMonth: YearMonth = YearMonth.of(this.endDate.year, this.endDate.monthValue) 49 | val weeks = currentYearMonth.atEndOfMonth().get(WeekFields.of(firstDayOfWeek, 1).weekOfMonth()) 50 | val monthWeeks = mutableListOf() 51 | monthWeeks.add( 52 | JetWeek.current( 53 | startDate, 54 | dayOfWeek = this.firstDayOfWeek 55 | ) 56 | ) 57 | while (monthWeeks.size != weeks) { 58 | monthWeeks.add(monthWeeks.last().nextWeek()) 59 | } 60 | return monthWeeks 61 | } 62 | 63 | } 64 | 65 | 66 | -------------------------------------------------------------------------------- /featcalendarview/src/main/java/com/praxis/feat/calendarview/ui/CalendarYearVM.kt: -------------------------------------------------------------------------------- 1 | package com.praxis.feat.calendarview.ui 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import com.mutualmobile.praxis.navigator.Navigator 6 | import com.mutualmobile.praxis.navigator.Screen 7 | import dagger.hilt.android.lifecycle.HiltViewModel 8 | import dev.baseio.libjetcalendar.data.JetDay 9 | import dev.baseio.libjetcalendar.data.JetYear 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.time.DayOfWeek 15 | import java.time.LocalDate 16 | import java.time.temporal.WeekFields 17 | import java.util.* 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class CalendarYearVM @Inject constructor(private val navigator: Navigator) : ViewModel() { 22 | private lateinit var year: JetYear 23 | private lateinit var selectedDate: JetDay 24 | val firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek 25 | 26 | var yearState = MutableStateFlow(null) 27 | private set 28 | var gridListSwitch = MutableStateFlow(true) 29 | private set 30 | 31 | init { 32 | setYear(LocalDate.now()) 33 | } 34 | 35 | fun setYear(date: LocalDate) { 36 | viewModelScope.launch { 37 | selectedDate = JetDay(date, isPartOfMonth = true) 38 | withContext(Dispatchers.Default) { 39 | year = JetYear.current(selectedDate.date, firstDayOfWeek) 40 | } 41 | yearState.value = year 42 | } 43 | } 44 | 45 | fun navigateMonth(jetDay: JetDay) { 46 | navigator.navigate(Screen.CalendarMonthRoute.createRoute(jetDay.date.toEpochDay())) 47 | } 48 | 49 | fun switchView() { 50 | gridListSwitch.value = !gridListSwitch.value 51 | } 52 | 53 | fun nextYear() { 54 | yearState.value?.endDate?.plusDays(1)?.let { setYear(it) } 55 | 56 | } 57 | 58 | fun previousYear() { 59 | yearState.value?.startDate?.minusDays(1)?.let { setYear(it) } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /featcalendarview/src/main/java/com/praxis/feat/calendarview/ui/CalendarMonthVM.kt: -------------------------------------------------------------------------------- 1 | package com.praxis.feat.calendarview.ui 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | import androidx.lifecycle.viewModelScope 6 | import com.mutualmobile.praxis.navigator.Navigator 7 | import com.mutualmobile.praxis.navigator.Screen 8 | import dagger.hilt.android.lifecycle.HiltViewModel 9 | import dev.baseio.libjetcalendar.data.JetMonth 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.flow.MutableStateFlow 12 | import kotlinx.coroutines.launch 13 | import kotlinx.coroutines.withContext 14 | import java.time.DayOfWeek 15 | import java.time.LocalDate 16 | import java.time.temporal.WeekFields 17 | import java.util.* 18 | import javax.inject.Inject 19 | 20 | @HiltViewModel 21 | class CalendarMonthVM @Inject constructor( 22 | savedStateHandle: SavedStateHandle, 23 | private val navigator: Navigator 24 | ) : ViewModel() { 25 | 26 | private val monthDate = 27 | savedStateHandle.get(Screen.CalendarMonthRoute.navArguments.first().name)!! 28 | var selectedDate: LocalDate = LocalDate.ofEpochDay(monthDate) 29 | private val firstDayOfWeek: DayOfWeek = WeekFields.of(Locale.getDefault()).firstDayOfWeek 30 | 31 | var titleState = MutableStateFlow("") 32 | private set 33 | var month = MutableStateFlow(null) 34 | private set 35 | 36 | init { 37 | dateSelected(selectedDate) 38 | } 39 | 40 | fun dateSelected(date: LocalDate) { 41 | viewModelScope.launch { 42 | selectedDate = date 43 | titleState.value = date.year.toString() 44 | val monthCurrent = withContext(Dispatchers.Default) { 45 | JetMonth.current(date, firstDayOfWeek) 46 | } 47 | month.value = monthCurrent 48 | } 49 | } 50 | 51 | fun navBack() { 52 | navigator.navigateUp() 53 | } 54 | 55 | fun nextMonth() { 56 | month.value?.endDate?.plusDays(1)?.let { dateSelected(it) } 57 | } 58 | 59 | fun previousMonth() { 60 | month.value?.startDate?.minusDays(1)?.let { dateSelected(it) } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/proguard-specific.txt: -------------------------------------------------------------------------------- 1 | #MM Proguard Settings pertaining to this project 2 | # this is an extension to the recommended settings for android 3 | # provide in proguard-android.pro 4 | # 5 | # It is also an extention to the proguard configuration of Praxis 6 | # 7 | # Add proguard directives to this file if this project requires additional 8 | # configuration 9 | 10 | -keepnames !abstract class com.customername.android.injection.* 11 | 12 | #Keeping the members of that have static vars 13 | -keepclassmembers public class com.customername.android.** { 14 | public static * ; 15 | public *; 16 | } 17 | 18 | # Below will be classes you want to explicity keep AND obfuscate - you shouldn't need to do this unless your class is only referenced at runtime and not compile time (IE injected via annotation or reflection) 19 | #-keep,allowobfuscation class com.customername.android.** { *; } 20 | 21 | #Things you don't want to obfuscate and you don't want to be shrunk usually GSON pojos. Add your domain/JSON below here 22 | -keep class com.customername.android.model.** { *; } 23 | 24 | -dontwarn okio.** 25 | -dontwarn org.simpleframework.** 26 | -keep class com.google.common.** { *; } 27 | 28 | 29 | #Rxjava 30 | -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { 31 | long producerIndex; 32 | long consumerIndex; 33 | } 34 | 35 | -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueProducerNodeRef { 36 | rx.internal.util.atomic.LinkedQueueNode producerNode; 37 | } 38 | 39 | -keepclassmembers class rx.internal.util.unsafe.BaseLinkedQueueConsumerNodeRef { 40 | rx.internal.util.atomic.LinkedQueueNode consumerNode; 41 | } 42 | 43 | 44 | # Retrofit2 45 | -dontwarn retrofit2.** 46 | -keep class retrofit2.** { *; } 47 | -keepattributes Signature 48 | -keepattributes Exceptions 49 | -keepattributes Annotation 50 | 51 | -dontwarn android.databinding.** 52 | -keep class android.databinding.** { *; } 53 | -dontwarn com.google.errorprone.annotations.** 54 | 55 | 56 | -keep class okhttp3.** { *; } 57 | -keep interface okhttp3.** { *; } 58 | -dontwarn okhttp3.** 59 | -dontwarn okio.** 60 | -dontwarn javax.annotation** 61 | -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/data/JetWeek.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.data 2 | 3 | import java.time.DayOfWeek 4 | import java.time.LocalDate 5 | import java.time.format.TextStyle 6 | import java.time.temporal.TemporalAdjusters 7 | import java.util.* 8 | 9 | 10 | fun dayNames(dayOfWeek: DayOfWeek): List { 11 | val days = mutableListOf() 12 | days.add(dayOfWeek) 13 | while (days.size != 7) { 14 | days.add(days.last().plus(1)) 15 | } 16 | return days.map { 17 | it.getDisplayName( 18 | TextStyle.NARROW, 19 | Locale.getDefault() 20 | ) 21 | } 22 | } 23 | 24 | class JetWeek private constructor( 25 | val startDate: LocalDate, 26 | val endDate: LocalDate, 27 | val monthOfWeek: Int, 28 | val dayOfWeek: DayOfWeek, 29 | ) : JetCalendarType() { 30 | lateinit var days: List 31 | 32 | companion object { 33 | fun current( 34 | date: LocalDate = LocalDate.now(), 35 | dayOfWeek: DayOfWeek 36 | ): JetWeek { 37 | val startOfCurrentWeek: LocalDate = 38 | date.with(TemporalAdjusters.previousOrSame(dayOfWeek)) 39 | val lastDayOfWeek = dayOfWeek.plus(6) // or minus(1) 40 | val endOfWeek: LocalDate = date.with(TemporalAdjusters.nextOrSame(lastDayOfWeek)) 41 | val week = JetWeek(startOfCurrentWeek, endOfWeek, date.monthValue, dayOfWeek) 42 | week.days = week.dates() 43 | return week 44 | } 45 | } 46 | 47 | fun dates(): List { 48 | val days = mutableListOf() 49 | val isPart = startDate.monthValue == this.monthOfWeek 50 | days.add(startDate.toJetDay(isPart)) 51 | while (days.size != 7) { 52 | days.add(days.last().nextDay(this)) 53 | } 54 | return days 55 | } 56 | 57 | } 58 | 59 | 60 | fun LocalDate.toJetDay(isPart: Boolean): JetDay { 61 | return JetDay(this, isPart) 62 | } 63 | 64 | private fun JetDay.nextDay(jetWeek: JetWeek): JetDay { 65 | val date = this.date.plusDays(1) 66 | val isPartOfMonth = this.date.plusDays(1).monthValue == jetWeek.monthOfWeek 67 | return JetDay(date, isPartOfMonth) 68 | } 69 | 70 | fun JetWeek.nextWeek(): JetWeek { 71 | val firstDay = this.endDate.plusDays(1) 72 | val week = JetWeek.current(firstDay, dayOfWeek = dayOfWeek) 73 | week.days = week.dates() 74 | return week 75 | } 76 | 77 | -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/weekly/JetCalendarWeekView.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.weekly 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.LazyRow 7 | import androidx.compose.foundation.lazy.items 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.draw.clip 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.compose.ui.text.font.FontWeight 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.unit.sp 20 | import dev.baseio.libjetcalendar.data.JetDay 21 | import dev.baseio.libjetcalendar.data.JetWeek 22 | 23 | @Composable 24 | fun JetCalendarWeekView( 25 | modifier: Modifier, 26 | week: JetWeek, 27 | onDateSelected: (JetDay) -> Unit, 28 | selectedDates: Set, 29 | isGridView: Boolean, 30 | ) { 31 | LazyRow( 32 | modifier = modifier 33 | .fillMaxWidth() 34 | .padding(bottom = 8.dp), 35 | verticalAlignment = Alignment.CenterVertically, 36 | horizontalArrangement = Arrangement.SpaceBetween, 37 | ) { 38 | items(week.days!!) { date -> 39 | Box( 40 | modifier = Modifier 41 | .size(if (!isGridView) 40.dp else 16.dp) 42 | .clip(CircleShape) 43 | .clickable { 44 | if (date.isPartOfMonth) { 45 | onDateSelected(date) 46 | } 47 | } 48 | .background(bgColor(selectedDates, date)), 49 | contentAlignment = Alignment.Center 50 | ) { 51 | Text( 52 | text = date.date.dayOfMonth.toString(), 53 | style = TextStyle( 54 | fontSize = if (isGridView) 8.sp else 14.sp, 55 | fontWeight = FontWeight.SemiBold, 56 | color = if (date.isPartOfMonth) MaterialTheme.typography.body1.color else Color.Transparent 57 | ) 58 | ) 59 | } 60 | } 61 | } 62 | } 63 | 64 | @Composable 65 | private fun bgColor( 66 | selectedDates: Set, 67 | date: JetDay 68 | ) = if (selectedDates.contains(date)) Color.Red else Color.Transparent -------------------------------------------------------------------------------- /featcalendarview/src/main/java/com/praxis/feat/calendarview/ui/CalendarYearView.kt: -------------------------------------------------------------------------------- 1 | package com.praxis.feat.calendarview.ui 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material.* 5 | import androidx.compose.material.icons.Icons 6 | import androidx.compose.material.icons.filled.* 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.unit.dp 11 | import androidx.hilt.navigation.compose.hiltViewModel 12 | import com.google.accompanist.insets.navigationBarsPadding 13 | import com.google.accompanist.insets.statusBarsPadding 14 | import com.mutualmobile.praxis.commonui.material.CommonTopAppBar 15 | import com.mutualmobile.praxis.commonui.theme.PraxisTheme 16 | import dev.baseio.libjetcalendar.data.JetDay 17 | import dev.baseio.libjetcalendar.yearly.JetCalendarYearlyView 18 | import java.time.LocalDate 19 | 20 | @Composable 21 | fun CalendarYearView(viewModel: CalendarYearVM = hiltViewModel()) { 22 | val viewType by viewModel.gridListSwitch.collectAsState() 23 | 24 | Scaffold( 25 | backgroundColor = PraxisTheme.colors.uiBackground, 26 | contentColor = PraxisTheme.colors.textSecondary, 27 | modifier = Modifier 28 | .statusBarsPadding() 29 | .navigationBarsPadding(), 30 | topBar = { 31 | CommonTopAppBar(title = { 32 | }, actions = { 33 | TextButton(modifier = Modifier.then(Modifier.padding(8.dp)), 34 | onClick = { 35 | viewModel.switchView() 36 | }, content = { 37 | Text(text = if (viewType) "Grid" else "List") 38 | }) 39 | }) 40 | }) { 41 | val yearState by viewModel.yearState.collectAsState() 42 | yearState?.let { jetYear -> 43 | JetCalendarYearlyView( 44 | onDateSelected = { 45 | updateViewForViewType(viewModel, it) 46 | }, 47 | selectedDates = setOf(JetDay(LocalDate.now(), isPartOfMonth = true)), 48 | jetYear = jetYear, 49 | dayOfWeek = viewModel.firstDayOfWeek, 50 | isGridView = viewType, 51 | onNextYear = { 52 | viewModel.nextYear() 53 | }, 54 | onPreviousYear = { 55 | viewModel.previousYear() 56 | }, 57 | needsYearNavigator = true 58 | ) 59 | } 60 | 61 | } 62 | 63 | 64 | } 65 | 66 | private fun updateViewForViewType( 67 | calendarYearVM: CalendarYearVM, 68 | jetDay: JetDay 69 | ) { 70 | calendarYearVM.navigateMonth(jetDay) 71 | } 72 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /commonui/src/main/java/com/mutualmobile/praxis/commonui/theme/PraxisSurface.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui.theme 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.border 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.material.LocalContentColor 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.CompositionLocalProvider 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.draw.shadow 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.RectangleShape 15 | import androidx.compose.ui.graphics.Shape 16 | import androidx.compose.ui.graphics.compositeOver 17 | import androidx.compose.ui.unit.Dp 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.zIndex 20 | import kotlin.math.ln 21 | 22 | /** 23 | * An alternative to [androidx.compose.material.Surface] 24 | */ 25 | @Composable 26 | fun PraxisSurface( 27 | modifier: Modifier = Modifier, 28 | shape: Shape = RectangleShape, 29 | color: Color = PraxisTheme.colors.uiBackground, 30 | contentColor: Color = PraxisTheme.colors.textSecondary, 31 | border: BorderStroke? = null, 32 | elevation: Dp = 0.dp, 33 | content: @Composable () -> Unit 34 | ) { 35 | Box( 36 | modifier = modifier 37 | .shadow(elevation = elevation, shape = shape, clip = false) 38 | .zIndex(elevation.value) 39 | .then(if (border != null) Modifier.border(border, shape) else Modifier) 40 | .background( 41 | color = getBackgroundColorForElevation(color, elevation), 42 | shape = shape 43 | ) 44 | .clip(shape) 45 | ) { 46 | CompositionLocalProvider(LocalContentColor provides contentColor, content = content) 47 | } 48 | } 49 | 50 | @Composable 51 | private fun getBackgroundColorForElevation( 52 | color: Color, 53 | elevation: Dp 54 | ): Color { 55 | return if (elevation > 0.dp 56 | ) { 57 | color.withElevation(elevation) 58 | } else { 59 | color 60 | } 61 | } 62 | 63 | /** 64 | * Applies a [Color.White] overlay to this color based on the [elevation]. This increases visibility 65 | * of elevation for surfaces in a dark theme. 66 | */ 67 | private fun Color.withElevation(elevation: Dp): Color { 68 | val foreground = calculateForeground(elevation) 69 | return foreground.compositeOver(this) 70 | } 71 | 72 | /** 73 | * @return the alpha-modified [Color.White] to overlay on top of the surface color to produce 74 | * the resultant color. 75 | */ 76 | private fun calculateForeground(elevation: Dp): Color { 77 | val alpha = ((4.5f * ln(elevation.value + 1)) + 2f) / 20f 78 | return Color.White.copy(alpha = alpha) 79 | } -------------------------------------------------------------------------------- /featcalendarview/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(BuildPlugins.ANDROID_LIBRARY_PLUGIN) 3 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 4 | id(BuildPlugins.KOTLIN_KAPT) 5 | id(BuildPlugins.DAGGER_HILT) 6 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 7 | id("org.jlleitschuh.gradle.ktlint") 8 | } 9 | 10 | android { 11 | compileSdk = ProjectProperties.COMPILE_SDK 12 | 13 | defaultConfig { 14 | minSdk = (ProjectProperties.MIN_SDK) 15 | targetSdk = (ProjectProperties.TARGET_SDK) 16 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 17 | } 18 | 19 | buildTypes { 20 | getByName("release") { 21 | isMinifyEnabled = false 22 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 23 | } 24 | } 25 | 26 | 27 | buildFeatures { 28 | compose = true 29 | } 30 | 31 | composeOptions { 32 | kotlinCompilerExtensionVersion = Lib.Android.COMPOSE_COMPILER 33 | } 34 | packagingOptions { 35 | resources.excludes.add("META-INF/LICENSE.txt") 36 | resources.excludes.add("META-INF/NOTICE.txt") 37 | resources.excludes.add("LICENSE.txt") 38 | resources.excludes.add( "/META-INF/{AL2.0,LGPL2.1}") 39 | } 40 | 41 | compileOptions { 42 | isCoreLibraryDesugaringEnabled = true 43 | sourceCompatibility = JavaVersion.VERSION_1_8 44 | targetCompatibility = JavaVersion.VERSION_1_8 45 | } 46 | 47 | kotlinOptions { 48 | jvmTarget = "1.8" 49 | } 50 | 51 | } 52 | 53 | // Required for annotation processing plugins like Dagger 54 | kapt { 55 | generateStubs = true 56 | correctErrorTypes = true 57 | } 58 | 59 | dependencies { 60 | /*Kotlin*/ 61 | implementation(project(":data")) 62 | implementation(project(":domain")) 63 | implementation(project(":common")) 64 | implementation(project(":navigator")) 65 | implementation(project(":commonui")) 66 | implementation(project(":libjetcalendar")) 67 | 68 | coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:1.1.5") 69 | 70 | api(Lib.Android.COMPOSE_UI) 71 | api(Lib.Android.COIL_COMPOSE) 72 | api(Lib.Android.COMPOSE_MATERIAL) 73 | api(Lib.Android.COMPOSE_UI) 74 | api(Lib.Android.COMPOSE_TOOLING) 75 | debugApi(Lib.Android.DEBUG_TOOLING) 76 | api(Lib.Android.ACT_COMPOSE) 77 | 78 | api(Lib.Android.appCompat) 79 | api(Lib.Kotlin.KTX_CORE) 80 | 81 | api(Lib.Android.ACCOMPANIST_INSETS) 82 | 83 | /*DI*/ 84 | api(Lib.Di.hilt) 85 | api(Lib.Di.hiltNavigationCompose) 86 | api(Lib.Di.viewmodel) 87 | 88 | kapt(Lib.Di.hiltCompiler) 89 | kapt(Lib.Di.hiltAndroidCompiler) 90 | 91 | /* Logger */ 92 | api(Lib.Logger.TIMBER) 93 | /* Async */ 94 | api(Lib.Async.COROUTINES) 95 | api(Lib.Async.COROUTINES_ANDROID) 96 | 97 | testImplementation(TestLib.JUNIT) 98 | testImplementation(TestLib.CORE_TEST) 99 | testImplementation(TestLib.ANDROID_JUNIT) 100 | testImplementation(TestLib.ARCH_CORE) 101 | testImplementation(TestLib.MOCK_WEB_SERVER) 102 | testImplementation(TestLib.ROBO_ELECTRIC) 103 | testImplementation(TestLib.COROUTINES) 104 | testImplementation(TestLib.MOCKK) 105 | } -------------------------------------------------------------------------------- /navigator/src/main/java/com/mutualmobile/praxis/navigator/PraxisNavigator.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.navigator 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavOptionsBuilder 5 | import androidx.navigation.navOptions 6 | import kotlinx.coroutines.flow.* 7 | 8 | class PraxisNavigator : Navigator() { 9 | 10 | override suspend fun handleNavigationCommands(navController: NavController) { 11 | navigationCommands 12 | .onSubscription { this@PraxisNavigator.navControllerFlow.value = navController } 13 | .onCompletion { this@PraxisNavigator.navControllerFlow.value = null } 14 | .collect { navController.handleNavigationCommand(it) } 15 | } 16 | 17 | override fun navigate(route: String, optionsBuilder: (NavOptionsBuilder.() -> Unit)?) { 18 | val options = optionsBuilder?.let { navOptions(it) } 19 | navigationCommands.tryEmit(NavigationCommand.NavigateToRoute(route, options)) 20 | } 21 | 22 | override fun navigateAndClearBackStack(route: String) { 23 | navigationCommands.tryEmit(NavigationCommand.NavigateToRoute(route, navOptions { 24 | popUpTo(0) 25 | })) 26 | } 27 | 28 | override fun navigateUp() { 29 | navigationCommands.tryEmit(NavigationCommand.NavigateUp) 30 | } 31 | 32 | override fun popUpTo(route: String, inclusive: Boolean) { 33 | navigationCommands.tryEmit(NavigationCommand.PopUpToRoute(route, inclusive)) 34 | } 35 | 36 | override fun navigateBackWithResult( 37 | key: String, 38 | result: T, 39 | destination: String? 40 | ) { 41 | navigationCommands.tryEmit( 42 | NavigationCommand.NavigateUpWithResult( 43 | key = key, 44 | result = result, 45 | destination = destination 46 | ) 47 | ) 48 | } 49 | 50 | override fun observeResult(key: String, route: String?): Flow { 51 | return navControllerFlow 52 | .filterNotNull() 53 | .flatMapLatest { navController -> 54 | val backStackEntry = route?.let { navController.getBackStackEntry(it) } 55 | ?: navController.currentBackStackEntry 56 | 57 | backStackEntry?.savedStateHandle?.let { savedStateHandle -> 58 | savedStateHandle.getLiveData(key) 59 | .asFlow() 60 | .filter { it != null } 61 | .onEach { 62 | // Nullify the result to avoid resubmitting it 63 | savedStateHandle.set(key, null) 64 | } 65 | } ?: emptyFlow() 66 | } 67 | } 68 | 69 | private fun NavController.handleNavigationCommand(navigationCommand: NavigationCommand) { 70 | when (navigationCommand) { 71 | is NavigationCommand.NavigateToRoute -> navigate( 72 | navigationCommand.route, 73 | navigationCommand.options 74 | ) 75 | NavigationCommand.NavigateUp -> navigateUp() 76 | is NavigationCommand.PopUpToRoute -> popBackStack( 77 | navigationCommand.route, 78 | navigationCommand.inclusive 79 | ) 80 | is NavigationCommand.NavigateUpWithResult<*> -> { 81 | val backStackEntry = 82 | navigationCommand.destination?.let { getBackStackEntry(it) } 83 | ?: previousBackStackEntry 84 | backStackEntry?.savedStateHandle?.set( 85 | navigationCommand.key, 86 | navigationCommand.result 87 | ) 88 | 89 | navigationCommand.destination?.let { 90 | popBackStack(it, false) 91 | } ?: run { 92 | navigateUp() 93 | } 94 | } 95 | } 96 | } 97 | } -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | // Manifest version information! 2 | 3 | plugins { 4 | id(BuildPlugins.ANDROID_APPLICATION_PLUGIN) 5 | id(BuildPlugins.KOTLIN_ANDROID_PLUGIN) 6 | id(BuildPlugins.KOTLIN_PARCELABLE_PLUGIN) 7 | id(BuildPlugins.KOTLIN_KAPT) 8 | id(BuildPlugins.DAGGER_HILT) 9 | id("org.jlleitschuh.gradle.ktlint") 10 | } 11 | 12 | subprojects { 13 | apply { 14 | from("variants.gradle.kts") 15 | } 16 | } 17 | 18 | // def preDexEnabled = "true" == System.getProperty("pre-dex", "true") 19 | 20 | android { 21 | compileSdk = (ProjectProperties.COMPILE_SDK) 22 | 23 | defaultConfig { 24 | applicationId = (ProjectProperties.APPLICATION_ID) 25 | minSdk = (ProjectProperties.MIN_SDK) 26 | targetSdk = (ProjectProperties.TARGET_SDK) 27 | versionCode = 1 28 | versionName = "1.0" 29 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 30 | vectorDrawables.useSupportLibrary = true 31 | } 32 | 33 | 34 | 35 | buildFeatures { 36 | compose = true 37 | } 38 | 39 | composeOptions { 40 | kotlinCompilerExtensionVersion = Lib.Android.COMPOSE_COMPILER 41 | } 42 | packagingOptions { 43 | resources.excludes.add("META-INF/LICENSE.txt") 44 | resources.excludes.add("META-INF/NOTICE.txt") 45 | resources.excludes.add("LICENSE.txt") 46 | resources.excludes.add( "/META-INF/{AL2.0,LGPL2.1}") 47 | } 48 | 49 | compileOptions { 50 | sourceCompatibility = JavaVersion.VERSION_1_8 51 | targetCompatibility = JavaVersion.VERSION_1_8 52 | } 53 | 54 | kotlinOptions { 55 | jvmTarget = "1.8" 56 | } 57 | } 58 | 59 | // Required for annotation processing plugins like Dagger 60 | kapt { 61 | generateStubs = true 62 | correctErrorTypes = true 63 | } 64 | 65 | dependencies { 66 | implementation(project(":featcalendarview")) 67 | implementation(project(":navigator")) 68 | implementation(project(":data")) 69 | implementation(project(":domain")) 70 | implementation(project(":common")) 71 | implementation(project(":commonui")) 72 | 73 | /* Android Designing and layout */ 74 | implementation(Lib.Android.livedata) 75 | implementation(Lib.Android.navigationCompose) 76 | implementation(Lib.Kotlin.KT_STD) 77 | implementation(Lib.Android.MATERIAL_DESIGN) 78 | implementation(Lib.Android.CONSTRAINT_LAYOUT_COMPOSE) 79 | implementation(Lib.Android.ACCOMPANIST_INSETS) 80 | 81 | implementation(Lib.Android.appCompat) 82 | implementation(Lib.Kotlin.KTX_CORE) 83 | 84 | /*DI*/ 85 | implementation(Lib.Di.hilt) 86 | implementation(Lib.Di.hiltNavigationCompose) 87 | implementation(Lib.Di.viewmodel) 88 | 89 | kapt(Lib.Di.hiltCompiler) 90 | kapt(Lib.Di.hiltAndroidCompiler) 91 | 92 | /* Logger */ 93 | implementation(Lib.Logger.TIMBER) 94 | /* Async */ 95 | implementation(Lib.Async.COROUTINES) 96 | implementation(Lib.Async.COROUTINES_ANDROID) 97 | 98 | /*Testing*/ 99 | testImplementation(TestLib.JUNIT) 100 | testImplementation(TestLib.CORE_TEST) 101 | testImplementation(TestLib.ANDROID_JUNIT) 102 | testImplementation(TestLib.ARCH_CORE) 103 | testImplementation(TestLib.MOCK_WEB_SERVER) 104 | testImplementation(TestLib.ROBO_ELECTRIC) 105 | testImplementation(TestLib.COROUTINES) 106 | testImplementation(TestLib.MOCKK) 107 | 108 | androidTestImplementation("androidx.compose.ui:ui-test-junit4:${Lib.Android.COMPOSE_VERSION}") 109 | debugImplementation("androidx.compose.ui:ui-test-manifest:${Lib.Android.COMPOSE_VERSION}") 110 | } 111 | -------------------------------------------------------------------------------- /featcalendarview/src/main/java/com/praxis/feat/calendarview/ui/CalendarMonthlyView.kt: -------------------------------------------------------------------------------- 1 | package com.praxis.feat.calendarview.ui 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.padding 5 | import androidx.compose.material.* 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.filled.Add 8 | import androidx.compose.material.icons.filled.ArrowBack 9 | import androidx.compose.material.icons.filled.Search 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.dp 14 | import androidx.hilt.navigation.compose.hiltViewModel 15 | import com.google.accompanist.insets.navigationBarsPadding 16 | import com.google.accompanist.insets.statusBarsPadding 17 | import com.mutualmobile.praxis.commonui.material.CommonTopAppBar 18 | import com.mutualmobile.praxis.commonui.theme.PraxisTheme 19 | import dev.baseio.libjetcalendar.data.JetDay 20 | import dev.baseio.libjetcalendar.data.toJetDay 21 | import dev.baseio.libjetcalendar.monthly.JetCalendarMonthlyView 22 | import dev.baseio.libjetcalendar.yearly.JetCalendarYearlyView 23 | import java.time.LocalDate 24 | 25 | @Composable 26 | fun CalendarMonthlyView(viewModel: CalendarMonthVM = hiltViewModel()) { 27 | Scaffold( 28 | backgroundColor = PraxisTheme.colors.uiBackground, 29 | contentColor = PraxisTheme.colors.textSecondary, 30 | modifier = Modifier 31 | .statusBarsPadding() 32 | .navigationBarsPadding(), 33 | topBar = { 34 | CommonTopAppBar(title = { 35 | AppBarTitle(viewModel) 36 | }, 37 | navigationIcon = { 38 | BackButton(viewModel) 39 | }, 40 | actions = { 41 | SearchIcon() 42 | AddEvents() 43 | }) 44 | }) { 45 | MainContent(viewModel) 46 | } 47 | 48 | 49 | } 50 | 51 | @Composable 52 | private fun AppBarTitle(viewModel: CalendarMonthVM) { 53 | val titleText by viewModel.titleState.collectAsState() 54 | Text( 55 | text = titleText, 56 | style = MaterialTheme.typography.h6.copy(color = Color.Red) 57 | ) 58 | } 59 | 60 | @Composable 61 | private fun MainContent(viewModel: CalendarMonthVM) { 62 | val month by viewModel.month.collectAsState() 63 | month?.let { jetMonth -> 64 | JetCalendarMonthlyView( 65 | jetMonth = jetMonth, 66 | onDateSelected = { 67 | viewModel.dateSelected(it.date) 68 | }, 69 | selectedDates = setOf(viewModel.selectedDate.toJetDay(true)), 70 | isGridView = false, 71 | onNextMonth = { 72 | viewModel.nextMonth() 73 | }, 74 | onPreviousMonth = { 75 | viewModel.previousMonth() 76 | }, 77 | needsMonthNavigator = true 78 | ) 79 | } 80 | } 81 | 82 | @Composable 83 | private fun AddEvents() { 84 | IconButton(modifier = Modifier.then(Modifier.padding(8.dp)), 85 | onClick = { 86 | }, content = { 87 | Icon( 88 | Icons.Filled.Add, 89 | "add events", 90 | tint = Color.Red 91 | ) 92 | }) 93 | } 94 | 95 | @Composable 96 | private fun SearchIcon() { 97 | IconButton(modifier = Modifier.then(Modifier.padding(8.dp)), 98 | onClick = { 99 | }, content = { 100 | Icon( 101 | Icons.Filled.Search, 102 | "search", 103 | tint = Color.Red 104 | ) 105 | }) 106 | } 107 | 108 | @Composable 109 | private fun BackButton(viewModel: CalendarMonthVM) { 110 | Icon( 111 | imageVector = Icons.Filled.ArrowBack, 112 | contentDescription = "Back", 113 | modifier = Modifier 114 | .padding(16.dp) 115 | .clickable { 116 | // Implement back action here 117 | viewModel.navBack() 118 | }, 119 | tint = Color.Red 120 | ) 121 | } 122 | -------------------------------------------------------------------------------- /app/proguard-common.txt: -------------------------------------------------------------------------------- 1 | #MM Proguard Settings for Praxis 2 | # this is an extension to the recommended settings for android 3 | # provided in the default android proguard configuration 4 | # 5 | # This file should only be edited if Praxis requires base configuration changes 6 | # please put project specific directives in proguard-specific.txt 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface class: 10 | #-keepclassmembers class com.example.android.fragment.dialog.WebPortalDialogFragment$SSOInterface { 11 | # public *; 12 | #} 13 | 14 | -target 1.6 15 | -optimizationpasses 5 16 | 17 | # For native methods, see http://proguard.sourceforge.net/manual/examples.html#native 18 | -keepclasseswithmembernames class * { 19 | native ; 20 | } 21 | 22 | -keep public class android.net.http.SslError 23 | -keep public class android.webkit.WebViewClient 24 | 25 | -dontwarn android.webkit.WebView 26 | -dontwarn android.net.http.SslError 27 | -dontwarn android.webkit.WebViewClient 28 | 29 | #This will print mappings - very useful for troubleshooting. 30 | -dump ./build/class_files.txt 31 | -printseeds ./build/seeds.txt 32 | -printusage ./build/unused.txt 33 | -printmapping ./build/mapping.txt 34 | 35 | #Some recommended settings for running with Android 36 | -keepattributes ** 37 | -keep public class * extends android.app.Activity 38 | -keep public class * extends android.app.Application 39 | -keep public class * extends android.app.Service 40 | -keep public class * extends android.content.BroadcastReceiver 41 | -keep public class * extends android.content.ContentProvider 42 | -keep public class * extends android.app.Fragment 43 | -keep public class * extends android.support.v4.app.Fragment 44 | 45 | # There's no way to keep all @Observes methods, so use the On*Event convention to identify event handlers 46 | -keepclassmembers class * { 47 | void *(**On*Event); 48 | } 49 | 50 | #Need this to keep serializable members as is 51 | -keepclassmembers class * implements java.io.Serializable { 52 | static final long serialVersionUID; 53 | private static final java.io.ObjectStreamField[] serialPersistentFields; 54 | private void writeObject(java.io.ObjectOutputStream); 55 | private void readObject(java.io.ObjectInputStream); 56 | java.lang.Object writeReplace(); 57 | java.lang.Object readResolve(); 58 | } 59 | 60 | #Handling library optimization 61 | #-repackageclasses '' 62 | -allowaccessmodification 63 | # The -optimizations option disables some arithmetic simplifications that Dalvik 1.0 and 1.5 can't handle. 64 | -optimizations !code/simplification/arithmetic,!code/allocation/variable 65 | 66 | -keep public enum * {} 67 | -keep public interface * {} 68 | -keepattributes ** 69 | 70 | # Keep db4o around 71 | -keep public class com.db4o.** { *; } 72 | -dontwarn sun.misc.Unsafe 73 | 74 | # Keep httpmime around 75 | -keep class org.apache.** { *; } 76 | 77 | # Keep gson around 78 | -keep class com.google.gson.stream.** { *; } 79 | 80 | # Keep hockey around 81 | -keep class net.hockeyapp.** { *; } 82 | 83 | # Keep system tests 84 | -keep class com.customername.androidui.tests.system.** { *; } 85 | 86 | #Avoid 3rd party library warnings 87 | -dontwarn net.hockeyapp.** 88 | -dontwarn javax.xml.stream.** 89 | -dontwarn java.awt.**,javax.security.**,java.beans.** 90 | -dontwarn org.apache.tools.ant.** 91 | -dontwarn org.simpleframework.** 92 | -dontwarn org.junit.** 93 | -dontwarn android.support.** 94 | -dontwarn javax.management.** 95 | -dontwarn java.lang.management.** 96 | -dontwarn android.test.** 97 | -dontwarn org.apache.commons.** 98 | -dontwarn com.google.gson.mm.internal.UnsafeAllocator.** 99 | -dontwarn com.google.inject.** 100 | -dontwarn org.mockito.** 101 | -dontwarn com.jayway.** 102 | -dontwarn org.objenesis.instantiator.** 103 | 104 | -keepattributes *Annotation* 105 | 106 | -keepclassmembers,allowobfuscation class * { 107 | @javax.inject.* *; 108 | @dagger.* *; 109 | (); 110 | } 111 | 112 | -keep class **$$ModuleAdapter 113 | -keep class **$$InjectAdapter 114 | -keep class **$$StaticInjection 115 | 116 | -keepnames class dagger.Lazy 117 | -keepnames class com.mutualmobile.** 118 | 119 | -keepclassmembers class ** { 120 | @com.squareup.otto.Subscribe public *; 121 | @com.squareup.otto.Produce public *; 122 | } 123 | 124 | 125 | -keep class com.google.common.** { *; } 126 | -dontwarn com.google.common.** 127 | 128 | -keepattributes *Annotation*,Signature 129 | -dontwarn retrofit.** 130 | -keep class retrofit.** { *; } 131 | -keepclasseswithmembers class * { 132 | @retrofit.* ; 133 | } 134 | 135 | -dontwarn java.lang.invoke.* -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/monthly/JetCalendarMonthlyView.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.monthly 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.foundation.lazy.items 6 | import androidx.compose.material.Icon 7 | import androidx.compose.material.IconButton 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.ArrowBack 12 | import androidx.compose.material.icons.filled.ArrowForward 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.text.TextStyle 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import dev.baseio.libjetcalendar.data.* 22 | import dev.baseio.libjetcalendar.weekly.JetCalendarWeekView 23 | import java.time.DayOfWeek 24 | 25 | @Composable 26 | fun JetCalendarMonthlyView( 27 | jetMonth: JetMonth, 28 | onDateSelected: (JetDay) -> Unit, 29 | selectedDates: Set, 30 | isGridView: Boolean, 31 | needsMonthNavigator: Boolean = false, 32 | onPreviousMonth: () -> Unit = {}, 33 | onNextMonth: () -> Unit = {} 34 | ) { 35 | Column( 36 | modifier = Modifier 37 | .fillMaxWidth() 38 | .wrapContentHeight() 39 | .padding(4.dp), 40 | verticalArrangement = Arrangement.SpaceAround, 41 | ) { 42 | if (needsMonthNavigator) { 43 | MonthNavigator(onPreviousMonth, jetMonth, isGridView, selectedDates, onNextMonth) 44 | } else { 45 | MonthName(jetMonth, isGridView, selectedDates) 46 | } 47 | jetMonth.monthWeeks.forEach { week -> 48 | JetCalendarWeekView( 49 | modifier = Modifier.fillMaxWidth(), 50 | week = week, 51 | onDateSelected = onDateSelected, 52 | selectedDates = selectedDates, 53 | isGridView 54 | ) 55 | } 56 | 57 | } 58 | } 59 | 60 | @Composable 61 | private fun MonthNavigator( 62 | onPreviousMonth: () -> Unit, 63 | jetMonth: JetMonth, 64 | isGridView: Boolean, 65 | selectedDates: Set, 66 | onNextMonth: () -> Unit 67 | ) { 68 | Row( 69 | horizontalArrangement = Arrangement.SpaceBetween, 70 | modifier = Modifier.fillMaxWidth(), 71 | verticalAlignment = Alignment.CenterVertically 72 | ) { 73 | IconButton(onClick = { 74 | onPreviousMonth() 75 | }) { 76 | Icon( 77 | Icons.Filled.ArrowBack, "Previous Month", 78 | tint = Color.Red 79 | ) 80 | } 81 | MonthName(jetMonth, isGridView, selectedDates) 82 | IconButton(onClick = { 83 | onNextMonth() 84 | }) { 85 | Icon( 86 | Icons.Filled.ArrowForward, "Next Month", 87 | tint = Color.Red 88 | ) 89 | } 90 | } 91 | } 92 | 93 | @Composable 94 | private fun MonthName( 95 | jetMonth: JetMonth, 96 | isGridView: Boolean, 97 | selectedDates: Set 98 | ) { 99 | Text( 100 | text = jetMonth.name(), 101 | style = TextStyle( 102 | fontSize = if (isGridView) 16.sp else 18.sp, 103 | fontWeight = FontWeight.Medium, 104 | color = colorCurrentMonthSelected(selectedDates, jetMonth) 105 | ), 106 | modifier = Modifier.padding(8.dp) 107 | ) 108 | } 109 | 110 | @Composable 111 | fun colorCurrentMonthSelected(selectedDates: Set, jetMonth: JetMonth): Color { 112 | return if (isSameMonth( 113 | jetMonth, 114 | selectedDates 115 | ) 116 | ) Color.Red else MaterialTheme.typography.body1.color 117 | } 118 | 119 | @Composable 120 | private fun isSameMonth( 121 | jetMonth: JetMonth, 122 | selectedDates: Set 123 | ): Boolean { 124 | return selectedDates.any { "${it.date.monthValue}${it.date.year}" == "${jetMonth.startDate.monthValue}${jetMonth.startDate.year}" } 125 | } 126 | 127 | 128 | @Composable 129 | fun WeekNames(isGridView: Boolean, dayOfWeek: DayOfWeek) { 130 | Row( 131 | modifier = Modifier 132 | .fillMaxWidth() 133 | .padding(start = 16.dp, end = 16.dp), 134 | verticalAlignment = Alignment.CenterVertically, 135 | horizontalArrangement = Arrangement.SpaceBetween 136 | ) { 137 | dayNames(dayOfWeek = dayOfWeek).forEach { 138 | Box( 139 | modifier = Modifier 140 | .padding(2.dp), 141 | contentAlignment = Alignment.Center 142 | ) { 143 | Text( 144 | text = it, modifier = Modifier.padding(2.dp), 145 | style = TextStyle( 146 | fontSize = if (isGridView) 8.sp else 12.sp, 147 | fontWeight = FontWeight.Bold 148 | ) 149 | ) 150 | } 151 | 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /commonui/src/main/java/com/mutualmobile/praxis/commonui/theme/SystemUiController.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui.theme 2 | 3 | import android.os.Build 4 | import android.view.View 5 | import android.view.Window 6 | import androidx.compose.runtime.staticCompositionLocalOf 7 | import androidx.compose.ui.graphics.Color 8 | import androidx.compose.ui.graphics.compositeOver 9 | import androidx.compose.ui.graphics.luminance 10 | import androidx.compose.ui.graphics.toArgb 11 | 12 | interface SystemUiController { 13 | fun setStatusBarColor( 14 | color: Color, 15 | darkIcons: Boolean = color.luminance() > 0.5f, 16 | transformColorForLightContent: (Color) -> Color = BlackScrimmed 17 | ) 18 | 19 | fun setNavigationBarColor( 20 | color: Color, 21 | darkIcons: Boolean = color.luminance() > 0.5f, 22 | transformColorForLightContent: (Color) -> Color = BlackScrimmed 23 | ) 24 | 25 | fun setSystemBarsColor( 26 | color: Color, 27 | darkIcons: Boolean = color.luminance() > 0.5f, 28 | transformColorForLightContent: (Color) -> Color = BlackScrimmed 29 | ) 30 | } 31 | 32 | fun SystemUiController(window: Window): SystemUiController { 33 | return SystemUiControllerImpl(window) 34 | } 35 | 36 | /** 37 | * A helper class for setting the navigation and status bar colors for a [Window], gracefully 38 | * degrading behavior based upon API level. 39 | */ 40 | private class SystemUiControllerImpl(private val window: Window) : SystemUiController { 41 | 42 | /** 43 | * Set the status bar color. 44 | * 45 | * @param color The **desired** [Color] to set. This may require modification if running on an 46 | * API level that only supports white status bar icons. 47 | * @param darkIcons Whether dark status bar icons would be preferable. Only available on 48 | * API 23+. 49 | * @param transformColorForLightContent A lambda which will be invoked to transform [color] if 50 | * dark icons were requested but are not available. Defaults to applying a black scrim. 51 | */ 52 | override fun setStatusBarColor( 53 | color: Color, 54 | darkIcons: Boolean, 55 | transformColorForLightContent: (Color) -> Color 56 | ) { 57 | val statusBarColor = when { 58 | darkIcons && Build.VERSION.SDK_INT < 23 -> transformColorForLightContent(color) 59 | else -> color 60 | } 61 | window.statusBarColor = statusBarColor.toArgb() 62 | 63 | if (Build.VERSION.SDK_INT >= 23) { 64 | @Suppress("DEPRECATION") 65 | if (darkIcons) { 66 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or 67 | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR 68 | } else { 69 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and 70 | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv() 71 | } 72 | } 73 | } 74 | 75 | /** 76 | * Set the navigation bar color. 77 | * 78 | * @param color The **desired** [Color] to set. This may require modification if running on an 79 | * API level that only supports white navigation bar icons. Additionally this will be ignored 80 | * and [Color.Transparent] will be used on API 29+ where gesture navigation is preferred or the 81 | * system UI automatically applies background protection in other navigation modes. 82 | * @param darkIcons Whether dark navigation bar icons would be preferable. Only available on 83 | * API 26+. 84 | * @param transformColorForLightContent A lambda which will be invoked to transform [color] if 85 | * dark icons were requested but are not available. Defaults to applying a black scrim. 86 | */ 87 | override fun setNavigationBarColor( 88 | color: Color, 89 | darkIcons: Boolean, 90 | transformColorForLightContent: (Color) -> Color 91 | ) { 92 | val navBarColor = when { 93 | Build.VERSION.SDK_INT >= 29 -> Color.Transparent // For gesture nav 94 | darkIcons && Build.VERSION.SDK_INT < 26 -> transformColorForLightContent(color) 95 | else -> color 96 | } 97 | window.navigationBarColor = navBarColor.toArgb() 98 | 99 | if (Build.VERSION.SDK_INT >= 26) { 100 | @Suppress("DEPRECATION") 101 | if (darkIcons) { 102 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility or 103 | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR 104 | } else { 105 | window.decorView.systemUiVisibility = window.decorView.systemUiVisibility and 106 | View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() 107 | } 108 | } 109 | } 110 | 111 | /** 112 | * Set the status and navigation bars to [color]. 113 | * 114 | * @see setStatusBarColor 115 | * @see setNavigationBarColor 116 | */ 117 | override fun setSystemBarsColor( 118 | color: Color, 119 | darkIcons: Boolean, 120 | transformColorForLightContent: (Color) -> Color 121 | ) { 122 | setStatusBarColor(color, darkIcons, transformColorForLightContent) 123 | setNavigationBarColor(color, darkIcons, transformColorForLightContent) 124 | } 125 | } 126 | 127 | /** 128 | * An [androidx.compose.runtime.CompositionLocalProvider] holding the current [LocalSysUiController]. Defaults to a 129 | * no-op controller; consumers should [provide][androidx.compose.runtime.CompositionLocalProvider] a real one. 130 | */ 131 | val LocalSysUiController = staticCompositionLocalOf { 132 | FakeSystemUiController 133 | } 134 | 135 | private val BlackScrim = Color(0f, 0f, 0f, 0.2f) // 20% opaque black 136 | private val BlackScrimmed: (Color) -> Color = { original -> 137 | BlackScrim.compositeOver(original) 138 | } 139 | 140 | /** 141 | * A fake implementation, useful as a default or used in Previews. 142 | */ 143 | private object FakeSystemUiController : SystemUiController { 144 | override fun setStatusBarColor( 145 | color: Color, 146 | darkIcons: Boolean, 147 | transformColorForLightContent: (Color) -> Color 148 | ) = Unit 149 | 150 | override fun setNavigationBarColor( 151 | color: Color, 152 | darkIcons: Boolean, 153 | transformColorForLightContent: (Color) -> Color 154 | ) = Unit 155 | 156 | override fun setSystemBarsColor( 157 | color: Color, 158 | darkIcons: Boolean, 159 | transformColorForLightContent: (Color) -> Color 160 | ) = Unit 161 | } 162 | -------------------------------------------------------------------------------- /libjetcalendar/src/main/java/dev/baseio/libjetcalendar/yearly/JetCalendarYearlyView.kt: -------------------------------------------------------------------------------- 1 | package dev.baseio.libjetcalendar.yearly 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.lazy.* 7 | import androidx.compose.material.CircularProgressIndicator 8 | import androidx.compose.material.Icon 9 | import androidx.compose.material.IconButton 10 | import androidx.compose.material.Text 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.ArrowBack 13 | import androidx.compose.material.icons.filled.ArrowForward 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.font.FontWeight 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import dev.baseio.libjetcalendar.data.* 23 | import dev.baseio.libjetcalendar.monthly.JetCalendarMonthlyView 24 | import dev.baseio.libjetcalendar.monthly.WeekNames 25 | import java.time.DayOfWeek 26 | 27 | @OptIn(ExperimentalFoundationApi::class) 28 | @Composable 29 | fun JetCalendarYearlyView( 30 | jetYear: JetYear, 31 | onDateSelected: (JetDay) -> Unit, 32 | selectedDates: Set, 33 | isGridView: Boolean = true, 34 | dayOfWeek: DayOfWeek, 35 | startIndex: Int = 0, 36 | needsYearNavigator: Boolean = false, 37 | onPreviousYear: () -> Unit = {}, 38 | onNextYear: () -> Unit = {} 39 | ) { 40 | // affected by https://stackoverflow.com/questions/69739108/how-to-save-paging-state-of-lazycolumn-during-navigation-in-jetpack-compose 41 | val listState = rememberLazyListState(startIndex) 42 | 43 | YearViewInternal( 44 | listState, 45 | jetYear, 46 | onDateSelected, 47 | selectedDates, 48 | isGridView, 49 | dayOfWeek = dayOfWeek, needsYearNavigator, onPreviousYear, onNextYear 50 | ) 51 | } 52 | 53 | 54 | @ExperimentalFoundationApi 55 | @Composable 56 | private fun YearViewInternal( 57 | listState: LazyListState, 58 | jetYear: JetYear, 59 | onDateSelected: (JetDay) -> Unit, 60 | selectedDates: Set, 61 | isGridView: Boolean, 62 | dayOfWeek: DayOfWeek, 63 | needsYearNavigator: Boolean, 64 | onPreviousYear: () -> Unit, 65 | onNextYear: () -> Unit, 66 | ) { 67 | when (jetYear.yearMonths.size) { 68 | 0 -> CircularProgressIndicator(color = Color.Black, modifier = Modifier.padding(8.dp)) 69 | else -> { 70 | if (isGridView) { 71 | GridViewYearly( 72 | listState, 73 | jetYear.yearMonths, 74 | onDateSelected, 75 | selectedDates, 76 | isGridView, 77 | needsYearNavigator, 78 | onPreviousYear, 79 | onNextYear 80 | ) 81 | } else { 82 | Column { 83 | YearNavigatorHeader(jetYear.year(), onPreviousYear, onNextYear) 84 | WeekNames(isGridView, dayOfWeek = dayOfWeek) 85 | ListViewYearly( 86 | listState, jetYear.yearMonths, onDateSelected, selectedDates, isGridView, 87 | needsYearNavigator, 88 | onPreviousYear, 89 | onNextYear 90 | ) 91 | } 92 | } 93 | } 94 | } 95 | 96 | } 97 | 98 | @ExperimentalFoundationApi 99 | @Composable 100 | private fun ListViewYearly( 101 | listState: LazyListState, 102 | pagedMonths: List, 103 | onDateSelected: (JetDay) -> Unit, 104 | selectedDates: Set, 105 | isGridView: Boolean, 106 | needsYearNavigator: Boolean, 107 | onPreviousYear: () -> Unit, 108 | onNextYear: () -> Unit 109 | ) { 110 | LazyColumn( 111 | state = listState, 112 | modifier = Modifier 113 | .fillMaxWidth() 114 | .fillMaxHeight() 115 | ) { 116 | for (index in pagedMonths.indices) { 117 | item { 118 | CalendarMonthlyBox( 119 | pagedMonths, 120 | index, 121 | onDateSelected, 122 | selectedDates, 123 | isGridView 124 | ) 125 | } 126 | } 127 | } 128 | } 129 | 130 | @ExperimentalFoundationApi 131 | @Composable 132 | private fun GridViewYearly( 133 | listState: LazyListState, 134 | pagedMonths: List, 135 | onDateSelected: (JetDay) -> Unit, 136 | selectedDates: Set, 137 | isGridView: Boolean, 138 | needsYearNavigator: Boolean, 139 | onPreviousYear: () -> Unit, 140 | onNextYear: () -> Unit, 141 | ) { 142 | LazyVerticalGrid( 143 | cells = GridCells.Fixed(3), 144 | state = listState, 145 | modifier = Modifier 146 | .fillMaxWidth() 147 | .fillMaxHeight() 148 | ) { 149 | for (index in pagedMonths.indices) { 150 | when { 151 | index % 12 == 0 -> { 152 | item(span = { GridItemSpan(3) }) { 153 | if (needsYearNavigator) { 154 | YearNavigatorHeader(pagedMonths[index].year(), onPreviousYear, onNextYear) 155 | } else { 156 | YearHeader(pagedMonths[index].year()) 157 | } 158 | } 159 | item { 160 | CalendarMonthlyBox( 161 | pagedMonths, 162 | index, 163 | onDateSelected, 164 | selectedDates, 165 | isGridView 166 | ) 167 | } 168 | } 169 | else -> { 170 | item { 171 | CalendarMonthlyBox( 172 | pagedMonths, 173 | index, 174 | onDateSelected, 175 | selectedDates, 176 | isGridView 177 | ) 178 | } 179 | } 180 | } 181 | } 182 | } 183 | } 184 | 185 | @Composable 186 | fun YearNavigatorHeader(year: String, onPreviousYear: () -> Unit, onNextYear: () -> Unit) { 187 | Row( 188 | horizontalArrangement = Arrangement.SpaceBetween, 189 | modifier = Modifier.fillMaxWidth(), 190 | verticalAlignment = Alignment.CenterVertically 191 | ) { 192 | IconButton(onClick = { 193 | onPreviousYear() 194 | }) { 195 | Icon( 196 | Icons.Filled.ArrowBack, "Previous Year", 197 | tint = Color.Red 198 | ) 199 | } 200 | YearHeader(year) 201 | IconButton(onClick = { 202 | onNextYear() 203 | }) { 204 | Icon( 205 | Icons.Filled.ArrowForward, "Next Year", 206 | tint = Color.Red 207 | ) 208 | } 209 | } 210 | } 211 | 212 | @Composable 213 | private fun YearHeader( 214 | year: String 215 | ) { 216 | Text( 217 | text = year, 218 | modifier = Modifier.padding(8.dp), 219 | style = TextStyle( 220 | color = Color.Red, 221 | fontSize = 24.sp, 222 | fontWeight = FontWeight.SemiBold 223 | ) 224 | ) 225 | } 226 | 227 | @Composable 228 | private fun CalendarMonthlyBox( 229 | pagedMonths: List, 230 | index: Int, 231 | onDateSelected: (JetDay) -> Unit, 232 | selectedDates: Set, 233 | isGridView: Boolean, 234 | ) { 235 | JetCalendarMonthlyView( 236 | pagedMonths[index], 237 | { 238 | onDateSelected(it) 239 | }, 240 | selectedDates, isGridView = isGridView 241 | ) 242 | } 243 | -------------------------------------------------------------------------------- /commonui/src/main/java/com/mutualmobile/praxis/commonui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package com.mutualmobile.praxis.commonui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.Colors 5 | import androidx.compose.material.MaterialTheme 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.CompositionLocalProvider 8 | import androidx.compose.runtime.SideEffect 9 | import androidx.compose.runtime.Stable 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import androidx.compose.runtime.staticCompositionLocalOf 15 | import androidx.compose.ui.graphics.Color 16 | 17 | private val LightColorPalette = PraxisColorPalette( 18 | brand = White, 19 | accent = PraxisColor, 20 | accentDark = PraxisColor, 21 | iconTint = Grey, 22 | uiBackground = Neutral0, 23 | uiBorder = VeryLightGrey, 24 | uiFloated = FunctionalGrey, 25 | textPrimary = TextPrimary, 26 | textSecondary = TextSecondary, 27 | textSecondaryDark = TextSecondaryDark, 28 | textHelp = Neutral6, 29 | textInteractive = Neutral0, 30 | textLink = Ocean11, 31 | iconSecondary = Neutral7, 32 | iconInteractive = PraxisColor, 33 | iconInteractiveInactive = Grey, 34 | error = FunctionalRed, 35 | progressIndicatorBg = LightGrey, 36 | switchColor = PraxisColor, 37 | statusBarColor = PraxisColor, 38 | isDark = false, 39 | searchBarBgColor = LightGrey, 40 | buttonColor = PraxisColor, 41 | buttonTextColor = White 42 | ) 43 | 44 | private val DarkColorPalette = PraxisColorPalette( 45 | brand = Shadow1, 46 | accent = PraxisColor, 47 | accentDark = DarkGreen, 48 | iconTint = Shadow1, 49 | uiBackground = GreyBg, 50 | uiBorder = Neutral3, 51 | uiFloated = FunctionalDarkGrey, 52 | textPrimary = Shadow1, 53 | textSecondary = Neutral0, 54 | textHelp = Neutral1, 55 | textInteractive = Neutral7, 56 | textLink = Ocean2, 57 | iconPrimary = Neutral3, 58 | iconSecondary = Neutral0, 59 | textSecondaryDark = Neutral0, 60 | iconInteractive = White, 61 | iconInteractiveInactive = Neutral2, 62 | error = FunctionalRedDark, 63 | progressIndicatorBg = LightGrey, 64 | switchColor = PraxisColor, 65 | statusBarColor = GreyBg, 66 | isDark = true, 67 | searchBarBgColor = SearchBarDarkColor, 68 | buttonColor = PraxisColor, 69 | buttonTextColor = White 70 | 71 | ) 72 | 73 | @Composable 74 | fun PraxisTheme( 75 | darkTheme: Boolean = isSystemInDarkTheme(), 76 | content: @Composable () -> Unit 77 | ) { 78 | val colors = if (darkTheme) DarkColorPalette else LightColorPalette 79 | 80 | val sysUiController = LocalSysUiController.current 81 | SideEffect { 82 | sysUiController.setSystemBarsColor( 83 | color = colors.uiBackground.copy(alpha = AlphaNearOpaque) 84 | ) 85 | } 86 | 87 | ProvidePraxisColors(colors) { 88 | MaterialTheme( 89 | colors = debugColors(darkTheme), 90 | typography = PraxisTypography, 91 | shapes = PraxisShapes, 92 | content = content 93 | ) 94 | } 95 | } 96 | 97 | object PraxisTheme { 98 | val colors: PraxisColorPalette 99 | @Composable 100 | get() = LocalPraxisColor.current 101 | } 102 | 103 | /** 104 | * Praxis custom Color Palette 105 | */ 106 | @Stable 107 | class PraxisColorPalette( 108 | brand: Color, 109 | accent: Color, 110 | accentDark: Color, 111 | iconTint: Color, 112 | uiBackground: Color, 113 | uiBorder: Color, 114 | uiFloated: Color, 115 | textPrimary: Color = brand, 116 | textSecondaryDark: Color, 117 | textSecondary: Color, 118 | textHelp: Color, 119 | textInteractive: Color, 120 | textLink: Color, 121 | iconPrimary: Color = brand, 122 | iconSecondary: Color, 123 | iconInteractive: Color, 124 | iconInteractiveInactive: Color, 125 | error: Color, 126 | notificationBadge: Color = error, 127 | progressIndicatorBg: Color, 128 | switchColor: Color, 129 | statusBarColor: Color, 130 | isDark: Boolean, 131 | searchBarBgColor: Color, 132 | buttonColor: Color, 133 | buttonTextColor: Color 134 | ) { 135 | var searchBarBg by mutableStateOf(searchBarBgColor) 136 | private set 137 | var brand by mutableStateOf(brand) 138 | private set 139 | var accent by mutableStateOf(accent) 140 | private set 141 | var accentDark by mutableStateOf(accentDark) 142 | private set 143 | var iconTint by mutableStateOf(iconTint) 144 | private set 145 | var uiBackground by mutableStateOf(uiBackground) 146 | private set 147 | var statusBarColor by mutableStateOf(statusBarColor) 148 | private set 149 | var uiBorder by mutableStateOf(uiBorder) 150 | private set 151 | var uiFloated by mutableStateOf(uiFloated) 152 | private set 153 | var textPrimary by mutableStateOf(textPrimary) 154 | private set 155 | var textSecondary by mutableStateOf(textSecondary) 156 | private set 157 | var textSecondaryDark by mutableStateOf(textSecondaryDark) 158 | private set 159 | var textHelp by mutableStateOf(textHelp) 160 | private set 161 | var textInteractive by mutableStateOf(textInteractive) 162 | private set 163 | var textLink by mutableStateOf(textLink) 164 | private set 165 | var iconPrimary by mutableStateOf(iconPrimary) 166 | private set 167 | var iconSecondary by mutableStateOf(iconSecondary) 168 | private set 169 | var iconInteractive by mutableStateOf(iconInteractive) 170 | private set 171 | var iconInteractiveInactive by mutableStateOf(iconInteractiveInactive) 172 | private set 173 | var error by mutableStateOf(error) 174 | private set 175 | var notificationBadge by mutableStateOf(notificationBadge) 176 | private set 177 | var progressIndicatorBg by mutableStateOf(progressIndicatorBg) 178 | private set 179 | var switchColor by mutableStateOf(switchColor) 180 | private set 181 | var isDark by mutableStateOf(isDark) 182 | private set 183 | var buttonColor by mutableStateOf(buttonColor) 184 | private set 185 | 186 | var buttonTextColor by mutableStateOf(buttonTextColor) 187 | private set 188 | 189 | 190 | fun update(other: PraxisColorPalette) { 191 | brand = other.brand 192 | uiBackground = other.uiBackground 193 | uiBorder = other.uiBorder 194 | uiFloated = other.uiFloated 195 | textPrimary = other.textPrimary 196 | textSecondary = other.textSecondary 197 | textHelp = other.textHelp 198 | textInteractive = other.textInteractive 199 | textLink = other.textLink 200 | iconPrimary = other.iconPrimary 201 | iconSecondary = other.iconSecondary 202 | iconInteractive = other.iconInteractive 203 | iconInteractiveInactive = other.iconInteractiveInactive 204 | error = other.error 205 | notificationBadge = other.notificationBadge 206 | switchColor = other.switchColor 207 | statusBarColor = other.statusBarColor 208 | isDark = other.isDark 209 | searchBarBg = other.searchBarBg 210 | buttonColor = other.buttonColor 211 | buttonTextColor = other.buttonTextColor 212 | } 213 | } 214 | 215 | @Composable 216 | fun ProvidePraxisColors( 217 | colors: PraxisColorPalette, 218 | content: @Composable () -> Unit 219 | ) { 220 | val colorPalette = remember { colors } 221 | colorPalette.update(colors) 222 | CompositionLocalProvider(LocalPraxisColor provides colorPalette, content = content) 223 | } 224 | 225 | private val LocalPraxisColor = staticCompositionLocalOf { 226 | error("No PraxisColorPalette provided") 227 | } 228 | 229 | /** 230 | * A Material [Colors] implementation which sets all colors to [debugColor] to discourage usage of 231 | * [MaterialTheme.colors] in preference to [PraxisTheme.colors]. 232 | */ 233 | fun debugColors( 234 | darkTheme: Boolean, 235 | debugColor: Color = GreyBg 236 | ) = Colors( 237 | primary = debugColor, 238 | primaryVariant = debugColor, 239 | secondary = debugColor, 240 | secondaryVariant = debugColor, 241 | background = debugColor, 242 | surface = debugColor, 243 | error = debugColor, 244 | onPrimary = debugColor, 245 | onSecondary = debugColor, 246 | onBackground = debugColor, 247 | onSurface = debugColor, 248 | onError = debugColor, 249 | isLight = !darkTheme 250 | ) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------