├── .github └── workflows │ └── verify-build.yml ├── .gitignore ├── .idea ├── .gitignore ├── androidTestResultsUserPreferences.xml ├── compiler.xml ├── deploymentTargetDropDown.xml ├── gradle.xml ├── inspectionProfiles │ └── Project_Default.xml ├── jarRepositories.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── benchmark-rules.pro ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── baseline-prof.txt │ ├── ic_launcher-playstore.png │ ├── java │ └── pseudoankit │ │ └── droid │ │ └── tasky │ │ ├── TaskyApplication.kt │ │ ├── di │ │ └── AppModule.kt │ │ ├── launcher │ │ ├── MainActivity.kt │ │ ├── MainActivityModule.kt │ │ └── MainActivityViewModel.kt │ │ ├── navigation │ │ ├── deeplink │ │ │ └── DeepLinkProvider.kt │ │ ├── di │ │ │ └── NavigationModule.kt │ │ ├── navgraph │ │ │ └── NavGraph.kt │ │ └── navigator │ │ │ ├── CoreFeatureNavigator.kt │ │ │ └── feature │ │ │ ├── AuthNavigatorImpl.kt │ │ │ └── HomeScreenNavigatorImpl.kt │ │ └── util │ │ └── SplashScreenUtil.kt │ └── res │ ├── values │ ├── strings.xml │ └── themes.xml │ └── xml │ ├── backup_rules.xml │ └── data_extraction_rules.xml ├── benchmark ├── .gitignore ├── build.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── pseudoankit │ └── tasky │ └── benchmark │ ├── BaselineProfileGenerator.kt │ ├── BenchmarkTest.kt │ └── util │ ├── AgendaScreenBenchmark.kt │ ├── AppStartBenchmark.kt │ ├── AuthBenchmark.kt │ ├── Constants.kt │ └── HomeScreenBenchmark.kt ├── build.gradle.kts ├── buildSrc ├── .gitignore ├── build.gradle.kts ├── settings.gradle.kts └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ ├── Modules.kt │ ├── Plugins.kt │ ├── VersionCatalog.kt │ └── plugin │ ├── RoomPlugin.kt │ ├── UnitTestPlugin.kt │ ├── base │ ├── CoreLibraryFeaturePlugin.kt │ └── CorePlugin.kt │ ├── compose │ ├── ComposeCorePlugin.kt │ └── ComposeFeaturePlugin.kt │ └── util │ ├── PluginConstants.kt │ └── PluginExtensions.kt ├── config └── detekt │ └── detekt.yml ├── core ├── agenda-manger │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── agendamanger │ │ │ ├── data │ │ │ ├── local │ │ │ │ ├── dao │ │ │ │ │ └── ReminderDao.kt │ │ │ │ └── entity │ │ │ │ │ └── ReminderEntity.kt │ │ │ └── repository │ │ │ │ ├── AgendaRepositoryImpl.kt │ │ │ │ └── ReminderRepositoryImpl.kt │ │ │ ├── di │ │ │ └── AgendaManagerModule.kt │ │ │ ├── domain │ │ │ ├── mapper │ │ │ │ └── ReminderMapper.kt │ │ │ ├── model │ │ │ │ ├── AgendaItem.kt │ │ │ │ └── AgendaTypes.kt │ │ │ ├── repository │ │ │ │ ├── AgendaRepository.kt │ │ │ │ └── ReminderRepository.kt │ │ │ └── usecase │ │ │ │ └── reminder │ │ │ │ ├── GetSavedAgendaItemsUseCase.kt │ │ │ │ ├── SaveReminderUseCase.kt │ │ │ │ └── TriggerAlarmUseCase.kt │ │ │ └── util │ │ │ └── AgendaConstants.kt │ │ └── test │ │ └── java │ │ └── pseudoankit │ │ └── droid │ │ └── agendamanger │ │ ├── data │ │ └── repository │ │ │ ├── AgendaRepositoryImplTest.kt │ │ │ └── ReminderRepositoryImplTest.kt │ │ └── util │ │ └── TestUtil.kt ├── alarm-manager │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── pseudoankit │ │ └── droid │ │ └── alarm_scheduler │ │ ├── data │ │ ├── AlarmReceiver.kt │ │ └── AndroidAlarmScheduler.kt │ │ ├── di │ │ └── AlarmManagerModule.kt │ │ └── domain │ │ ├── AlarmScheduler.kt │ │ └── model │ │ └── Alarm.kt ├── app-shortcuts-n-widgets │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── pseudoankit │ │ │ └── droid │ │ │ ├── WidgetsNShortcutsManager.kt │ │ │ ├── app_shortcuts │ │ │ └── TaskyShortCutManager.kt │ │ │ ├── app_widgets │ │ │ ├── agenda_items │ │ │ │ ├── AgendaItemAppWidgetReceiver.kt │ │ │ │ └── AgendaItemsAppWidget.kt │ │ │ └── util │ │ │ │ └── WidgetDeeplinkProvider.kt │ │ │ └── di │ │ │ └── WidgetsNShortcutsModule.kt │ │ └── res │ │ └── xml │ │ └── tasky_widget_info.xml ├── core-ui │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── coreui │ │ │ ├── deeplink │ │ │ └── DeepLinkReciever.kt │ │ │ ├── destination │ │ │ └── TaskyDestinationStyle.kt │ │ │ ├── koin │ │ │ └── ComposeKoinModule.kt │ │ │ ├── model │ │ │ └── TextFieldUiConfig.kt │ │ │ └── util │ │ │ └── extension │ │ │ ├── ContextExtension.kt │ │ │ ├── ModifierExtension.kt │ │ │ ├── NavControllerExtension.kt │ │ │ ├── OrbitMviExtension.kt │ │ │ ├── StateExtension.kt │ │ │ └── TextResourceExtension.kt │ │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_splash.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ └── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png ├── core │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── core │ │ │ ├── coroutine │ │ │ └── CoroutineUtil.kt │ │ │ ├── deeplink │ │ │ ├── DeepLinkUtil.kt │ │ │ └── TaskyDeeplink.kt │ │ │ ├── di │ │ │ └── CoreModule.kt │ │ │ ├── dispatcher │ │ │ ├── DispatcherProvider.kt │ │ │ └── DispatcherProviderImpl.kt │ │ │ ├── koin │ │ │ └── BaseKoinModule.kt │ │ │ ├── logger │ │ │ └── TaskyLogger.kt │ │ │ ├── model │ │ │ ├── TaskyDate.kt │ │ │ └── TaskyTime.kt │ │ │ ├── testtag │ │ │ ├── AgendaTestTag.kt │ │ │ ├── AuthTestTag.kt │ │ │ └── HomeTestTag.kt │ │ │ ├── util │ │ │ ├── JsonSerializer.kt │ │ │ ├── TaskyResult.kt │ │ │ ├── TextResource.kt │ │ │ ├── datetime │ │ │ │ ├── DateUtils.kt │ │ │ │ └── TimeUtils.kt │ │ │ ├── extension │ │ │ │ ├── CollectionExtension.kt │ │ │ │ ├── DateTimeExtension.kt │ │ │ │ ├── IntentExtension.kt │ │ │ │ ├── LazyInitializerExtension.kt │ │ │ │ ├── PrimitiveExtension.kt │ │ │ │ └── SafeCall.kt │ │ │ └── validator │ │ │ │ └── Validator.kt │ │ │ └── widget │ │ │ └── UpdateAppWidgetFlow.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── database-manager │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── pseudoankit │ │ └── droid │ │ └── dbmanager │ │ ├── TaskyDataBase.kt │ │ ├── di │ │ └── DataBaseModule.kt │ │ └── typeconvertor │ │ └── DateTimeTypeConvertor.kt ├── design-system │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ ├── debug │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── unify │ │ │ └── UnifyActivity.kt │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── unify │ │ │ ├── component │ │ │ ├── button │ │ │ │ ├── UnifyButton.kt │ │ │ │ └── UnifyButtonMapper.kt │ │ │ ├── card │ │ │ │ └── UnifyCard.kt │ │ │ ├── dialog │ │ │ │ ├── UnifyDialog.kt │ │ │ │ ├── UnifyDialogState.kt │ │ │ │ ├── datepicker │ │ │ │ │ ├── UnifyDatePicker.kt │ │ │ │ │ └── UnifyDatePickerToken.kt │ │ │ │ └── timepicker │ │ │ │ │ ├── UnifyTimePicker.kt │ │ │ │ │ └── UnifyTimePickerToken.kt │ │ │ ├── divider │ │ │ │ └── UnifyDivider.kt │ │ │ ├── fab │ │ │ │ └── UnifyFloatingButton.kt │ │ │ ├── icon │ │ │ │ ├── UnifyIcon.kt │ │ │ │ ├── UnifyIconInternal.kt │ │ │ │ └── UnifyIcons.kt │ │ │ ├── list │ │ │ │ ├── UnifyListComponents.kt │ │ │ │ └── UnifyListItem.kt │ │ │ ├── pill │ │ │ │ └── UnifyPill.kt │ │ │ ├── progressbar │ │ │ │ └── UnifyProgressIndicator.kt │ │ │ ├── swipeablecard │ │ │ │ └── SwipeableCard.kt │ │ │ ├── switch │ │ │ │ ├── UnifySwitch.kt │ │ │ │ └── UnifySwitchTokens.kt │ │ │ ├── textfield │ │ │ │ ├── UnifyTextField.kt │ │ │ │ ├── UnifyTextFieldDefaults.kt │ │ │ │ └── internal │ │ │ │ │ ├── UnifyTextFieldCommon.kt │ │ │ │ │ ├── UnifyTextFieldInternal.kt │ │ │ │ │ └── UnifyTextFieldTokens.kt │ │ │ ├── textview │ │ │ │ ├── UnifyTextType.kt │ │ │ │ └── UnifyTextView.kt │ │ │ ├── topbar │ │ │ │ ├── UnifySmallTopBar.kt │ │ │ │ └── UnifyTopBar.kt │ │ │ └── viewpager │ │ │ │ ├── HorizontalTabsInternal.kt │ │ │ │ └── HorizontalViewPager.kt │ │ │ ├── screen │ │ │ ├── UnifyScreen.kt │ │ │ └── UnifyScreenConfig.kt │ │ │ ├── token │ │ │ ├── UnifyColors.kt │ │ │ ├── UnifyDimens.kt │ │ │ ├── UnifyTheme.kt │ │ │ └── UnifyTokens.kt │ │ │ └── utils │ │ │ ├── DarkThemeRipple.kt │ │ │ ├── Logs.kt │ │ │ ├── ModifierExtension.kt │ │ │ ├── Utils.kt │ │ │ └── internal │ │ │ └── Utils.kt │ │ └── res │ │ └── drawable │ │ ├── ic_add.xml │ │ ├── ic_calendar.xml │ │ ├── ic_notification.xml │ │ ├── ic_task.xml │ │ └── img_placeholder.xml ├── notification-manager │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── notification_manager │ │ │ ├── TaskyNotifier.kt │ │ │ ├── TaskyNotifierConfig.kt │ │ │ ├── di │ │ │ └── NotifierModule.kt │ │ │ └── util │ │ │ └── TaskyNotifierUtils.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── permission-manager │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── com │ │ └── example │ │ └── permission_manager │ │ ├── PermissionUtils.kt │ │ └── TaskyPermissionStatus.kt ├── preferences-manager │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── pseudoankit │ │ └── droid │ │ ├── di │ │ └── PreferencesModule.kt │ │ └── preferencesmanager │ │ ├── BasePreference.kt │ │ ├── Keys.kt │ │ └── PreferenceRepository.kt └── test-helper │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── pseudoankit │ └── test_helper │ ├── CoroutineDispatcherRule.kt │ ├── TestCoroutineDispatcherProvider.kt │ └── TestUtils.kt ├── feature ├── agenda │ ├── event │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ └── AndroidManifest.xml │ ├── reminder │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── java │ │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── tasky │ │ │ └── reminder │ │ │ ├── di │ │ │ └── ReminderModule.kt │ │ │ ├── navigator │ │ │ ├── ActionNavTypeSerializer.kt │ │ │ ├── ReminderDeepLinkProvider.kt │ │ │ └── ReminderNavigator.kt │ │ │ └── presentation │ │ │ ├── ReminderUiState.kt │ │ │ ├── ReminderViewModel.kt │ │ │ ├── mapper │ │ │ ├── ReminderMapper.kt │ │ │ └── RepeatIntervalUiMapper.kt │ │ │ └── ui │ │ │ ├── ReminderScreen.kt │ │ │ └── ReminderScreenComponents.kt │ └── task │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ └── src │ │ └── main │ │ └── AndroidManifest.xml ├── authentication │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── pseudoankit │ │ │ └── droid │ │ │ └── authentication │ │ │ ├── di │ │ │ ├── LoginModule.kt │ │ │ └── RegistrationModule.kt │ │ │ ├── domain │ │ │ ├── LoginUserUseCase.kt │ │ │ └── RegisterUserUseCase.kt │ │ │ ├── navigator │ │ │ └── AuthNavigator.kt │ │ │ └── presentation │ │ │ ├── login │ │ │ ├── LoginUiState.kt │ │ │ ├── LoginViewModel.kt │ │ │ └── ui │ │ │ │ ├── LoginScreen.kt │ │ │ │ └── LoginScreenComponents.kt │ │ │ └── registration │ │ │ ├── RegistrationScreen.kt │ │ │ ├── RegistrationUiState.kt │ │ │ ├── RegistrationViewModel.kt │ │ │ └── ui │ │ │ └── RegistrationScreenComponents.kt │ │ └── res │ │ └── values │ │ └── strings.xml ├── developer-tools │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── AndroidManifest.xml ├── home │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ └── java │ │ └── pseudoankit │ │ └── droid │ │ └── tasky │ │ └── home │ │ ├── di │ │ ├── AgendaItemScreenModule.kt │ │ └── HomeScreenModule.kt │ │ ├── domain │ │ └── usecase │ │ │ ├── DeleteAgendaUseCase.kt │ │ │ └── ToggleAgendaItemCompletionUseCase.kt │ │ ├── navigator │ │ ├── HomeDeepLinkProvider.kt │ │ └── HomeScreenNavigator.kt │ │ └── presentation │ │ ├── home │ │ ├── HomeUiState.kt │ │ ├── HomeViewModel.kt │ │ └── ui │ │ │ ├── AgendaReminderCard.kt │ │ │ ├── HomeScreen.kt │ │ │ ├── HomeScreenComponents.kt │ │ │ ├── SavedAgendaItem.kt │ │ │ └── ShowBannerAndRequestIfNotificationPermissionDenied.kt │ │ ├── mapper │ │ ├── AgendaItemsUiMapper.kt │ │ └── AgendaTypesUiMapper.kt │ │ └── taskyitems │ │ ├── AgendaItemsScreen.kt │ │ ├── AgendaItemsUiState.kt │ │ └── AgendaItemsViewModel.kt └── profile │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ └── main │ ├── AndroidManifest.xml │ └── java │ └── pseudoankit │ └── droid │ └── profile │ └── presentation │ └── ui │ └── ProfileScreen.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── preview ├── add-reminder.png ├── home.png ├── login.png ├── shortcut.png └── widget.png ├── renovate.json └── settings.gradle.kts /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetDropDown.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tasky 2 | App to set events & reminder, repo created to apply new concepts in practice 3 | 4 | Login | Home | App Widget | Add Reminder | Shortcuts 5 | | :---------------: | :---------------: | :---------------: | :---------------: | :---------------: | 6 | | | | | 7 | 8 | 9 | [Demo](https://drive.google.com/file/d/1FpIbR176fost0GjCY3byaZ6Bqba6ioTY/view?usp=sharing) 10 | 11 | 12 | 🎯 Key Features: 13 | 14 | - Architecture :- MVI [uses orbit mvi](https://orbit-mvi.org/) + Clean Architecture 15 | - Single Activity Arch 16 | - Multi-Module Setup:- demonstrates how to structure an Android project with multiple modules for better code organization and scalability. 17 | - Dependency Injection:- [Koin](https://github.com/InsertKoinIO/koin) 18 | - Jetpack Compose UI 19 | - Navigation in Multi-Module project [uses compose destinations](https://github.com/raamcosta/compose-destinations) 20 | - CI/CD Setup 21 | - generate apk on push to master 22 | - static code analysis with sonarcloud 23 | - lint check 24 | - BuildSrc Setup: Dependency management is centralized using BuildSrc, simplifying dependency declarations and version management. 25 | - Adding widgets and app shortcut 26 | 27 | 28 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | ../.idea -------------------------------------------------------------------------------- /app/benchmark-rules.pro: -------------------------------------------------------------------------------- 1 | -dontobfuscate -------------------------------------------------------------------------------- /app/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 -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 21 | 24 | 25 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/TaskyApplication.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky 2 | 3 | import android.app.Application 4 | import org.koin.android.ext.koin.androidContext 5 | import org.koin.android.ext.koin.androidLogger 6 | import org.koin.core.context.loadKoinModules 7 | import org.koin.core.context.startKoin 8 | import pseudoankit.droid.WidgetsNShortcutsManager 9 | import pseudoankit.droid.tasky.di.AppModule 10 | 11 | class TaskyApplication : Application() { 12 | 13 | override fun onCreate() { 14 | super.onCreate() 15 | loadKoinModules() 16 | } 17 | 18 | private fun loadKoinModules() { 19 | startKoin { 20 | androidLogger() 21 | androidContext(this@TaskyApplication) 22 | } 23 | loadKoinModules(AppModule) 24 | WidgetsNShortcutsManager.initialize(context = this) 25 | } 26 | } -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.di 2 | 3 | import pseudoankit.droid.agendamanger.di.AgendaManagerModule 4 | import pseudoankit.droid.alarm_scheduler.di.AlarmManagerModule 5 | import pseudoankit.droid.core.di.CoreModule 6 | import pseudoankit.droid.dbmanager.di.DataBaseModule 7 | import pseudoankit.droid.di.PreferencesModule 8 | import pseudoankit.droid.di.WidgetsNShortcutsModule 9 | import pseudoankit.droid.notification_manager.di.NotifierModule 10 | import pseudoankit.droid.tasky.navigation.di.NavigationModule 11 | 12 | internal val AppModule = listOf( 13 | DataBaseModule(), 14 | AgendaManagerModule(), // todo remove 15 | AlarmManagerModule(), 16 | NavigationModule(), 17 | CoreModule(), 18 | NotifierModule(), 19 | PreferencesModule(), 20 | WidgetsNShortcutsModule() 21 | ) 22 | -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/launcher/MainActivityModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.launcher 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.module 6 | import pseudoankit.droid.core.koin.BaseKoinModule 7 | 8 | object MainActivityModule : BaseKoinModule() { 9 | 10 | override val modules: Module 11 | get() = module { 12 | viewModel { MainActivityViewModel(get()) } 13 | } 14 | } -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/navigation/deeplink/DeepLinkProvider.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.navigation.deeplink 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 4 | import pseudoankit.droid.app_widgets.util.WidgetDeeplinkProvider 5 | import pseudoankit.droid.core.deeplink.TaskyDeeplink 6 | import pseudoankit.droid.tasky.home.navigator.HomeDeepLinkProvider 7 | import pseudoankit.droid.tasky.reminder.navigator.ActionNavTypeSerializer 8 | import pseudoankit.droid.tasky.reminder.navigator.ReminderDeepLinkProvider 9 | 10 | /** 11 | * Deeplink provider, This class will contains method to provide different screen deeplink 12 | * Just pass the required data and it will convert it the proper deeplink 13 | */ 14 | internal class DeepLinkProvider : ReminderDeepLinkProvider, HomeDeepLinkProvider, 15 | WidgetDeeplinkProvider { 16 | 17 | override fun agendaDetailRoute(action: AgendaTypes): String = when (action) { 18 | is AgendaTypes.Event -> TODO() 19 | is AgendaTypes.Reminder -> reminderScreenRoute(action.action) 20 | is AgendaTypes.Task -> TODO() 21 | } 22 | 23 | override fun reminderScreenRoute(action: AgendaTypes.Action): String { 24 | return TaskyDeeplink.reminder.replace( 25 | TaskyDeeplink.Path.Reminder.action, 26 | ActionNavTypeSerializer.toRouteString(action) 27 | ) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/navigation/di/NavigationModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.navigation.di 2 | 3 | import org.koin.dsl.module 4 | import pseudoankit.droid.app_widgets.util.WidgetDeeplinkProvider 5 | import pseudoankit.droid.tasky.home.navigator.HomeDeepLinkProvider 6 | import pseudoankit.droid.tasky.navigation.deeplink.DeepLinkProvider 7 | import pseudoankit.droid.tasky.reminder.navigator.ReminderDeepLinkProvider 8 | 9 | object NavigationModule { 10 | 11 | operator fun invoke() = module { 12 | val deepLinkProvider = DeepLinkProvider() 13 | factory { deepLinkProvider } 14 | factory { deepLinkProvider } 15 | factory { deepLinkProvider } 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/navigation/navgraph/NavGraph.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.navigation.navgraph 2 | 3 | import com.ramcosta.composedestinations.spec.DestinationSpec 4 | import com.ramcosta.composedestinations.spec.NavGraphSpec 5 | import com.ramcosta.composedestinations.spec.Route 6 | import pseudoankit.droid.authentication.presentation.authenticationDestinations 7 | import pseudoankit.droid.authentication.presentation.destinations.LoginScreenDestination 8 | import pseudoankit.droid.tasky.home.presentation.destinations.HomeScreenDestination 9 | import pseudoankit.droid.tasky.home.presentation.homeDestinations 10 | import pseudoankit.droid.tasky.reminder.presentation.ui.destinations.ReminderScreenDestination 11 | 12 | object NavGraph { 13 | 14 | private val auth = object : NavGraphSpec { 15 | override val destinationsByRoute: Map> = 16 | authenticationDestinations 17 | .associateBy { it.route } 18 | override val route: String = "auth" 19 | override val startRoute: Route = LoginScreenDestination 20 | } 21 | 22 | private val main = object : NavGraphSpec { 23 | override val destinationsByRoute: Map> = 24 | homeDestinations 25 | .plus(ReminderScreenDestination) 26 | .associateBy { it.route } 27 | override val route: String = "main" 28 | override val startRoute: Route = HomeScreenDestination 29 | } 30 | 31 | fun root(isUserLoggedIn: Boolean) = object : NavGraphSpec { 32 | override val destinationsByRoute: Map> = emptyMap() 33 | override val route: String = "root" 34 | override val startRoute: Route = if (isUserLoggedIn) main else auth 35 | override val nestedNavGraphs: List = listOf( 36 | auth, main 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/navigation/navigator/CoreFeatureNavigator.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.navigation.navigator 2 | 3 | import android.content.Context 4 | import androidx.navigation.NavController 5 | import com.ramcosta.composedestinations.navigation.navigate 6 | import pseudoankit.droid.authentication.navigator.AuthNavigator 7 | import pseudoankit.droid.coreui.util.extension.clearStack 8 | import pseudoankit.droid.coreui.util.extension.finish 9 | import pseudoankit.droid.tasky.home.navigator.HomeScreenNavigator 10 | import pseudoankit.droid.tasky.home.presentation.destinations.HomeScreenDestination 11 | import pseudoankit.droid.tasky.navigation.navigator.feature.AuthNavigatorImpl 12 | import pseudoankit.droid.tasky.navigation.navigator.feature.HomeScreenNavigatorImpl 13 | import pseudoankit.droid.tasky.reminder.navigator.ReminderNavigator 14 | 15 | class CoreFeatureNavigator( 16 | private val navController: NavController, 17 | private val context: Context 18 | ) : AuthNavigator by AuthNavigatorImpl(navController, context), 19 | HomeScreenNavigator by HomeScreenNavigatorImpl(navController, context), 20 | ReminderNavigator { 21 | 22 | override fun navigateToHomeScreen() { 23 | navController.navigate(HomeScreenDestination) { 24 | navController.clearStack(this) 25 | } 26 | } 27 | 28 | override fun navigateUp() { 29 | if (navController.popBackStack().not()) { 30 | context.finish() 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/navigation/navigator/feature/AuthNavigatorImpl.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.navigation.navigator.feature 2 | 3 | import android.content.Context 4 | import androidx.navigation.NavController 5 | import com.ramcosta.composedestinations.navigation.navigate 6 | import pseudoankit.droid.authentication.navigator.AuthNavigator 7 | import pseudoankit.droid.authentication.presentation.destinations.RegistrationScreenDestination 8 | 9 | internal class AuthNavigatorImpl( 10 | private val navController: NavController, 11 | private val context: Context 12 | ) : AuthNavigator { 13 | 14 | override fun navigateUp() { 15 | // overridden in CoreNavigatorImpl 16 | } 17 | 18 | override fun navigateToHomeScreen() { 19 | // overridden in CoreNavigatorImpl 20 | } 21 | 22 | override fun navigateToRegistrationScreen() { 23 | navController.navigate(RegistrationScreenDestination) 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/navigation/navigator/feature/HomeScreenNavigatorImpl.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.navigation.navigator.feature 2 | 3 | import android.content.Context 4 | import androidx.navigation.NavController 5 | import com.ramcosta.composedestinations.navigation.navigate 6 | import kotlinx.coroutines.runBlocking 7 | import org.koin.java.KoinJavaComponent.inject 8 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 9 | import pseudoankit.droid.core.deeplink.TaskyDeeplink 10 | import pseudoankit.droid.coreui.deeplink.navigateViaDeepLink 11 | import pseudoankit.droid.coreui.util.extension.toastNotImplemented 12 | import pseudoankit.droid.preferencesmanager.PreferenceRepository 13 | import pseudoankit.droid.tasky.home.navigator.HomeScreenNavigator 14 | import pseudoankit.droid.tasky.reminder.presentation.ui.destinations.ReminderScreenDestination 15 | 16 | internal class HomeScreenNavigatorImpl( 17 | private val navController: NavController, 18 | private val context: Context 19 | ) : HomeScreenNavigator { 20 | 21 | private val prefs: PreferenceRepository by inject(PreferenceRepository::class.java) 22 | 23 | override fun navigateToProfileScreen() { 24 | runBlocking { prefs.setIsLoggedIn(false) } 25 | } 26 | 27 | override fun navigateToAgendaItemsSelectorScreen() { 28 | navController.navigateViaDeepLink(TaskyDeeplink.agendaSelection) 29 | } 30 | 31 | override fun navigateToAgendaScreen(agendaTypes: AgendaTypes) { 32 | when (agendaTypes) { 33 | is AgendaTypes.Reminder -> navController.navigate( 34 | ReminderScreenDestination(agendaTypes.action) 35 | ) 36 | is AgendaTypes.Event -> context.toastNotImplemented() 37 | is AgendaTypes.Task -> context.toastNotImplemented() 38 | } 39 | } 40 | 41 | override fun navigateUp() { 42 | // overridden in CoreNavigatorImpl 43 | } 44 | } -------------------------------------------------------------------------------- /app/src/main/java/pseudoankit/droid/tasky/util/SplashScreenUtil.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.util 2 | 3 | import androidx.core.splashscreen.SplashScreen 4 | 5 | fun SplashScreen.show() { 6 | setKeepOnScreenCondition { true } 7 | } 8 | 9 | fun SplashScreen.hide() { 10 | setKeepOnScreenCondition { false } 11 | } -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Tasky 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /benchmark/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /benchmark/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.android.build.api.dsl.ManagedVirtualDevice 2 | 3 | plugins { 4 | id("com.android.test") 5 | id(Plugins.Core) 6 | } 7 | 8 | android { 9 | namespace = "com.pseudoankit.tasky.benchmark" 10 | 11 | defaultConfig { 12 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 13 | } 14 | 15 | buildTypes { 16 | // This benchmark buildType is used for benchmarking, and should function like your 17 | // release build (for example, with minification on). It"s signed with a debug key 18 | // for easy local/CI testing. 19 | create("benchmark") { 20 | isDebuggable = true 21 | signingConfig = getByName("debug").signingConfig 22 | matchingFallbacks += listOf("release") 23 | proguardFiles("benchmark-rules.pro") 24 | } 25 | } 26 | 27 | testOptions { 28 | managedDevices { 29 | devices { 30 | create("pixelapi31", ManagedVirtualDevice::class) { 31 | device = "Pixel" 32 | apiLevel = 31 33 | systemImageSource = "aosp" 34 | } 35 | } 36 | } 37 | } 38 | 39 | targetProjectPath = ":app" 40 | experimentalProperties["android.experimental.self-instrumenting"] = true 41 | } 42 | 43 | dependencies { 44 | implementation(projects.core.core) 45 | implementation(libs.androidx.benchmark) 46 | implementation(libs.bundles.uiTest) 47 | } 48 | 49 | androidComponents { 50 | beforeVariants(selector().all()) { 51 | it.enabled = it.buildType == "benchmark" 52 | } 53 | } -------------------------------------------------------------------------------- /benchmark/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /benchmark/src/main/java/com/pseudoankit/tasky/benchmark/BaselineProfileGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.tasky.benchmark 2 | 3 | import androidx.benchmark.macro.junit4.BaselineProfileRule 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | import com.pseudoankit.tasky.benchmark.util.PackageName 6 | import com.pseudoankit.tasky.benchmark.util.performHomeScreenOperations 7 | import org.junit.Rule 8 | import org.junit.Test 9 | import org.junit.runner.RunWith 10 | 11 | /* 12 | * to generate run 13 | * ./gradlew :benchmark:pixelapi31BenchmarkAndroidTest -P android.testInstrumentationRunnerArguments.androidx.benchmark.enabledRules=BaselineProfile 14 | */ 15 | 16 | @RunWith(AndroidJUnit4::class) 17 | class BaselineProfileGenerator { 18 | 19 | @get:Rule 20 | val rule = BaselineProfileRule() 21 | 22 | @Test 23 | fun homeScreenTest() = rule.collectBaselineProfile( 24 | packageName = PackageName 25 | ) { 26 | performHomeScreenOperations() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /benchmark/src/main/java/com/pseudoankit/tasky/benchmark/BenchmarkTest.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.tasky.benchmark 2 | 3 | import androidx.benchmark.macro.CompilationMode 4 | import androidx.benchmark.macro.FrameTimingMetric 5 | import androidx.benchmark.macro.StartupMode 6 | import androidx.benchmark.macro.StartupTimingMetric 7 | import androidx.benchmark.macro.junit4.MacrobenchmarkRule 8 | import androidx.test.ext.junit.runners.AndroidJUnit4 9 | import com.pseudoankit.tasky.benchmark.util.PackageName 10 | import com.pseudoankit.tasky.benchmark.util.openApplication 11 | import com.pseudoankit.tasky.benchmark.util.performHomeScreenOperations 12 | import org.junit.Rule 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | 16 | @RunWith(AndroidJUnit4::class) 17 | class BenchmarkTest { 18 | @get:Rule 19 | val benchmarkRule = MacrobenchmarkRule() 20 | 21 | @Test 22 | fun startUpCompilationPartial() = startup(CompilationMode.Partial()) 23 | 24 | @Test 25 | fun startUpCompilationNone() = startup(CompilationMode.None()) 26 | 27 | @Test 28 | fun homeScreenTestCompilationPartial() = homeScreenTest(CompilationMode.Partial()) 29 | 30 | @Test 31 | fun homeScreenTestCompilationNone() = homeScreenTest(CompilationMode.None()) 32 | 33 | private fun startup(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( 34 | packageName = PackageName, 35 | metrics = listOf(StartupTimingMetric()), 36 | iterations = 5, 37 | startupMode = StartupMode.COLD, 38 | compilationMode = compilationMode 39 | ) { 40 | openApplication() 41 | } 42 | 43 | private fun homeScreenTest(compilationMode: CompilationMode) = benchmarkRule.measureRepeated( 44 | packageName = PackageName, 45 | metrics = listOf(FrameTimingMetric()), 46 | iterations = 5, 47 | startupMode = StartupMode.COLD, 48 | compilationMode = compilationMode 49 | ) { 50 | performHomeScreenOperations() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /benchmark/src/main/java/com/pseudoankit/tasky/benchmark/util/AgendaScreenBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.tasky.benchmark.util 2 | 3 | import androidx.benchmark.macro.MacrobenchmarkScope 4 | import androidx.test.uiautomator.By 5 | import pseudoankit.droid.core.testtag.AgendaTestTag 6 | import pseudoankit.droid.core.testtag.ReminderTestTag 7 | 8 | fun MacrobenchmarkScope.addReminder( 9 | text: String = "Remind me about ${Math.random()}" 10 | ) { 11 | val edtReminder = device.findObject(By.res(ReminderTestTag.edtRemindMe)) 12 | val btnSave = device.findObject(By.res(AgendaTestTag.save)) 13 | 14 | edtReminder.text = text 15 | 16 | btnSave.click() 17 | } -------------------------------------------------------------------------------- /benchmark/src/main/java/com/pseudoankit/tasky/benchmark/util/AppStartBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.tasky.benchmark.util 2 | 3 | import androidx.benchmark.macro.MacrobenchmarkScope 4 | 5 | fun MacrobenchmarkScope.openApplication() { 6 | pressHome() 7 | startActivityAndWait() 8 | } -------------------------------------------------------------------------------- /benchmark/src/main/java/com/pseudoankit/tasky/benchmark/util/AuthBenchmark.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.tasky.benchmark.util 2 | 3 | import androidx.benchmark.macro.MacrobenchmarkScope 4 | import androidx.test.uiautomator.By 5 | import pseudoankit.droid.core.testtag.AuthTestTag 6 | 7 | fun MacrobenchmarkScope.performLogin() { 8 | val email = device.findObject(By.res(AuthTestTag.email)) 9 | val password = device.findObject(By.res(AuthTestTag.password)) 10 | val loginBtn = device.findObject(By.res(AuthTestTag.loginBtn)) 11 | 12 | email.text = "lostankit7@gmail.com" 13 | password.text = "qwerty@123" 14 | loginBtn.click() 15 | } -------------------------------------------------------------------------------- /benchmark/src/main/java/com/pseudoankit/tasky/benchmark/util/Constants.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.tasky.benchmark.util 2 | 3 | const val PackageName = "pseudoankit.droid.tasky" -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | maven { url = uri("https://plugins.gradle.org/m2/") } 7 | } 8 | dependencies { 9 | classpath(libs.android.gradle.plugin) 10 | classpath(libs.kotlin.gradle.plugin) 11 | classpath(libs.kotlin.serialization.plugin) 12 | classpath(libs.sonarQube.gradle.plugin) 13 | classpath(libs.detekt.gradle.plugin) 14 | classpath(libs.ksp.gradle.plugin) 15 | } 16 | } 17 | 18 | allprojects { 19 | repositories { 20 | google() 21 | mavenCentral() 22 | maven { url = uri("https://plugins.gradle.org/m2/") } 23 | maven { url = uri("https://jitpack.io") } 24 | } 25 | } 26 | 27 | tasks.register("clean", Delete::class) { 28 | delete(rootProject.buildDir) 29 | } -------------------------------------------------------------------------------- /buildSrc/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | gradlePluginPortal() 7 | google() 8 | } 9 | 10 | dependencies { 11 | implementation(libs.android.gradle.plugin) 12 | implementation(libs.kotlin.gradle.plugin) 13 | 14 | // Make version catalog available in precompiled scripts 15 | // https://github.com/gradle/gradle/issues/15383#issuecomment-1567461389 16 | implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location)) 17 | } 18 | 19 | gradlePlugin { 20 | plugins { 21 | val plugins = listOf( 22 | "core-plugin" to "plugin.base.CorePlugin", 23 | "core-library-feature-plugin" to "plugin.base.CoreLibraryFeaturePlugin", 24 | "compose-core-plugin" to "plugin.compose.ComposeCorePlugin", 25 | "compose-feature-plugin" to "plugin.compose.ComposeFeaturePlugin", 26 | "room-db-plugin" to "plugin.RoomPlugin", 27 | "ut-plugin" to "plugin.UnitTestPlugin" 28 | ) 29 | 30 | plugins.forEach { 31 | register(it.first) { 32 | id = it.first 33 | implementationClass = it.second 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /buildSrc/settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | } 6 | } 7 | 8 | dependencyResolutionManagement { 9 | repositories { 10 | google() 11 | gradlePluginPortal() 12 | } 13 | 14 | versionCatalogs { 15 | create("libs") { 16 | from(files("../gradle/libs.versions.toml")) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /buildSrc/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/Modules.kt: -------------------------------------------------------------------------------- 1 | object Modules { 2 | object Core { 3 | const val Core = ":core:core" 4 | const val CoreUi = ":core:core-ui" 5 | const val DesignSystem = ":core:design-system" 6 | const val TestHelper = ":core:test-helper" 7 | } 8 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/Plugins.kt: -------------------------------------------------------------------------------- 1 | object Plugins { 2 | 3 | /** 4 | * Plugin containing all common code for any gradle 5 | * It includes defaultConfigs, compileOptions, kotlinOptions and koin deps 6 | * Serialization, Parcelable, 7 | * ### Every module should atleast include this plugin 8 | */ 9 | const val Core = "core-plugin" 10 | 11 | /** 12 | * Plugin containing [com.android.library] plugin, core module along with [Plugins.Core] features internally 13 | * ###Applicable in library modules only 14 | */ 15 | const val CoreFeatureLib = "core-library-feature-plugin" 16 | 17 | /** 18 | * Plugin containing setup for enabling compose at module where applied along with compose core deps 19 | */ 20 | const val ComposeCore = "compose-core-plugin" 21 | 22 | /** 23 | * Plugin containing setup for adding compose core [Plugins.ComposeCore] deps with navigation library setup 24 | * It also includes coreUi, koin deps, design-system orbit MVI deps 25 | * Mainly this is intented to use at feature module 26 | * ###Applicable only for library level module 27 | */ 28 | const val ComposeFeatureLib = "compose-feature-plugin" 29 | 30 | /** 31 | * Plugin containing setup to enable room db 32 | */ 33 | const val RoomDatabase = "room-db-plugin" 34 | 35 | /** Plugin containing setup to add unit test in a module */ 36 | const val UnitTestPlugin = "ut-plugin" 37 | 38 | const val AndroidLibrary = "com.android.library" 39 | const val Ksp = "com.google.devtools.ksp" 40 | const val Detekt = "io.gitlab.arturbosch.detekt" 41 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/VersionCatalog.kt: -------------------------------------------------------------------------------- 1 | @file:Suppress("Filename") 2 | 3 | import org.gradle.accessors.dm.LibrariesForLibs 4 | import org.gradle.api.Project 5 | import org.gradle.api.artifacts.VersionCatalog 6 | import org.gradle.api.artifacts.VersionCatalogsExtension 7 | import org.gradle.kotlin.dsl.getByType 8 | import org.gradle.kotlin.dsl.the 9 | 10 | //val org.gradle.api.Project.`projects`: org.gradle.accessors.dm.RootProjectAccessor get() = 11 | // (this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("projects") as org.gradle.accessors.dm.RootProjectAccessor 12 | 13 | // Make version catalog available in precompiled scripts 14 | // https://github.com/gradle/gradle/issues/15383#issuecomment-1567461389 15 | val Project.libs: LibrariesForLibs 16 | get() = the() 17 | 18 | internal val Project.catalog: VersionCatalog 19 | get() = 20 | project.extensions.getByType().named("libs") 21 | 22 | fun VersionCatalog.version(alias: String): String = 23 | this.findVersion(alias).get().requiredVersion 24 | 25 | fun VersionCatalog.bundle(alias: String): Any = 26 | this.findBundle(alias).get() 27 | 28 | fun VersionCatalog.library(alias: String): Any = 29 | this.findLibrary(alias).get() 30 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugin/RoomPlugin.kt: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import Plugins 4 | import libs 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.dependencies 8 | import plugin.util.implementation 9 | import plugin.util.ksp 10 | 11 | class RoomPlugin : Plugin { 12 | 13 | override fun apply(project: Project) { 14 | 15 | project.plugins.apply { 16 | apply(Plugins.Ksp) 17 | } 18 | 19 | project.dependencies { 20 | implementation(project.libs.room.ktx) 21 | implementation(project.libs.room.runtime) 22 | ksp(project.libs.room.compiler) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugin/UnitTestPlugin.kt: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import Modules 4 | import libs 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.dependencies 8 | import plugin.util.testImplementation 9 | import plugin.util.testImplementationProject 10 | 11 | 12 | class UnitTestPlugin : Plugin { 13 | 14 | override fun apply(project: Project) { 15 | 16 | project.dependencies { 17 | testImplementation(project.libs.bundles.test) 18 | testImplementationProject(Modules.Core.TestHelper) 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugin/base/CoreLibraryFeaturePlugin.kt: -------------------------------------------------------------------------------- 1 | package plugin.base 2 | 3 | import Modules 4 | import org.gradle.api.Project 5 | import org.gradle.kotlin.dsl.dependencies 6 | import plugin.util.implementationProject 7 | 8 | /** 9 | * Plugin containing all common code for library gradle 10 | * ###Applicable in library modules only 11 | */ 12 | class CoreLibraryFeaturePlugin : CorePlugin() { 13 | override fun apply(project: Project) { 14 | project.plugins.apply { 15 | apply("com.android.library") 16 | } 17 | 18 | project.dependencies { 19 | with(Modules.Core) { 20 | this@dependencies.implementationProject(Core) 21 | } 22 | } 23 | 24 | super.apply(project) 25 | } 26 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugin/compose/ComposeCorePlugin.kt: -------------------------------------------------------------------------------- 1 | package plugin.compose 2 | 3 | import com.android.build.gradle.BaseExtension 4 | import libs 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | import org.gradle.kotlin.dsl.dependencies 8 | import plugin.util.PluginConstants 9 | import plugin.util.implementation 10 | import plugin.util.lintChecks 11 | 12 | /** 13 | * Plugin containing setup for adding compose core deps 14 | */ 15 | open class ComposeCorePlugin : Plugin { 16 | 17 | override fun apply(project: Project) { 18 | 19 | val androidExtension = 20 | project.extensions.getByName(PluginConstants.ANDROID) as BaseExtension 21 | 22 | androidExtension.apply { 23 | buildFeatures.apply { 24 | compose = true 25 | } 26 | } 27 | 28 | project.dependencies { 29 | implementation(project.libs.bundles.compose) 30 | implementation(platform(project.libs.compose.bom)) 31 | lintChecks(project.libs.slack.lint.compose) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugin/compose/ComposeFeaturePlugin.kt: -------------------------------------------------------------------------------- 1 | package plugin.compose 2 | 3 | import Modules 4 | import libs 5 | import org.gradle.api.Project 6 | import org.gradle.kotlin.dsl.dependencies 7 | import plugin.util.implementation 8 | import plugin.util.implementationProject 9 | import plugin.util.kotlinExtension 10 | import plugin.util.ksp 11 | import plugin.util.libraryExtension 12 | 13 | /** 14 | * Plugin containing setup for adding compose core deps with navigation lib setup 15 | * Also includes coreUi and koin deps 16 | * ###Applicable only for library level module 17 | */ 18 | class ComposeFeaturePlugin : ComposeCorePlugin() { 19 | 20 | override fun apply(project: Project) { 21 | project.plugins.apply { 22 | apply("com.google.devtools.ksp") 23 | } 24 | 25 | project.libraryExtension?.apply { 26 | libraryVariants.all { 27 | project.kotlinExtension.sourceSets.getByName(name) { 28 | kotlin.srcDir("build/generated/ksp/$name/kotlin") 29 | } 30 | } 31 | } 32 | 33 | project.dependencies { 34 | implementation(project.libs.compose.destinations) 35 | ksp(project.libs.compose.destinations.ksp) 36 | implementation(project.libs.compose.orbit.mvi) 37 | implementation(project.libs.koin.compose) 38 | 39 | with(Modules.Core) { 40 | this@dependencies.implementationProject(DesignSystem) 41 | this@dependencies.implementationProject(CoreUi) 42 | } 43 | } 44 | 45 | super.apply(project) 46 | } 47 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugin/util/PluginConstants.kt: -------------------------------------------------------------------------------- 1 | package plugin.util 2 | 3 | internal object PluginConstants { 4 | const val IMPLEMENTATION = "implementation" 5 | const val ANDROID = "android" 6 | const val PATH = "path" 7 | const val KOTLIN = "kotlin" 8 | const val KSP = "ksp" 9 | const val TEST_IMPLEMENTATION = "testImplementation" 10 | } -------------------------------------------------------------------------------- /buildSrc/src/main/java/plugin/util/PluginExtensions.kt: -------------------------------------------------------------------------------- 1 | package plugin.util 2 | 3 | import com.android.build.gradle.AppExtension 4 | import com.android.build.gradle.LibraryExtension 5 | import org.gradle.api.Project 6 | import org.gradle.api.artifacts.Dependency 7 | import org.gradle.api.plugins.ExtensionAware 8 | import org.gradle.kotlin.dsl.DependencyHandlerScope 9 | import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension 10 | 11 | internal val Project.kotlinExtension 12 | get() = (this as ExtensionAware).extensions.getByName(PluginConstants.KOTLIN) as KotlinAndroidProjectExtension 13 | 14 | internal val Project.libraryExtension 15 | get() = project.extensions.getByName(PluginConstants.ANDROID) as? LibraryExtension 16 | 17 | internal val Project.appExtension 18 | get() = project.extensions.getByName(PluginConstants.ANDROID) as? AppExtension 19 | 20 | internal fun DependencyHandlerScope.implementation(dependency: Any) { 21 | add(PluginConstants.IMPLEMENTATION, dependency) 22 | } 23 | 24 | internal fun DependencyHandlerScope.lintChecks(dependency: Any) { 25 | add(PluginConstants.IMPLEMENTATION, dependency) 26 | } 27 | 28 | internal fun DependencyHandlerScope.testImplementation(dependencyNotation: Any): Dependency? = 29 | add(PluginConstants.TEST_IMPLEMENTATION, dependencyNotation) 30 | 31 | internal fun DependencyHandlerScope.testImplementationProject(dependency: String) { 32 | add( 33 | PluginConstants.TEST_IMPLEMENTATION, 34 | project(mapOf(PluginConstants.PATH to dependency)) 35 | ) 36 | } 37 | 38 | internal fun DependencyHandlerScope.implementationProject(dependency: String) { 39 | add( 40 | PluginConstants.IMPLEMENTATION, 41 | project(mapOf(PluginConstants.PATH to dependency)) 42 | ) 43 | } 44 | 45 | internal fun DependencyHandlerScope.ksp(dependency: Any) { 46 | add(PluginConstants.KSP, dependency) 47 | } -------------------------------------------------------------------------------- /core/agenda-manger/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/agenda-manger/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.RoomDatabase) 4 | id(Plugins.UnitTestPlugin) 5 | } 6 | 7 | android { 8 | namespace = "pseudoankit.droid.agendamanger" 9 | } 10 | 11 | dependencies { 12 | implementation(projects.core.alarmManager) 13 | implementation(projects.core.notificationManager) 14 | } 15 | -------------------------------------------------------------------------------- /core/agenda-manger/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/data/local/dao/ReminderDao.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.data.local.dao 2 | 3 | import androidx.room.* 4 | import kotlinx.coroutines.flow.Flow 5 | import pseudoankit.droid.agendamanger.data.local.entity.ReminderEntity 6 | import java.time.LocalDate 7 | 8 | @Dao 9 | interface ReminderDao { 10 | 11 | @Query("Select * from reminderentity") 12 | fun getReminders(): List 13 | 14 | @Query("Select * from reminderentity") 15 | fun getRemindersFlow(): Flow> 16 | 17 | @Query("Select * from reminderentity where date= :date") 18 | fun getRemindersFlow(date: LocalDate): Flow> 19 | 20 | @Query("Select * from reminderentity where id=:id") 21 | fun getReminder(id: Long): ReminderEntity 22 | 23 | @Insert(onConflict = OnConflictStrategy.REPLACE) 24 | suspend fun insert(item: ReminderEntity) 25 | 26 | @Update 27 | suspend fun update(item: ReminderEntity) 28 | 29 | @Delete 30 | suspend fun delete(item: ReminderEntity) 31 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/data/local/entity/ReminderEntity.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.data.local.entity 2 | 3 | import androidx.room.Entity 4 | import androidx.room.PrimaryKey 5 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 6 | import java.time.LocalDate 7 | import java.time.LocalTime 8 | 9 | @Entity 10 | data class ReminderEntity( 11 | val title: String?, 12 | val remindAllDay: Boolean?, 13 | val date: LocalDate?, 14 | val time: LocalTime?, 15 | val repeatInterval: AgendaItem.Reminder.RepeatInterval?, 16 | val completed: Boolean?, 17 | @PrimaryKey val id: Long 18 | ) 19 | -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/data/repository/AgendaRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.data.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import kotlinx.coroutines.flow.combine 5 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 6 | import pseudoankit.droid.agendamanger.domain.repository.AgendaRepository 7 | import pseudoankit.droid.agendamanger.domain.repository.ReminderRepository 8 | import java.time.LocalDate 9 | 10 | // TODO sorting for all agenda types 11 | internal class AgendaRepositoryImpl( 12 | private val reminderRepository: ReminderRepository 13 | ) : AgendaRepository { 14 | 15 | override fun getAllSavedItemFlow(selectedDate: LocalDate?): Flow> { 16 | return combine( 17 | reminderRepository.getReminders(selectedDate) 18 | ) { items -> 19 | items[0] 20 | } 21 | } 22 | 23 | override fun getAllSavedItem(): List { 24 | val reminderItems = reminderRepository.getReminders() 25 | // combine others 26 | 27 | return reminderItems 28 | } 29 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/di/AgendaManagerModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.di 2 | 3 | import org.koin.dsl.module 4 | import pseudoankit.droid.agendamanger.data.repository.AgendaRepositoryImpl 5 | import pseudoankit.droid.agendamanger.data.repository.ReminderRepositoryImpl 6 | import pseudoankit.droid.agendamanger.domain.repository.AgendaRepository 7 | import pseudoankit.droid.agendamanger.domain.repository.ReminderRepository 8 | import pseudoankit.droid.agendamanger.domain.repository.ReminderRepositoryInternal 9 | import pseudoankit.droid.agendamanger.domain.usecase.reminder.SaveReminderUseCase 10 | import pseudoankit.droid.agendamanger.domain.usecase.reminder.TriggerAlarmUseCase 11 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlowInstance 12 | 13 | // TODO move to individual screen 14 | object AgendaManagerModule { 15 | 16 | operator fun invoke() = module { 17 | single { SaveReminderUseCase(UpdateAppWidgetFlowInstance) } 18 | single { TriggerAlarmUseCase(get()) } 19 | 20 | single { ReminderRepositoryImpl(get()) } 21 | single { ReminderRepositoryImpl(get()) } 22 | single { AgendaRepositoryImpl(get()) } 23 | } 24 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/domain/mapper/ReminderMapper.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.domain.mapper 2 | 3 | import pseudoankit.droid.agendamanger.data.local.entity.ReminderEntity 4 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 5 | import pseudoankit.droid.core.model.TaskyDate 6 | import pseudoankit.droid.core.model.TaskyTime 7 | import pseudoankit.droid.core.util.extension.orFalse 8 | import pseudoankit.droid.core.util.extension.orNow 9 | import pseudoankit.droid.core.util.extension.orToday 10 | 11 | internal object ReminderMapper { 12 | 13 | val AgendaItem.Reminder.mapToEntity 14 | get() = ReminderEntity( 15 | title = title, 16 | remindAllDay = time == AgendaItem.Reminder.Time.AllDay, 17 | date = date.value, 18 | time = when (time) { 19 | AgendaItem.Reminder.Time.AllDay -> null 20 | is AgendaItem.Reminder.Time.Time -> time.value.value 21 | }, 22 | repeatInterval = repeatInterval, 23 | id = id, 24 | completed = completed 25 | ) 26 | 27 | val ReminderEntity.mapToDomain 28 | get() = AgendaItem.Reminder( 29 | title = title.orEmpty(), 30 | date = TaskyDate(date.orToday), 31 | time = if (time == null) { 32 | AgendaItem.Reminder.Time.AllDay 33 | } else { 34 | AgendaItem.Reminder.Time.Time(TaskyTime(time.orNow)) 35 | }, 36 | repeatInterval = repeatInterval ?: AgendaItem.Reminder.RepeatInterval.DoNotRepeat, 37 | id = id, 38 | completed = completed.orFalse 39 | ) 40 | 41 | val List.sortByAscDateTime 42 | get() = sortedWith { o1, o2 -> 43 | if (o1.date.value > o2.date.value) return@sortedWith 1 44 | if (o1.date.value < o2.date.value) return@sortedWith -1 45 | o1.time.seconds - o2.time.seconds 46 | } 47 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/domain/model/AgendaTypes.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.domain.model 2 | 3 | import android.os.Parcelable 4 | import kotlinx.parcelize.Parcelize 5 | import pseudoankit.droid.core.testtag.AgendaTestTag 6 | 7 | sealed interface AgendaTypes { 8 | data class Reminder(val action: Action) : AgendaTypes 9 | data class Task(val action: Action) : AgendaTypes 10 | data class Event(val action: Action) : AgendaTypes 11 | 12 | @Parcelize 13 | sealed class Action : Parcelable { 14 | data class Edit(val id: Long) : Action() 15 | object Create : Action() 16 | } 17 | 18 | val testTag 19 | get() = when (this) { 20 | is Event -> AgendaTestTag.event 21 | is Reminder -> AgendaTestTag.reminder 22 | is Task -> AgendaTestTag.task 23 | } 24 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/domain/repository/AgendaRepository.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.domain.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 5 | import java.time.LocalDate 6 | 7 | interface AgendaRepository { 8 | 9 | fun getAllSavedItemFlow(selectedDate: LocalDate? = null): Flow> 10 | 11 | fun getAllSavedItem(): List 12 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/domain/repository/ReminderRepository.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.domain.repository 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 5 | import java.time.LocalDate 6 | 7 | interface ReminderRepository { 8 | fun getReminders(date: LocalDate?): Flow> 9 | fun getReminders(): List 10 | fun getReminder(id: Long): AgendaItem.Reminder 11 | suspend fun delete(payload: AgendaItem.Reminder) 12 | } 13 | 14 | internal interface ReminderRepositoryInternal { 15 | suspend fun save(payload: AgendaItem.Reminder) 16 | suspend fun update(payload: AgendaItem.Reminder) 17 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/domain/usecase/reminder/GetSavedAgendaItemsUseCase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.domain.usecase.reminder 2 | 3 | import kotlinx.collections.immutable.ImmutableList 4 | import kotlinx.collections.immutable.toImmutableList 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.map 7 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 8 | import pseudoankit.droid.agendamanger.domain.repository.AgendaRepository 9 | import pseudoankit.droid.core.model.TaskyDate 10 | 11 | class GetSavedAgendaItemsUseCase( 12 | private val agendaRepository: AgendaRepository 13 | ) { 14 | 15 | operator fun invoke(selectedDate: TaskyDate): Flow> { 16 | return agendaRepository.getAllSavedItemFlow(selectedDate.value) 17 | .map { it.toImmutableList() } 18 | } 19 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/domain/usecase/reminder/SaveReminderUseCase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.domain.usecase.reminder 2 | 3 | import kotlinx.coroutines.coroutineScope 4 | import kotlinx.coroutines.launch 5 | import org.koin.core.component.KoinComponent 6 | import org.koin.core.component.inject 7 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 8 | import pseudoankit.droid.agendamanger.domain.repository.ReminderRepositoryInternal 9 | import pseudoankit.droid.core.util.TaskyResult 10 | import pseudoankit.droid.core.util.extension.safeCall 11 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlow 12 | 13 | class SaveReminderUseCase( 14 | private val updateAppWidgetFlow: UpdateAppWidgetFlow 15 | ) : KoinComponent { 16 | 17 | private val repository: ReminderRepositoryInternal by inject() 18 | private val triggerAlarmUseCase: TriggerAlarmUseCase by inject() 19 | 20 | // TODO: cancel notification if item completed 21 | suspend operator fun invoke( 22 | payload: AgendaItem.Reminder, 23 | alarmDeepLink: String 24 | ): TaskyResult = safeCall( 25 | block = { 26 | coroutineScope { 27 | launch { 28 | triggerAlarmUseCase( 29 | payload, 30 | alarmDeepLink 31 | ) 32 | } 33 | launch { 34 | repository.save(payload) 35 | } 36 | updateAppWidgetFlow.emit(Unit) 37 | } 38 | TaskyResult.Success(Unit) 39 | }, 40 | onError = { 41 | TaskyResult.Error(it) 42 | } 43 | ) 44 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/domain/usecase/reminder/TriggerAlarmUseCase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.domain.usecase.reminder 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 4 | import pseudoankit.droid.alarm_scheduler.domain.AlarmScheduler 5 | import pseudoankit.droid.alarm_scheduler.domain.model.Alarm 6 | import pseudoankit.droid.notification_manager.TaskyNotifierConfig 7 | import java.time.LocalDateTime 8 | import java.time.LocalTime 9 | 10 | internal class TriggerAlarmUseCase( 11 | private val alarmScheduler: AlarmScheduler 12 | ) { 13 | 14 | operator fun invoke(payload: AgendaItem.Reminder, alarmDeepLink: String) { 15 | val time = when (payload.time) { 16 | AgendaItem.Reminder.Time.AllDay -> LocalTime.NOON 17 | is AgendaItem.Reminder.Time.Time -> payload.time.value.value 18 | } 19 | 20 | val alarm = Alarm( 21 | localDateTime = LocalDateTime.of( 22 | payload.date.value.year, 23 | payload.date.value.month, 24 | payload.date.value.dayOfMonth, 25 | time.hour, 26 | time.minute, 27 | time.second 28 | ), 29 | title = payload.title, 30 | navigationUrl = alarmDeepLink, 31 | id = payload.id, 32 | source = TaskyNotifierConfig.Source.Reminder 33 | ) 34 | 35 | if (payload.completed) { 36 | alarmScheduler.cancel(alarm) 37 | } else { 38 | alarmScheduler.schedule(alarm) 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/main/java/pseudoankit/droid/agendamanger/util/AgendaConstants.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.util 2 | 3 | object AgendaConstants { 4 | val id = System.currentTimeMillis() 5 | } -------------------------------------------------------------------------------- /core/agenda-manger/src/test/java/pseudoankit/droid/agendamanger/data/repository/AgendaRepositoryImplTest.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.data.repository 2 | 3 | import io.mockk.coEvery 4 | import io.mockk.mockk 5 | import org.junit.Test 6 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 7 | import pseudoankit.droid.agendamanger.domain.repository.ReminderRepository 8 | import kotlin.test.assertEquals 9 | 10 | 11 | internal class AgendaRepositoryImplTest { 12 | 13 | private val reminderRepository: ReminderRepository = mockk() 14 | private val repository = AgendaRepositoryImpl(reminderRepository) 15 | 16 | 17 | @Test 18 | fun `getAllSavedItem should return data from reminderRepository`() { 19 | val expected = listOf( 20 | AgendaItem.Reminder() 21 | ) 22 | coEvery { reminderRepository.getReminders() } returns expected 23 | 24 | val actual = repository.getAllSavedItem() 25 | 26 | assertEquals(expected, actual) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/agenda-manger/src/test/java/pseudoankit/droid/agendamanger/util/TestUtil.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.agendamanger.util 2 | 3 | import pseudoankit.droid.agendamanger.data.local.entity.ReminderEntity 4 | 5 | val reminderEntity = ReminderEntity( 6 | null, 7 | null, 8 | null, 9 | null, 10 | null, 11 | null, 12 | 1 13 | ) 14 | -------------------------------------------------------------------------------- /core/alarm-manager/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/alarm-manager/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | } 4 | 5 | android { 6 | namespace = "pseudoankit.droid.alarm_scheduler" 7 | } 8 | 9 | dependencies { 10 | implementation(projects.core.notificationManager) 11 | } -------------------------------------------------------------------------------- /core/alarm-manager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /core/alarm-manager/src/main/java/pseudoankit/droid/alarm_scheduler/data/AlarmReceiver.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.alarm_scheduler.data 2 | 3 | import android.content.BroadcastReceiver 4 | import android.content.Context 5 | import android.content.Intent 6 | import org.koin.java.KoinJavaComponent.inject 7 | import pseudoankit.droid.alarm_scheduler.domain.model.Alarm 8 | import pseudoankit.droid.core.logger.TaskyLogger 9 | import pseudoankit.droid.core.util.extension.getParcelableData 10 | import pseudoankit.droid.notification_manager.TaskyNotifier 11 | import pseudoankit.droid.notification_manager.TaskyNotifierConfig 12 | 13 | internal class AlarmReceiver : BroadcastReceiver() { 14 | 15 | private val taskyNotifier: TaskyNotifier by inject(TaskyNotifier::class.java) 16 | 17 | companion object { 18 | private const val ALARM_ARG = "alarm" 19 | fun instance(context: Context, alarm: Alarm): Intent { 20 | return Intent(context, AlarmReceiver::class.java).apply { 21 | putExtra(ALARM_ARG, alarm) 22 | } 23 | } 24 | } 25 | 26 | override fun onReceive(context: Context?, intent: Intent?) { 27 | val alarm = intent?.extras?.getParcelableData(ALARM_ARG) ?: return 28 | 29 | TaskyLogger.info("triggering alarm", alarm.toString()) 30 | taskyNotifier.displayNotification( 31 | TaskyNotifierConfig( 32 | description = alarm.title, 33 | priority = TaskyNotifierConfig.Priority.High, 34 | source = alarm.source, 35 | navigationUrl = alarm.navigationUrl 36 | ) 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/alarm-manager/src/main/java/pseudoankit/droid/alarm_scheduler/data/AndroidAlarmScheduler.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.alarm_scheduler.data 2 | 3 | import android.app.AlarmManager 4 | import android.app.PendingIntent 5 | import android.content.Context 6 | import android.content.Intent 7 | import pseudoankit.droid.alarm_scheduler.domain.AlarmScheduler 8 | import pseudoankit.droid.alarm_scheduler.domain.model.Alarm 9 | import pseudoankit.droid.core.logger.TaskyLogger 10 | 11 | internal class AndroidAlarmScheduler( 12 | private val context: Context 13 | ) : AlarmScheduler { 14 | 15 | private val alarmManager = context.getSystemService(AlarmManager::class.java) 16 | 17 | override fun schedule(alarm: Alarm) { 18 | TaskyLogger.info("scheduling alarm", alarm.toString()) 19 | 20 | createPendingIntent(alarm, intent = { 21 | AlarmReceiver.instance(context, alarm) 22 | })?.let { 23 | alarmManager.setExactAndAllowWhileIdle( 24 | AlarmManager.RTC_WAKEUP, 25 | alarm.timeInMillis, 26 | it 27 | ) 28 | } 29 | } 30 | 31 | override fun cancel(alarm: Alarm) { 32 | TaskyLogger.info("cancelling alarm", alarm.toString()) 33 | createPendingIntent(alarm, intent = { 34 | AlarmReceiver.instance(context, alarm) 35 | })?.let { 36 | alarmManager.cancel( 37 | it 38 | ) 39 | } 40 | } 41 | 42 | private fun createPendingIntent(alarm: Alarm, intent: () -> Intent): PendingIntent? { 43 | return PendingIntent.getBroadcast( 44 | context, 45 | alarm.id.toInt(), 46 | intent(), 47 | PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 48 | ) 49 | } 50 | } -------------------------------------------------------------------------------- /core/alarm-manager/src/main/java/pseudoankit/droid/alarm_scheduler/di/AlarmManagerModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.alarm_scheduler.di 2 | 3 | import org.koin.dsl.module 4 | import pseudoankit.droid.alarm_scheduler.data.AndroidAlarmScheduler 5 | import pseudoankit.droid.alarm_scheduler.domain.AlarmScheduler 6 | 7 | object AlarmManagerModule { 8 | 9 | operator fun invoke() = module { 10 | single { AndroidAlarmScheduler(get()) } 11 | } 12 | } -------------------------------------------------------------------------------- /core/alarm-manager/src/main/java/pseudoankit/droid/alarm_scheduler/domain/AlarmScheduler.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.alarm_scheduler.domain 2 | 3 | import pseudoankit.droid.alarm_scheduler.domain.model.Alarm 4 | 5 | interface AlarmScheduler { 6 | fun schedule(alarm: Alarm) 7 | fun cancel(alarm: Alarm) 8 | } -------------------------------------------------------------------------------- /core/alarm-manager/src/main/java/pseudoankit/droid/alarm_scheduler/domain/model/Alarm.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.alarm_scheduler.domain.model 2 | 3 | import android.os.Build 4 | import android.os.Parcelable 5 | import kotlinx.parcelize.Parcelize 6 | import pseudoankit.droid.notification_manager.TaskyNotifierConfig 7 | import java.time.LocalDateTime 8 | import java.time.ZoneId 9 | import java.time.ZonedDateTime 10 | import java.util.* 11 | 12 | @Parcelize 13 | data class Alarm( 14 | val localDateTime: LocalDateTime, 15 | val title: String, 16 | val navigationUrl: String, 17 | val id: Long, 18 | val source: TaskyNotifierConfig.Source 19 | ) : Parcelable { 20 | 21 | val timeInMillis get() = localDateTime.atZone(ZoneId.systemDefault()).toEpochSecond().times(1000) 22 | } 23 | -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeCore) 4 | } 5 | 6 | android { 7 | namespace = "pseudoankit.droid.app_shortcuts_n_widgets" 8 | } 9 | 10 | dependencies { 11 | implementation(libs.glance.appwidget) 12 | implementation(libs.glance) 13 | implementation(libs.androidx.appShortcut) 14 | 15 | implementation(projects.core.agendaManger) 16 | implementation(projects.core.designSystem) 17 | implementation(projects.core.coreUi) 18 | implementation(projects.core.preferencesManager) 19 | } -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/src/main/java/pseudoankit/droid/WidgetsNShortcutsManager.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid 2 | 3 | import android.content.Context 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.flow.MutableSharedFlow 7 | import kotlinx.coroutines.launch 8 | import org.koin.java.KoinJavaComponent.inject 9 | import pseudoankit.droid.app_shortcuts.TaskyShortCutManager 10 | import pseudoankit.droid.app_widgets.agenda_items.AgendaItemAppWidgetReceiver 11 | import pseudoankit.droid.core.logger.TaskyLogger 12 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlow 13 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlowNamed 14 | 15 | object WidgetsNShortcutsManager { 16 | 17 | private val widgetUpdateFlow: UpdateAppWidgetFlow by inject( 18 | MutableSharedFlow::class.java, 19 | UpdateAppWidgetFlowNamed 20 | ) 21 | 22 | fun initialize(context: Context) { 23 | TaskyShortCutManager.initialize(context) 24 | 25 | CoroutineScope(Dispatchers.IO).launch { 26 | widgetUpdateFlow.collect { 27 | TaskyLogger.info("updating widget") 28 | AgendaItemAppWidgetReceiver.sendAppsUpdatedBroadcast(context) 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/src/main/java/pseudoankit/droid/app_shortcuts/TaskyShortCutManager.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.app_shortcuts 2 | 3 | import android.content.Context 4 | import androidx.annotation.DrawableRes 5 | import androidx.core.content.pm.ShortcutInfoCompat 6 | import androidx.core.content.pm.ShortcutManagerCompat 7 | import androidx.core.graphics.drawable.IconCompat 8 | import pseudoankit.droid.core.deeplink.DeepLinkUtil 9 | import pseudoankit.droid.core.deeplink.TaskyDeeplink 10 | import pseudoankit.droid.unify.utils.UnifyDrawable 11 | 12 | internal object TaskyShortCutManager { 13 | 14 | fun initialize(context: Context) { 15 | addShortCut( 16 | context = context, 17 | id = "Reminder", 18 | label = "Add Reminder", 19 | icon = UnifyDrawable.ic_notification, 20 | navigationDeepLink = TaskyDeeplink.reminder 21 | ) 22 | 23 | /* addShortCut( 24 | context = context, 25 | id = "Event", 26 | label = "Add Event", 27 | icon = UnifyDrawable.ic_calendar, 28 | navigationDeepLink = TaskyDeeplink.reminder 29 | ) 30 | 31 | addShortCut( 32 | context = context, 33 | id = "Task", 34 | label = "Add Task", 35 | icon = UnifyDrawable.ic_task, 36 | navigationDeepLink = TaskyDeeplink.reminder 37 | )*/ 38 | } 39 | 40 | private fun addShortCut( 41 | context: Context, 42 | id: String, 43 | label: String, 44 | longLabel: String = "", 45 | @DrawableRes icon: Int, 46 | navigationDeepLink: String 47 | ) { 48 | val shortcut = ShortcutInfoCompat.Builder(context, id) 49 | .setShortLabel(label) 50 | .setLongLabel(longLabel) 51 | .setIcon(IconCompat.createWithResource(context, icon)) 52 | .setIntent(DeepLinkUtil.createDeeplinkIntent(navigationDeepLink)) 53 | .build() 54 | 55 | ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) 56 | } 57 | } -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/src/main/java/pseudoankit/droid/app_widgets/agenda_items/AgendaItemAppWidgetReceiver.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.app_widgets.agenda_items 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.content.ComponentName 5 | import android.content.Context 6 | import android.content.Intent 7 | import androidx.glance.appwidget.GlanceAppWidget 8 | import androidx.glance.appwidget.GlanceAppWidgetReceiver 9 | 10 | class AgendaItemAppWidgetReceiver : GlanceAppWidgetReceiver() { 11 | 12 | override val glanceAppWidget: GlanceAppWidget 13 | get() = AgendaItemsAppWidget 14 | 15 | override fun onReceive(context: Context, intent: Intent) { 16 | super.onReceive(context, intent) 17 | 18 | val appWidgetManager = AppWidgetManager.getInstance(context) 19 | val componentName = 20 | ComponentName(context.packageName, checkNotNull(javaClass.canonicalName)) 21 | onUpdate( 22 | context, 23 | appWidgetManager, 24 | appWidgetManager.getAppWidgetIds(componentName), 25 | ) 26 | } 27 | 28 | companion object { 29 | fun sendAppsUpdatedBroadcast(context: Context) { 30 | context.sendBroadcast( 31 | Intent(context, AgendaItemAppWidgetReceiver::class.java) 32 | ) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/src/main/java/pseudoankit/droid/app_widgets/util/WidgetDeeplinkProvider.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.app_widgets.util 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 4 | 5 | interface WidgetDeeplinkProvider { 6 | fun agendaDetailRoute(action: AgendaTypes): String 7 | } 8 | -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/src/main/java/pseudoankit/droid/di/WidgetsNShortcutsModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.di 2 | 3 | import kotlinx.coroutines.flow.MutableSharedFlow 4 | import org.koin.dsl.module 5 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlow 6 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlowNamed 7 | 8 | object WidgetsNShortcutsModule { 9 | operator fun invoke() = module { 10 | single(UpdateAppWidgetFlowNamed) { MutableSharedFlow() } 11 | } 12 | } -------------------------------------------------------------------------------- /core/app-shortcuts-n-widgets/src/main/res/xml/tasky_widget_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | -------------------------------------------------------------------------------- /core/core-ui/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/core-ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeCore) 4 | } 5 | 6 | android { 7 | namespace = "pseudoankit.droid.coreui" 8 | } 9 | 10 | dependencies { 11 | implementation(projects.core.designSystem) 12 | implementation(libs.compose.destinations) 13 | implementation(libs.compose.orbit.mvi) 14 | } -------------------------------------------------------------------------------- /core/core-ui/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/deeplink/DeepLinkReciever.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.deeplink 2 | 3 | import android.net.Uri 4 | import androidx.navigation.NavController 5 | import pseudoankit.droid.core.logger.logError 6 | 7 | fun NavController.navigateViaDeepLink(deepLink: String) { 8 | navigateViaDeepLink(Uri.parse(deepLink)) 9 | } 10 | 11 | fun NavController.navigateViaDeepLink(uri: Uri?) = try { 12 | navigate(uri!!) 13 | } catch (e: IllegalArgumentException) { 14 | e.printStackTrace() 15 | logError("deeplink [${uri}] failed with error msg ${e.message}") 16 | } 17 | -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/destination/TaskyDestinationStyle.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.destination 2 | 3 | import androidx.compose.ui.window.DialogProperties 4 | import com.ramcosta.composedestinations.spec.DestinationStyle 5 | 6 | object TaskyDestinationStyle { 7 | 8 | object Dialog : DestinationStyle.Dialog { 9 | override val properties: DialogProperties 10 | get() = DialogProperties( 11 | usePlatformDefaultWidth = false 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/koin/ComposeKoinModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.koin 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import pseudoankit.droid.core.koin.BaseKoinModule 6 | 7 | /** 8 | * method to manage loading / unloading of koin modules 9 | * @param[module] requires instance of [BaseKoinModule] to load and unload modules 10 | */ 11 | @Composable 12 | fun BaseKoinModule.load(content: @Composable () -> Unit) { 13 | loadModules() 14 | DisposableEffect(Unit) { 15 | onDispose { 16 | unloadModules() 17 | } 18 | } 19 | content() 20 | } 21 | 22 | /** 23 | * method to manage loading / unloading of koin modules 24 | * @param[module] requires instance of [BaseKoinModule] to load and unload modules 25 | */ 26 | @Composable 27 | fun BaseKoinModule.load() { 28 | loadModules() 29 | DisposableEffect(Unit) { 30 | onDispose { 31 | unloadModules() 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/model/TextFieldUiConfig.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.model 2 | 3 | import pseudoankit.droid.core.util.TextResource 4 | 5 | data class TextFieldUiConfig( 6 | val value: String, 7 | val errorMessage: TextResource? 8 | ) -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/util/extension/ContextExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.util.extension 2 | 3 | import android.app.Activity 4 | import android.content.ActivityNotFoundException 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.net.Uri 8 | import android.provider.Settings 9 | import android.widget.Toast 10 | import pseudoankit.droid.core.deeplink.DeepLinkUtil 11 | import pseudoankit.droid.core.util.TextResource 12 | import pseudoankit.droid.core.util.TextResource.Companion.asString 13 | 14 | fun Context.openDeeplink(deeplink: String) { 15 | startActivity(DeepLinkUtil.createDeeplinkIntent(deeplink)) 16 | } 17 | 18 | fun Context.finish() { 19 | (this as? Activity)?.finish() 20 | } 21 | 22 | fun Context.navigateToSettings() = try { 23 | startActivity( 24 | Intent().apply { 25 | action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS 26 | data = Uri.fromParts("package", packageName, null) 27 | } 28 | ) 29 | } catch (e: ActivityNotFoundException) { 30 | showToast("Oops! Something went wrong, Long press app icon -> click on info -> grant permission") 31 | e.printStackTrace() 32 | } 33 | 34 | fun Context.showToast(message: String) { 35 | Toast.makeText(this, message, Toast.LENGTH_SHORT).show() 36 | } 37 | 38 | fun Context.showToast(message: TextResource) { 39 | showToast(message.asString(this)) 40 | } 41 | 42 | fun Context.toastNotImplemented() { 43 | showToast("This feature is not implemented yet!") 44 | } -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/util/extension/ModifierExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.util.extension 2 | 3 | import androidx.compose.ui.Modifier 4 | import androidx.compose.ui.composed 5 | import androidx.compose.ui.layout.layout 6 | import androidx.compose.ui.unit.Dp 7 | import androidx.compose.ui.unit.dp 8 | 9 | fun Modifier.placeWhileIgnoringHorizontalParentPadding(paddingToIgnore: Dp = 16.dp) = composed { 10 | layout { measurable, constraints -> 11 | val placeable = measurable.measure( 12 | constraints.copy( 13 | maxWidth = paddingToIgnore.roundToPx() + constraints.maxWidth + paddingToIgnore.roundToPx(), //add the end padding 16.dp 14 | ) 15 | ) 16 | layout(placeable.width, placeable.height) { 17 | placeable.place(0, 0) 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/util/extension/NavControllerExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.util.extension 2 | 3 | import androidx.navigation.NavController 4 | import androidx.navigation.NavOptionsBuilder 5 | 6 | fun NavController.clearStack(navOptionsBuilder: NavOptionsBuilder) { 7 | navOptionsBuilder.popUpTo(this.graph.id) { 8 | inclusive = true 9 | } 10 | } 11 | 12 | fun NavController.clearStack() { 13 | popBackStack(this.graph.id, true) 14 | } -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/util/extension/StateExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.util.extension 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.MutableState 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | 8 | @Composable 9 | fun rememberMutableStateOf(value: T, vararg keys: Any): MutableState = remember(*keys) { 10 | mutableStateOf(value) 11 | } 12 | -------------------------------------------------------------------------------- /core/core-ui/src/main/java/pseudoankit/droid/coreui/util/extension/TextResourceExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.coreui.util.extension 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.platform.LocalContext 5 | import pseudoankit.droid.core.util.TextResource 6 | import pseudoankit.droid.core.util.TextResource.Companion.asString 7 | 8 | @Composable 9 | fun TextResource?.asString(): String { 10 | return this.asString(LocalContext.current) 11 | } -------------------------------------------------------------------------------- /core/core-ui/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /core/core-ui/src/main/res/drawable/ic_splash.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /core/core-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/core/core-ui/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /core/core/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.AndroidLibrary) 3 | id(Plugins.Core) 4 | } 5 | 6 | android { 7 | namespace = "pseudoankit.droid.core" 8 | } 9 | -------------------------------------------------------------------------------- /core/core/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/coroutine/CoroutineUtil.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.coroutine 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.Job 6 | import kotlinx.coroutines.launch 7 | import kotlin.coroutines.CoroutineContext 8 | 9 | fun launchCoroutine( 10 | dispatcher: CoroutineContext = Dispatchers.IO, 11 | block: suspend CoroutineScope.() -> Unit 12 | ): Job { 13 | return CoroutineScope(dispatcher).launch { 14 | block() 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/deeplink/DeepLinkUtil.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.deeplink 2 | 3 | import android.content.Intent 4 | import android.net.Uri 5 | 6 | object DeepLinkUtil { 7 | fun createDeeplinkIntent(deeplinkUri: String): Intent { 8 | return Intent(Intent.ACTION_VIEW).apply { 9 | data = Uri.parse(deeplinkUri) 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/deeplink/TaskyDeeplink.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.deeplink 2 | 3 | /** 4 | * Global deeplink file 5 | * Preferred way to create deeplink is via [pseudoankit.droid.navigation.deeplink.DeepLinkProvider] 6 | */ 7 | object TaskyDeeplink { 8 | object Path { 9 | object Reminder { 10 | const val action = "{action}" 11 | } 12 | } 13 | 14 | const val login = "tasky://login" 15 | const val registration = "tasky://registration" 16 | const val home = "tasky://home" 17 | const val reminder = "tasky://reminder/${Path.Reminder.action}" 18 | const val agendaSelection = "tasky://agendaSelection" 19 | const val profile = "tasky://profile" 20 | } 21 | -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/di/CoreModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.di 2 | 3 | import org.koin.dsl.module 4 | import pseudoankit.droid.core.dispatcher.DispatcherProvider 5 | import pseudoankit.droid.core.dispatcher.DispatcherProviderImpl 6 | 7 | object CoreModule { 8 | 9 | operator fun invoke() = module { 10 | single { DispatcherProviderImpl() } 11 | } 12 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/dispatcher/DispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.dispatcher 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | 6 | interface DispatcherProvider { 7 | val io: CoroutineDispatcher 8 | val default: CoroutineDispatcher 9 | suspend fun switchToIo(block: suspend CoroutineScope.() -> T): T 10 | suspend fun switchToDefault(block: suspend CoroutineScope.() -> T): T 11 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/dispatcher/DispatcherProviderImpl.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.dispatcher 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | 8 | internal class DispatcherProviderImpl : DispatcherProvider { 9 | 10 | override val io: CoroutineDispatcher 11 | get() = Dispatchers.IO 12 | 13 | override val default: CoroutineDispatcher 14 | get() = Dispatchers.Default 15 | 16 | override suspend fun switchToDefault(block: suspend CoroutineScope.() -> T): T { 17 | return withContext(context = default, block = block) 18 | } 19 | 20 | override suspend fun switchToIo(block: suspend CoroutineScope.() -> T): T { 21 | return withContext(context = io, block = block) 22 | } 23 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/koin/BaseKoinModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.koin 2 | 3 | import org.koin.core.context.loadKoinModules 4 | import org.koin.core.context.unloadKoinModules 5 | import org.koin.core.module.Module 6 | 7 | abstract class BaseKoinModule { 8 | private var isLoaded = false 9 | 10 | abstract val modules: Module 11 | 12 | fun loadModules() { 13 | if (isLoaded) return 14 | 15 | isLoaded = true 16 | loadKoinModules(modules) 17 | } 18 | 19 | fun unloadModules() { 20 | if (isLoaded.not()) return 21 | 22 | isLoaded = false 23 | unloadKoinModules(modules) 24 | } 25 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/logger/TaskyLogger.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.logger 2 | 3 | import android.util.Log 4 | 5 | object TaskyLogger { 6 | 7 | private const val INTERNAL_TAG = "TASKY LOGS" 8 | 9 | fun info(vararg value: Any, tag: String = "") { 10 | Log.i("$INTERNAL_TAG $tag", value.joinToString(", ")) 11 | } 12 | 13 | fun error(vararg value: Any, tag: String = "") { 14 | Log.e("$INTERNAL_TAG $tag", value.joinToString(", ")) 15 | } 16 | } 17 | 18 | fun logInfo(vararg value: Any, tag: String = "") { 19 | TaskyLogger.info(*value, tag) 20 | } 21 | 22 | fun logError(vararg value: Any, tag: String = "") { 23 | TaskyLogger.error(*value, tag) 24 | } 25 | -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/model/TaskyDate.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.model 2 | 3 | import android.os.Parcelable 4 | import androidx.compose.runtime.Stable 5 | import kotlinx.parcelize.Parcelize 6 | import java.time.LocalDate 7 | 8 | @Parcelize 9 | @Stable 10 | @JvmInline 11 | value class TaskyDate(val value: LocalDate) : Parcelable { 12 | 13 | companion object { 14 | val Today: TaskyDate get() = TaskyDate(LocalDate.now()) 15 | } 16 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/model/TaskyTime.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.model 2 | 3 | import android.os.Parcelable 4 | import androidx.compose.runtime.Stable 5 | import kotlinx.parcelize.Parcelize 6 | import java.time.LocalTime 7 | 8 | @Parcelize 9 | @Stable 10 | @JvmInline 11 | value class TaskyTime(val value: LocalTime) : Parcelable { 12 | 13 | companion object { 14 | val Now = TaskyTime(LocalTime.now()) 15 | } 16 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/testtag/AgendaTestTag.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.testtag 2 | 3 | object AgendaTestTag { 4 | const val reminder = "reminder" 5 | const val task = "task" 6 | const val event = "event" 7 | const val save = "save" 8 | } 9 | 10 | object ReminderTestTag { 11 | const val edtRemindMe = "edt_remind_me" 12 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/testtag/AuthTestTag.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.testtag 2 | 3 | object AuthTestTag { 4 | const val email = "email" 5 | const val password = "password" 6 | const val loginBtn = "login_btn" 7 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/testtag/HomeTestTag.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.testtag 2 | 3 | object HomeTestTag { 4 | const val monthDatesHorizontalList = "month_date_items" 5 | const val agendaItemList = "agenda_items" 6 | const val fab = "fab" 7 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/JsonSerializer.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | val defaultJsonSerializer = Json { 6 | isLenient = true 7 | explicitNulls = true 8 | ignoreUnknownKeys = true 9 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/TaskyResult.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util 2 | 3 | sealed interface TaskyResult { 4 | data class Success(val data: T) : TaskyResult 5 | data class Error(val error: TextResource) : TaskyResult 6 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/TextResource.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Stable 5 | 6 | @Stable 7 | sealed interface TextResource { 8 | 9 | @Stable 10 | class ResourceString(internal val stringId: Int, internal vararg val params: Any) : TextResource 11 | 12 | @Stable 13 | @JvmInline 14 | value class NormalString(internal val text: String) : TextResource 15 | 16 | companion object { 17 | fun TextResource?.asString(context: Context): String { 18 | return when (this) { 19 | null -> "" 20 | is ResourceString -> context.resources.getString(stringId, *params) 21 | is NormalString -> text 22 | } 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/datetime/DateUtils.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.datetime 2 | 3 | import kotlinx.collections.immutable.ImmutableList 4 | import pseudoankit.droid.core.model.TaskyDate 5 | import pseudoankit.droid.core.util.extension.mapToImmutableList 6 | import java.time.LocalDate 7 | import java.time.Month 8 | 9 | object DateUtils { 10 | private val dateRangeOfMonthCache = mutableMapOf>() 11 | 12 | /** 13 | * @return[List] list of days which falls in the same month passed in the param 14 | * @param[date] date for which range needed 15 | */ 16 | fun getDateRangeForMonth(date: TaskyDate): ImmutableList { 17 | return dateRangeOfMonthCache.getOrPut(date.value.month) { 18 | (1..date.value.lengthOfMonth()).map { 19 | LocalDate.of(date.value.year, date.value.month, it) 20 | } 21 | }.mapToImmutableList { 22 | // wrapping with TaskyDate and mapping the year same passed by client 23 | TaskyDate( 24 | value = LocalDate.of(date.value.year, it.month, it.dayOfMonth) 25 | ) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/datetime/TimeUtils.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.datetime 2 | 3 | import pseudoankit.droid.core.model.TaskyTime 4 | import java.time.LocalTime 5 | 6 | object TimeUtils { 7 | val TaskyTime?.orNow get() = this ?: TaskyTime(LocalTime.now()) 8 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/extension/CollectionExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.extension 2 | 3 | import kotlinx.collections.immutable.ImmutableList 4 | import kotlinx.collections.immutable.toImmutableList 5 | 6 | inline fun Iterable.mapToImmutableList(transform: (T) -> R): ImmutableList { 7 | return map(transform).toImmutableList() 8 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/extension/DateTimeExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.extension 2 | 3 | import pseudoankit.droid.core.model.TaskyDate 4 | import pseudoankit.droid.core.model.TaskyTime 5 | import java.time.LocalDate 6 | import java.time.LocalTime 7 | import java.time.format.DateTimeFormatter 8 | 9 | val LocalDate?.orToday: LocalDate get() = this ?: LocalDate.now() 10 | 11 | fun TaskyDate?.parseToString(pattern: String): String? = this?.value.parseToString(pattern) 12 | 13 | fun LocalDate?.parseToString(pattern: String): String? = this?.run { 14 | format(DateTimeFormatter.ofPattern(pattern)) 15 | } 16 | 17 | /* -------------------------------------- -------------------------------------- */ 18 | 19 | val LocalTime?.orNow: LocalTime get() = this ?: LocalTime.now() 20 | 21 | fun TaskyTime?.parseToString(pattern: String = "hh:mm a"): String? = 22 | this?.value.parseToString(pattern) 23 | 24 | fun LocalTime?.parseToString(pattern: String = "hh:mm a"): String? = this?.run { 25 | format(DateTimeFormatter.ofPattern(pattern)) 26 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/extension/IntentExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.extension 2 | 3 | import android.os.Build 4 | import android.os.Bundle 5 | import android.os.Parcelable 6 | 7 | inline fun Bundle.getParcelableData(key: String): T? { 8 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 9 | getParcelable(key, T::class.java) 10 | } else { 11 | getParcelable(key) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/extension/LazyInitializerExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.extension 2 | 3 | fun nonThreadSafeLazy(initializer: () -> T) = lazy(LazyThreadSafetyMode.NONE, initializer) -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/extension/PrimitiveExtension.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.extension 2 | 3 | val Boolean?.orFalse get() = this ?: false 4 | val Boolean?.orTrue get() = this ?: true 5 | 6 | val Int?.orZero get() = this ?: 0 -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/extension/SafeCall.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.extension 2 | 3 | import pseudoankit.droid.core.R 4 | import pseudoankit.droid.core.logger.TaskyLogger 5 | import pseudoankit.droid.core.util.TaskyResult 6 | import pseudoankit.droid.core.util.TextResource 7 | import java.net.SocketTimeoutException 8 | 9 | /** 10 | * helper function to execute a code block and handle exception 11 | * @param[block] execute your code 12 | * @param[onError] executed when any exception occurs with [TextResource] as error message 13 | * @return[T] type of result to be executed 14 | */ 15 | inline fun safeCall(block: () -> T, onError: (TextResource) -> T): T = try { 16 | block() 17 | } catch (exception: Exception) { 18 | TaskyLogger.info("SafeCall exception", exception.message.orEmpty()) 19 | val errorMessage = when (exception) { 20 | is SocketTimeoutException -> TextResource.ResourceString(R.string.error_no_internet) 21 | else -> { 22 | when { 23 | exception.message.isNullOrBlank().not() -> 24 | TextResource.NormalString(exception.message.orEmpty()) 25 | else -> TextResource.ResourceString(R.string.something_went_wrong) 26 | } 27 | } 28 | } 29 | onError(errorMessage) 30 | } 31 | 32 | /** 33 | * helper function to execute a code block and handle exception 34 | * @param[block] execute your code 35 | * @return[TextResource] 36 | */ 37 | inline fun safeCall(block: () -> TaskyResult): TaskyResult = safeCall( 38 | block = block, 39 | onError = { 40 | TaskyResult.Error(it) 41 | } 42 | ) -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/util/validator/Validator.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.util.validator 2 | 3 | import pseudoankit.droid.core.util.TextResource 4 | 5 | object Validator { 6 | private const val EMAIL_PATTERN = "^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}\$" 7 | 8 | fun validateName(name: String?): TextResource? { 9 | if (name.isNullOrBlank()) return TextResource.NormalString("Name cannot be empty") 10 | return null 11 | } 12 | 13 | /** 14 | * email validator returns null if [email] is valid else will return error message 15 | */ 16 | fun validateEmail(email: String?): TextResource? { 17 | if (email.isNullOrBlank()) return TextResource.NormalString("Email cannot be empty") 18 | 19 | val isValid = email.matches(Regex(pattern = EMAIL_PATTERN)) 20 | if (isValid.not()) return TextResource.NormalString("You have entered an invalid email address") 21 | 22 | return null 23 | } 24 | 25 | /** 26 | * password validator returns null if email is valid else will return error message 27 | */ 28 | fun validatePassword( 29 | password: String?, 30 | emptyErrorMessage: TextResource = TextResource.NormalString("Password cannot be empty"), 31 | invalidLengthErrorMessage: TextResource = TextResource.NormalString("Password cannot be less than 6 digits"), 32 | ): TextResource? { 33 | if (password.isNullOrBlank()) return emptyErrorMessage 34 | if (password.contains(' ')) return TextResource.NormalString("Password cannot contain whitespaces") 35 | if (password.length < 6) return invalidLengthErrorMessage 36 | 37 | return null 38 | } 39 | } -------------------------------------------------------------------------------- /core/core/src/main/java/pseudoankit/droid/core/widget/UpdateAppWidgetFlow.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.core.widget 2 | 3 | import kotlinx.coroutines.flow.MutableSharedFlow 4 | import org.koin.core.qualifier.named 5 | import org.koin.core.scope.Scope 6 | 7 | /* 8 | * flow to update app widgets in home screen 9 | */ 10 | 11 | typealias UpdateAppWidgetFlow = MutableSharedFlow 12 | 13 | val UpdateAppWidgetFlowNamed = named("widget") 14 | 15 | val Scope.UpdateAppWidgetFlowInstance get() = get(UpdateAppWidgetFlowNamed) -------------------------------------------------------------------------------- /core/core/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | No Internet 4 | Something went wrong 5 | -------------------------------------------------------------------------------- /core/database-manager/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/database-manager/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.AndroidLibrary) 3 | id(Plugins.Core) 4 | id(Plugins.RoomDatabase) 5 | } 6 | 7 | android { 8 | namespace = "pseudoankit.droid.dbmanager" 9 | } 10 | 11 | dependencies { 12 | implementation(projects.core.agendaManger) 13 | implementation(projects.core.core) 14 | } -------------------------------------------------------------------------------- /core/database-manager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/database-manager/src/main/java/pseudoankit/droid/dbmanager/TaskyDataBase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.dbmanager 2 | 3 | import android.content.Context 4 | import androidx.room.Database 5 | import androidx.room.Room 6 | import androidx.room.RoomDatabase 7 | import androidx.room.TypeConverters 8 | import pseudoankit.droid.agendamanger.data.local.dao.ReminderDao 9 | import pseudoankit.droid.agendamanger.data.local.entity.ReminderEntity 10 | import pseudoankit.droid.dbmanager.typeconvertor.DateTimeTypeConvertor 11 | 12 | @Database( 13 | entities = [ReminderEntity::class], 14 | version = 1 15 | ) 16 | @TypeConverters( 17 | DateTimeTypeConvertor::class 18 | ) 19 | abstract class TaskyDataBase : RoomDatabase() { 20 | 21 | abstract val reminderDao: ReminderDao 22 | 23 | companion object { 24 | 25 | @Volatile 26 | private var instance: TaskyDataBase? = null 27 | private val LOCK = Any() 28 | 29 | operator fun invoke(context: Context) = instance ?: synchronized(LOCK) { 30 | instance ?: buildDatabase(context).also { 31 | instance = it 32 | } 33 | } 34 | 35 | private fun buildDatabase(context: Context) = 36 | Room.databaseBuilder( 37 | context.applicationContext, 38 | TaskyDataBase::class.java, 39 | "tasky_manager" 40 | ).build() 41 | } 42 | } -------------------------------------------------------------------------------- /core/database-manager/src/main/java/pseudoankit/droid/dbmanager/di/DataBaseModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.dbmanager.di 2 | 3 | import org.koin.android.ext.koin.androidApplication 4 | import org.koin.dsl.module 5 | import pseudoankit.droid.dbmanager.TaskyDataBase 6 | 7 | object DataBaseModule { 8 | 9 | operator fun invoke() = module { 10 | single { TaskyDataBase(androidApplication()) } 11 | 12 | factory { get().reminderDao } 13 | } 14 | } -------------------------------------------------------------------------------- /core/database-manager/src/main/java/pseudoankit/droid/dbmanager/typeconvertor/DateTimeTypeConvertor.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.dbmanager.typeconvertor 2 | 3 | import androidx.room.TypeConverter 4 | import kotlinx.serialization.decodeFromString 5 | import kotlinx.serialization.encodeToString 6 | import pseudoankit.droid.core.util.defaultJsonSerializer 7 | import java.time.LocalDate 8 | import java.time.LocalTime 9 | 10 | class DateTimeTypeConvertor { 11 | 12 | @kotlinx.serialization.Serializable 13 | private data class ParseDate(val year: Int, val month: Int, val day: Int) 14 | 15 | @kotlinx.serialization.Serializable 16 | private data class ParseTime(val hour: Int, val minute: Int) 17 | 18 | 19 | @TypeConverter 20 | fun encode(date: LocalDate): String { 21 | return defaultJsonSerializer.encodeToString( 22 | ParseDate( 23 | date.year, 24 | date.month.value, 25 | date.dayOfMonth 26 | ) 27 | ) 28 | } 29 | 30 | @TypeConverter 31 | fun decodeToDate(date: String): LocalDate { 32 | val parsed = defaultJsonSerializer.decodeFromString(date) 33 | return LocalDate.of(parsed.year, parsed.month, parsed.day) 34 | } 35 | 36 | @TypeConverter 37 | fun encode(time: LocalTime): String { 38 | return defaultJsonSerializer.encodeToString(ParseTime(time.hour, time.minute)) 39 | } 40 | 41 | @TypeConverter 42 | fun decodeToTime(time: String): LocalTime { 43 | val parse = defaultJsonSerializer.decodeFromString(time) 44 | return LocalTime.of(parse.hour, parse.minute) 45 | } 46 | } -------------------------------------------------------------------------------- /core/design-system/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/design-system/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.AndroidLibrary) 3 | id(Plugins.Core) 4 | id(Plugins.ComposeCore) 5 | } 6 | 7 | android { 8 | namespace = "pseudoankit.droid.unify" 9 | } 10 | 11 | dependencies { 12 | implementation(libs.compose.fontawesome) 13 | implementation(libs.compose.datepicker) 14 | implementation(libs.compose.placeholder) 15 | implementation(libs.compose.swipe) 16 | } 17 | -------------------------------------------------------------------------------- /core/design-system/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /core/design-system/src/debug/java/pseudoankit/droid/unify/UnifyActivity.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import androidx.activity.ComponentActivity 7 | import androidx.activity.compose.setContent 8 | import androidx.compose.foundation.layout.Arrangement 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.lazy.LazyColumn 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | fun Context.navigateToUnifyDemo() { 15 | Intent(this, UnifyActivity::class.java).apply { 16 | startActivity(this) 17 | } 18 | } 19 | 20 | class UnifyActivity : ComponentActivity() { 21 | 22 | override fun onCreate(savedInstanceState: Bundle?) { 23 | super.onCreate(savedInstanceState) 24 | setContent { 25 | 26 | LazyColumn( 27 | modifier = Modifier.fillMaxSize(), 28 | verticalArrangement = Arrangement.spacedBy(16.dp) 29 | ) { 30 | item { 31 | } 32 | } 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /core/design-system/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/button/UnifyButtonMapper.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.button 2 | 3 | import androidx.compose.material3.ButtonDefaults 4 | import androidx.compose.runtime.Composable 5 | import pseudoankit.droid.unify.token.UnifyColors 6 | 7 | internal object UnifyButtonMapper { 8 | 9 | val UnifyButtonConfig.State.isDisabled get() = this == UnifyButtonConfig.State.Disabled 10 | val UnifyButtonConfig.State.isLoading get() = this == UnifyButtonConfig.State.Loading 11 | val UnifyButtonConfig.State.isEnabled get() = this == UnifyButtonConfig.State.Enabled 12 | 13 | @Composable 14 | fun UnifyButtonConfig.State.buttonColors() = when (this) { 15 | UnifyButtonConfig.State.Loading -> ButtonDefaults.buttonColors( 16 | containerColor = UnifyColors.Black, 17 | contentColor = UnifyColors.White 18 | ) 19 | UnifyButtonConfig.State.Enabled -> ButtonDefaults.buttonColors( 20 | containerColor = UnifyColors.Black, 21 | contentColor = UnifyColors.White 22 | ) 23 | UnifyButtonConfig.State.Disabled -> ButtonDefaults.buttonColors( 24 | containerColor = UnifyColors.Gray400, 25 | contentColor = UnifyColors.Gray800 26 | ) 27 | } 28 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/card/UnifyCard.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.card 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.material3.Surface 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.unit.Dp 10 | import androidx.compose.ui.unit.dp 11 | import pseudoankit.droid.unify.token.UnifyColors 12 | import pseudoankit.droid.unify.token.UnifyDimens 13 | 14 | data class UnifyCardConfig( 15 | val radius: Dp = UnifyDimens.Radius.Medium, 16 | val color: Color = UnifyColors.White, 17 | val modifier: Modifier = Modifier, 18 | val elevation: Dp = 2.dp 19 | ) 20 | 21 | @Composable 22 | fun UnifyCard(config: UnifyCardConfig = UnifyCardConfig(), content: @Composable () -> Unit) { 23 | Surface( 24 | modifier = config.modifier.then(Modifier.padding(UnifyDimens.Dp_1)), 25 | shape = RoundedCornerShape(config.radius), 26 | color = config.color, 27 | content = content, 28 | shadowElevation = config.elevation, 29 | tonalElevation = config.elevation, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/dialog/UnifyDialog.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.dialog 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.remember 6 | import androidx.compose.ui.text.TextStyle 7 | import com.vanpra.composematerialdialogs.MaterialDialog 8 | import com.vanpra.composematerialdialogs.MaterialDialogScope 9 | import com.vanpra.composematerialdialogs.rememberMaterialDialogState 10 | import pseudoankit.droid.unify.token.UnifyColors 11 | import pseudoankit.droid.unify.token.UnifyDimens 12 | 13 | @Composable 14 | fun UnifyDialog( 15 | state: UnifyDialogState, 16 | showActionButton: Boolean = true, 17 | content: @Composable MaterialDialogScope.() -> Unit 18 | ) { 19 | val materialPickerState = rememberMaterialDialogState() 20 | when (state.showing) { 21 | true -> materialPickerState.show() 22 | false -> materialPickerState.hide() 23 | } 24 | 25 | val onActionButtonClick = remember { 26 | { 27 | state.hide() 28 | } 29 | } 30 | val actionButtonTextStyle = remember { 31 | TextStyle.Default.copy(color = UnifyColors.Black) 32 | } 33 | 34 | MaterialDialog( 35 | dialogState = materialPickerState, 36 | buttons = { 37 | if (showActionButton.not()) return@MaterialDialog 38 | positiveButton( 39 | "OK", 40 | onClick = onActionButtonClick, 41 | textStyle = actionButtonTextStyle 42 | ) 43 | negativeButton( 44 | "Cancel", 45 | onClick = onActionButtonClick, 46 | textStyle = actionButtonTextStyle 47 | ) 48 | }, 49 | shape = RoundedCornerShape(UnifyDimens.Radius.Large), 50 | autoDismiss = true, 51 | onCloseRequest = { 52 | state.hide() 53 | }, 54 | content = content 55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/dialog/UnifyDialogState.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.dialog 2 | 3 | import androidx.compose.runtime.* 4 | 5 | @Composable 6 | fun rememberUnifyDialogState(initialValue: Boolean = false): UnifyDialogState { 7 | return remember { 8 | UnifyDialogState(initialValue = initialValue) 9 | } 10 | } 11 | 12 | class UnifyDialogState internal constructor(initialValue: Boolean = false) { 13 | internal var showing by mutableStateOf(initialValue) 14 | 15 | fun show() { 16 | showing = true 17 | } 18 | 19 | fun hide() { 20 | showing = false 21 | } 22 | 23 | fun toggle() { 24 | showing = showing.not() 25 | } 26 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/dialog/datepicker/UnifyDatePicker.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.dialog.datepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Immutable 5 | import com.vanpra.composematerialdialogs.datetime.date.datepicker 6 | import pseudoankit.droid.unify.component.dialog.UnifyDialog 7 | import pseudoankit.droid.unify.component.dialog.UnifyDialogState 8 | import pseudoankit.droid.unify.component.dialog.rememberUnifyDialogState 9 | import java.time.LocalDate 10 | 11 | /** 12 | * create date picker dialog 13 | * @param[initialDate] date to be selected when picker is opened initially 14 | */ 15 | @Immutable 16 | data class UnifyDatePickerConfig( 17 | val initialDate: LocalDate = LocalDate.now() 18 | ) 19 | 20 | /** 21 | * create date picker dialog 22 | * @param[datePickerState] pass the state of date picker, create state instance by [rememberUnifyDialogState()] 23 | * @param[onDateChanged] callback when positive button is clicked 24 | * @param config configs for date picker 25 | */ 26 | @Composable 27 | fun UnifyDatePicker( 28 | config: UnifyDatePickerConfig, 29 | datePickerState: UnifyDialogState, 30 | onDateChanged: (LocalDate) -> Unit, 31 | ) = with(config) { 32 | UnifyDialog(state = datePickerState) { 33 | datepicker( 34 | initialDate = initialDate, 35 | onDateChange = onDateChanged, 36 | colors = UnifyDatePickerToken.colors 37 | ) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/dialog/datepicker/UnifyDatePickerToken.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.dialog.datepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.State 5 | import androidx.compose.ui.graphics.Color 6 | import com.vanpra.composematerialdialogs.datetime.date.DatePickerColors 7 | import pseudoankit.droid.unify.token.UnifyColors 8 | import pseudoankit.droid.unify.utils.internal.rememberMutableState 9 | 10 | internal object UnifyDatePickerToken { 11 | 12 | val colors = object : DatePickerColors { 13 | override val calendarHeaderTextColor: Color 14 | get() = UnifyColors.Black 15 | override val headerBackgroundColor: Color 16 | get() = UnifyColors.Black 17 | override val headerTextColor: Color 18 | get() = UnifyColors.White 19 | 20 | @Composable 21 | override fun dateBackgroundColor(active: Boolean): State { 22 | return rememberMutableState( 23 | value = if (active) UnifyColors.Black else UnifyColors.White, 24 | active, 25 | ) 26 | } 27 | 28 | @Composable 29 | override fun dateTextColor(active: Boolean): State { 30 | return rememberMutableState( 31 | value = if (active) UnifyColors.White else UnifyColors.Black, 32 | active, 33 | ) 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/dialog/timepicker/UnifyTimePicker.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.dialog.timepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import com.vanpra.composematerialdialogs.datetime.time.timepicker 6 | import pseudoankit.droid.unify.component.dialog.UnifyDialog 7 | import pseudoankit.droid.unify.component.dialog.UnifyDialogState 8 | import java.time.LocalTime 9 | 10 | /** 11 | * create date picker dialog 12 | * @param[timePickerState] pass the state of date picker, create state instance by [rememberUnifyDialogState()] 13 | * @param[config] config param for time picker 14 | * @param[onTimeChanged] callback when positive button is clicked 15 | */ 16 | 17 | @Composable 18 | fun UnifyTimePicker( 19 | config: UnifyTimePickerConfig, 20 | timePickerState: UnifyDialogState, 21 | onTimeChanged: (LocalTime) -> Unit 22 | ) = with(config) { 23 | UnifyDialog( 24 | state = timePickerState 25 | ) { 26 | timepicker( 27 | initialTime = initialTime, 28 | onTimeChange = onTimeChanged, 29 | colors = UnifyTimePickerToken.colors 30 | ) 31 | } 32 | } 33 | 34 | /** 35 | * @param[initialTime] date to be selected when picker is opened initially 36 | */ 37 | @Stable 38 | data class UnifyTimePickerConfig( 39 | val initialTime: LocalTime = LocalTime.now() 40 | ) 41 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/divider/UnifyDivider.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.divider 2 | 3 | import androidx.compose.material3.Divider 4 | import androidx.compose.material3.DividerDefaults 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 | 11 | @Composable 12 | fun UnifyDivider( 13 | modifier: Modifier = Modifier, 14 | color: Color = DividerDefaults.color, 15 | height: Dp = DividerDefaults.Thickness, 16 | ) { 17 | Divider(modifier = modifier, color = color, thickness = height) 18 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/fab/UnifyFloatingButton.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.fab 2 | 3 | import androidx.compose.foundation.shape.CircleShape 4 | import androidx.compose.material3.FloatingActionButton 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | import pseudoankit.droid.unify.component.icon.UnifyIcon 9 | import pseudoankit.droid.unify.component.icon.UnifyIconConfig 10 | import pseudoankit.droid.unify.token.UnifyColors 11 | 12 | @Composable 13 | fun UnifyFloatingButton( 14 | iconConfig: UnifyIconConfig, 15 | modifier: Modifier = Modifier, 16 | onClick: () -> Unit, 17 | ) { 18 | val updatedIconConfig = remember(iconConfig) { 19 | iconConfig.copy(tint = UnifyColors.White) 20 | } 21 | 22 | FloatingActionButton( 23 | onClick = onClick, 24 | shape = CircleShape, 25 | containerColor = UnifyColors.Black, 26 | modifier = modifier 27 | ) { 28 | UnifyIcon(config = updatedIconConfig) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/icon/UnifyIcon.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.icon 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.size 5 | import androidx.compose.material3.IconButton 6 | import androidx.compose.material3.LocalMinimumTouchTargetEnforcement 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.CompositionLocalProvider 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.unit.Dp 13 | import pseudoankit.droid.unify.token.UnifyDimens 14 | 15 | /** 16 | * Icon composable to display icon 17 | * @param config configurations for icon 18 | */ 19 | @Composable 20 | fun UnifyIcon(config: UnifyIconConfig?) { 21 | when { 22 | config == null -> return 23 | config.onClick != null -> CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides config.provideTouchTargetSpacing) { 24 | IconButton(onClick = config.onClick, modifier = config.modifier) { 25 | UnifyIconInternal(config = config) 26 | } 27 | } 28 | else -> Box(modifier = config.modifier, contentAlignment = Alignment.Center) { 29 | UnifyIconInternal(config = config) 30 | } 31 | } 32 | } 33 | 34 | /** 35 | * @param[icon] icon to show 36 | * @param[size] size of icon 37 | * @param[onClick] consuming onClick will use icon button to provide ripple effect 38 | * @param[provideTouchTargetSpacing] boolean to change extra padding when using icon button 39 | * @param modifier modifier of outer box in which icon is wrapped 40 | */ 41 | data class UnifyIconConfig( 42 | val icon: UnifyIcons, 43 | val modifier: Modifier = Modifier, 44 | val contentDescription: String = "", 45 | val tint: Color = Color.Black, 46 | val size: Dp = UnifyDimens.Dp_24, 47 | val onClick: (() -> Unit)? = null, 48 | val provideTouchTargetSpacing: Boolean = false 49 | ) 50 | 51 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/icon/UnifyIconInternal.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.icon 2 | 3 | import androidx.compose.foundation.layout.size 4 | import androidx.compose.material3.Icon 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.res.painterResource 8 | 9 | @Composable 10 | fun UnifyIconInternal(config: UnifyIconConfig) { 11 | when (config.icon.iconType) { 12 | is UnifyIcons.IconType.FontAwesome -> FontAwesomeIcon(config = config) 13 | is UnifyIcons.IconType.Vector -> VectorIcon(config = config) 14 | is UnifyIcons.IconType.Svg -> SvgIcon(config = config) 15 | } 16 | } 17 | 18 | @Composable 19 | private fun SvgIcon(config: UnifyIconConfig) { 20 | val icon = (config.icon.iconType as? UnifyIcons.IconType.Svg)?.drawableRes ?: return 21 | Icon( 22 | painter = painterResource(id = icon), 23 | contentDescription = "", 24 | modifier = Modifier.size(config.size), 25 | tint = config.tint 26 | ) 27 | } 28 | 29 | @Composable 30 | private fun FontAwesomeIcon(config: UnifyIconConfig) { 31 | val faIcon = (config.icon.iconType as? UnifyIcons.IconType.FontAwesome)?.faIcon ?: return 32 | 33 | lazycoder21.droid.compose.FontAwesomeIcon( 34 | faIcon = faIcon, 35 | size = config.size, 36 | tint = config.tint 37 | ) 38 | } 39 | 40 | @Composable 41 | private fun VectorIcon(config: UnifyIconConfig) { 42 | val imageVector = 43 | (config.icon.iconType as? UnifyIcons.IconType.Vector)?.imageVector ?: return 44 | Icon( 45 | imageVector = imageVector, 46 | contentDescription = config.contentDescription, 47 | modifier = Modifier.size(config.size), 48 | tint = config.tint 49 | ) 50 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/list/UnifyListItem.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.list 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Alignment 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import pseudoankit.droid.unify.component.icon.UnifyIcons 10 | import pseudoankit.droid.unify.component.switch.UnifySwitchConfig 11 | import pseudoankit.droid.unify.token.UnifyColors 12 | 13 | @Composable 14 | fun UnifyListItem(config: UnifyListItemConfig) { 15 | Row( 16 | modifier = config.modifier, 17 | verticalAlignment = Alignment.CenterVertically 18 | ) { 19 | UnifyListComponents.LeadingIcon( 20 | leadingIcon = config.leadingIcon, 21 | tint = config.color 22 | ) 23 | 24 | UnifyListComponents.Label( 25 | label = config.label, 26 | color = config.color, 27 | modifier = Modifier.weight(1f) 28 | ) 29 | 30 | UnifyListComponents.TrailingIcon( 31 | trailingIcon = config.trailingSection, 32 | color = config.color 33 | ) 34 | } 35 | } 36 | 37 | data class UnifyListItemConfig( 38 | val leadingIcon: UnifyIcons? = null, 39 | val label: String, 40 | val trailingSection: TrailingSection? = null, 41 | val modifier: Modifier = Modifier, 42 | val color: Color = UnifyColors.Black 43 | ) { 44 | 45 | sealed interface TrailingSection { 46 | @JvmInline 47 | value class Switch(val value: UnifySwitchConfig) : TrailingSection 48 | 49 | @JvmInline 50 | value class NoAction(val value: UnifyIcons) : TrailingSection 51 | } 52 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/pill/UnifyPill.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.pill 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Row 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.draw.clip 11 | import androidx.compose.ui.unit.dp 12 | import pseudoankit.droid.unify.component.textview.UnifyTextView 13 | import pseudoankit.droid.unify.component.textview.UnifyTextViewConfig 14 | import pseudoankit.droid.unify.token.UnifyColors 15 | import pseudoankit.droid.unify.token.UnifyDimens 16 | import pseudoankit.droid.unify.utils.clickable 17 | 18 | data class UnifyPillConfig( 19 | val label: String, 20 | val modifier: Modifier = Modifier, 21 | ) 22 | 23 | @Composable 24 | fun UnifyPill( 25 | config: UnifyPillConfig, 26 | onClick: (() -> Unit)? = null 27 | ) { 28 | Row( 29 | modifier = config.modifier 30 | .clip(RoundedCornerShape(UnifyDimens.Radius.Large)) 31 | .background(color = UnifyColors.Blue400) 32 | .clickable(onClick = onClick) 33 | .padding(horizontal = 8.dp, vertical = 6.dp), 34 | verticalAlignment = Alignment.CenterVertically 35 | ) { 36 | UnifyTextView(config = UnifyTextViewConfig(text = config.label, color = UnifyColors.White)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/progressbar/UnifyProgressIndicator.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.progressbar 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.material3.CircularProgressIndicator 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 pseudoankit.droid.unify.token.UnifyColors 10 | import pseudoankit.droid.unify.token.UnifyDimens 11 | 12 | @Composable 13 | fun UnifyProgressIndicator( 14 | config: UnifyProgressIndicatorConfig = UnifyProgressIndicatorConfig() 15 | ) { 16 | when (config.type) { 17 | UnifyProgressIndicatorConfig.Type.Circular -> CircularProgressIndicator( 18 | modifier = config.modifier, 19 | color = config.color, 20 | strokeWidth = config.strokeWidth 21 | ) 22 | } 23 | } 24 | 25 | data class UnifyProgressIndicatorConfig( 26 | val type: Type = Type.Circular, 27 | val modifier: Modifier = Modifier, 28 | val color: Color = UnifyColors.Black, 29 | val strokeWidth: Dp = UnifyDimens.Dp_4 30 | ) { 31 | enum class Type { Circular } 32 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/switch/UnifySwitch.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.switch 2 | 3 | import androidx.compose.material3.Switch 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | 7 | data class UnifySwitchConfig( 8 | val checked: Boolean, 9 | val onCheckedChange: (() -> Unit)?, 10 | val modifier: Modifier = Modifier, 11 | ) 12 | 13 | @Composable 14 | fun UnifySwitch(config: UnifySwitchConfig) = with(config) { 15 | Switch( 16 | checked = checked, 17 | onCheckedChange = { 18 | onCheckedChange?.invoke() 19 | }, 20 | enabled = onCheckedChange != null, 21 | colors = UnifySwitchTokens.colors() 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/switch/UnifySwitchTokens.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.switch 2 | 3 | import androidx.compose.material3.SwitchColors 4 | import androidx.compose.material3.SwitchDefaults 5 | import androidx.compose.runtime.Composable 6 | import pseudoankit.droid.unify.token.UnifyColors 7 | 8 | internal object UnifySwitchTokens { 9 | 10 | private val BorderColor = UnifyColors.Transparent 11 | 12 | @Composable 13 | fun colors(): SwitchColors { 14 | return SwitchDefaults.colors( 15 | checkedThumbColor = UnifyColors.Black, 16 | uncheckedThumbColor = UnifyColors.Black, 17 | disabledCheckedThumbColor = UnifyColors.Gray100, 18 | disabledUncheckedThumbColor = UnifyColors.Gray100, 19 | checkedTrackColor = UnifyColors.Black.copy(alpha = .1f), 20 | uncheckedTrackColor = UnifyColors.Gray100.copy(alpha = .9f), 21 | // disabledCheckedTrackColor =, 22 | // disabledUncheckedTrackColor =, 23 | // checkedIconColor = , 24 | // uncheckedIconColor =, 25 | // disabledCheckedIconColor =, 26 | // disabledUncheckedIconColor =, 27 | checkedBorderColor = BorderColor, 28 | uncheckedBorderColor = BorderColor, 29 | disabledCheckedBorderColor = BorderColor, 30 | disabledUncheckedBorderColor = BorderColor, 31 | ) 32 | } 33 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/textfield/UnifyTextFieldDefaults.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.textfield 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.ui.focus.FocusRequester 6 | import androidx.compose.ui.platform.LocalSoftwareKeyboardController 7 | import kotlinx.coroutines.delay 8 | import pseudoankit.droid.unify.component.textview.UnifyTextType 9 | import pseudoankit.droid.unify.component.textview.UnifyTextView 10 | import pseudoankit.droid.unify.component.textview.UnifyTextViewConfig 11 | import pseudoankit.droid.unify.token.UnifyColors 12 | 13 | object UnifyTextFieldDefaults { 14 | 15 | fun placeHolder( 16 | value: String, 17 | textType: UnifyTextType = UnifyTextType.BodyLarge 18 | ) = UnifyTextViewConfig( 19 | text = value, 20 | textType = textType, 21 | color = UnifyColors.Gray800 22 | ) 23 | 24 | /** 25 | * Utility method to request focus and open keyboard 26 | */ 27 | @Composable 28 | fun ManageFocus(focusState: UnifyTextFieldConfig.FocusState, focusRequester: FocusRequester) { 29 | val keyboard = LocalSoftwareKeyboardController.current 30 | 31 | LaunchedEffect(focusState) { 32 | when (focusState) { 33 | UnifyTextFieldConfig.FocusState.Request -> { 34 | focusRequester.requestFocus() 35 | delay(100) 36 | keyboard?.show() 37 | } 38 | UnifyTextFieldConfig.FocusState.Capture -> focusRequester.captureFocus() 39 | UnifyTextFieldConfig.FocusState.Free -> { 40 | focusRequester.freeFocus() 41 | delay(100) 42 | keyboard?.hide() 43 | } 44 | UnifyTextFieldConfig.FocusState.None -> {} 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/textfield/internal/UnifyTextFieldTokens.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.textfield.internal 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import pseudoankit.droid.unify.component.textfield.UnifyTextFieldConfig 5 | import pseudoankit.droid.unify.component.textview.UnifyTextViewConfig 6 | import pseudoankit.droid.unify.token.UnifyColors 7 | import pseudoankit.droid.unify.token.UnifyDimens 8 | 9 | internal object UnifyTextFieldTokens { 10 | object Icon { 11 | val Color = UnifyColors.Gray500 12 | val Size = UnifyDimens.Dp_24 13 | } 14 | 15 | object Background { 16 | fun shape(type: UnifyTextFieldConfig.Type) = when(type) { 17 | UnifyTextFieldConfig.Type.Outlined -> RoundedCornerShape(UnifyDimens.Radius.Small) 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/textview/UnifyTextType.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.textview 2 | 3 | import androidx.compose.ui.text.TextStyle 4 | import androidx.compose.ui.text.font.FontWeight 5 | import androidx.compose.ui.unit.sp 6 | 7 | private val typography by lazy { 8 | androidx.compose.material3.Typography() 9 | } 10 | 11 | enum class UnifyTextType(val textStyle: TextStyle) { 12 | DisplayLarge(typography.displayLarge), 13 | DisplayMedium(typography.displayMedium), 14 | DisplaySmall(typography.displaySmall), 15 | HeadlineLarge(typography.headlineLarge), 16 | HeadlineMedium(typography.headlineMedium), 17 | HeadlineSmall(typography.headlineSmall), 18 | TitleLarge(typography.titleLarge.copy(fontWeight = FontWeight.Bold)), 19 | TitleMedium(typography.titleMedium.copy(fontWeight = FontWeight.Bold)), 20 | TitleSmall(typography.titleSmall.copy(fontWeight = FontWeight.Bold)), 21 | BodyLarge(typography.bodyLarge.copy(fontSize = 18.sp)), 22 | BodyMedium(typography.bodyMedium), 23 | BodySmall(typography.bodySmall), 24 | LabelLarge(typography.labelLarge), 25 | LabelMedium(typography.labelMedium), 26 | LabelSmall(typography.labelSmall) 27 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/textview/UnifyTextView.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.textview 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import androidx.compose.ui.graphics.Color 7 | import androidx.compose.ui.text.font.FontStyle 8 | import androidx.compose.ui.text.style.TextAlign 9 | import androidx.compose.ui.text.style.TextDecoration 10 | import androidx.compose.ui.text.style.TextOverflow 11 | 12 | @Composable 13 | fun UnifyTextView(config: UnifyTextViewConfig?) { 14 | 15 | config?.apply { 16 | if (text == null) return 17 | 18 | Text( 19 | text = text, 20 | color = color, 21 | textDecoration = textDecoration, 22 | textAlign = textAlign, 23 | maxLines = maxLines, 24 | fontStyle = fontStyle, 25 | style = textType.textStyle, 26 | modifier = modifier, 27 | overflow = TextOverflow.Ellipsis 28 | ) 29 | } 30 | } 31 | 32 | data class UnifyTextViewConfig( 33 | val text: String?, 34 | val textType: UnifyTextType = UnifyTextType.LabelMedium, 35 | val color: Color = Color.Unspecified, 36 | val textDecoration: TextDecoration? = null, 37 | val textAlign: TextAlign? = null, 38 | val maxLines: Int = Int.MAX_VALUE, 39 | val fontStyle: FontStyle? = null, 40 | val modifier: Modifier = Modifier 41 | ) 42 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/topbar/UnifyTopBar.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.component.topbar 2 | 3 | import androidx.compose.material.ripple.LocalRippleTheme 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.CompositionLocalProvider 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.graphics.Color 8 | import pseudoankit.droid.unify.component.icon.UnifyIconConfig 9 | import pseudoankit.droid.unify.component.textview.UnifyTextViewConfig 10 | import pseudoankit.droid.unify.token.UnifyColors 11 | import pseudoankit.droid.unify.utils.DarkThemeRipple 12 | 13 | @Composable 14 | fun UnifyTopBar(config: UnifyTopBarConfig?) { 15 | if (config == null) return 16 | 17 | // changing ripple color for black background 18 | CompositionLocalProvider(LocalRippleTheme provides DarkThemeRipple) { 19 | when (config.type) { 20 | UnifyTopBarConfig.Type.Small -> UnifySmallTopBar(config) 21 | } 22 | } 23 | } 24 | 25 | data class UnifyTopBarConfig( 26 | val leadingIcon: UnifyIconConfig? = null, 27 | val title: String = "", 28 | val type: Type = Type.Small, 29 | val trailingSection: TrailingSection? = null, 30 | val tintColor: Color = UnifyColors.White, 31 | val modifier: Modifier = Modifier 32 | ) { 33 | 34 | data class TrailingSection( 35 | val text: UnifyTextViewConfig? = null, 36 | val icon: UnifyIconConfig? = null, 37 | val modifier: Modifier = Modifier, 38 | val onClick: (() -> Unit)? = null 39 | ) 40 | 41 | enum class Type { Small } 42 | } 43 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/component/viewpager/HorizontalViewPager.kt: -------------------------------------------------------------------------------- 1 | //package pseudoankit.droid.unify.component.viewpager 2 | // 3 | //import android.annotation.SuppressLint 4 | //import androidx.compose.foundation.layout.Column 5 | //import androidx.compose.foundation.layout.fillMaxSize 6 | //import androidx.compose.runtime.Composable 7 | //import androidx.compose.ui.Modifier 8 | //import androidx.compose.ui.graphics.Color 9 | //import com.google.accompanist.pager.HorizontalPager 10 | //import com.google.accompanist.pager.rememberPagerState 11 | //import kotlinx.collections.immutable.ImmutableList 12 | //import pseudoankit.droid.unify.token.UnifyColors 13 | // 14 | //data class HorizontalViewPagerConfig( 15 | // val items: ImmutableList, 16 | // val unSelectedColor: Color = UnifyColors.Black, 17 | // val selectedColor: Color = UnifyColors.Green800, 18 | // val modifier: Modifier = Modifier.fillMaxSize() 19 | //) { 20 | // 21 | // data class Item( 22 | // val label: String, 23 | // val tag: Tag 24 | // ) 25 | // 26 | // interface Tag 27 | //} 28 | // 29 | //@Composable 30 | //fun HorizontalViewPager( 31 | // config: HorizontalViewPagerConfig, 32 | // content: @Composable (position: Int, tag: HorizontalViewPagerConfig.Tag) -> Unit 33 | //) { 34 | // val pagerState = rememberPagerState() 35 | // 36 | // Column(modifier = config.modifier) { 37 | // HorizontalTabsInternal.HorizontalTabs(items = config.items, pagerState = pagerState) 38 | // HorizontalPager( 39 | // count = config.items.size, 40 | // state = pagerState 41 | // ) { currentPage -> 42 | // val item = config.items.getOrNull(currentPage) ?: return@HorizontalPager 43 | // content.invoke(currentPage, item.tag) 44 | // } 45 | // } 46 | //} -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/screen/UnifyScreenConfig.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.screen 2 | 3 | import androidx.compose.foundation.layout.BoxScope 4 | import androidx.compose.runtime.Composable 5 | import pseudoankit.droid.unify.component.topbar.UnifyTopBar 6 | import pseudoankit.droid.unify.component.topbar.UnifyTopBarConfig 7 | 8 | /** 9 | * @param[topBar] for custom topBars 10 | */ 11 | data class UnifyScreenConfig( 12 | val topBar: @Composable BoxScope.() -> Unit, 13 | val floatingActionButton: @Composable () -> Unit = {} 14 | ) { 15 | 16 | /** 17 | * @param[topBar] topBar or actionBar configs 18 | */ 19 | constructor( 20 | topBar: UnifyTopBarConfig? = null, 21 | floatingActionButton: @Composable () -> Unit = {}, 22 | ) : this( 23 | topBar = { 24 | UnifyTopBar(topBar) 25 | }, 26 | floatingActionButton = floatingActionButton, 27 | ) 28 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/token/UnifyDimens.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.token 2 | 3 | import androidx.compose.ui.unit.dp 4 | 5 | object UnifyDimens { 6 | val Dp_0 = 0.dp 7 | val Dp_1 = 1.dp 8 | val Dp_2 = 2.dp 9 | val Dp_4 = 4.dp 10 | val Dp_6 = 6.dp 11 | val Dp_8 = 8.dp 12 | val Dp_10 = 10.dp 13 | val Dp_12 = 12.dp 14 | val Dp_14 = 14.dp 15 | val Dp_16 = 16.dp 16 | val Dp_20 = 20.dp 17 | val Dp_24 = 24.dp 18 | val Dp_30 = 30.dp 19 | val Dp_32 = 32.dp 20 | val Dp_34 = 34.dp 21 | val Dp_36 = 36.dp 22 | val Dp_44 = 44.dp 23 | val Dp_48 = 48.dp 24 | val Dp_50 = 50.dp 25 | val Dp_52 = 52.dp 26 | val Dp_54 = 54.dp 27 | val Dp_58 = 58.dp 28 | val Dp_60 = 60.dp 29 | val Dp_64 = 64.dp 30 | val Dp_70 = 70.dp 31 | 32 | val ScreenPadding = Dp_16 33 | 34 | object Radius { 35 | val Large = Dp_32 36 | val Medium = Dp_16 37 | val Small = Dp_12 38 | val XSmall = Dp_8 39 | } 40 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/token/UnifyTheme.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.token 2 | 3 | import android.app.Activity 4 | import android.os.Build 5 | import androidx.compose.foundation.isSystemInDarkTheme 6 | import androidx.compose.material3.* 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.SideEffect 9 | import androidx.compose.ui.graphics.toArgb 10 | import androidx.compose.ui.platform.LocalContext 11 | import androidx.compose.ui.platform.LocalView 12 | import androidx.core.view.ViewCompat 13 | 14 | private val DarkColorScheme = darkColorScheme( 15 | ) 16 | 17 | private val LightColorScheme = lightColorScheme( 18 | ) 19 | 20 | @Composable 21 | fun UnifyTheme( 22 | darkTheme: Boolean = isSystemInDarkTheme(), 23 | // Dynamic color is available on Android 12+ 24 | dynamicColor: Boolean = true, 25 | content: @Composable () -> Unit 26 | ) { 27 | val colorScheme = when { 28 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { 29 | val context = LocalContext.current 30 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) 31 | } 32 | darkTheme -> DarkColorScheme 33 | else -> LightColorScheme 34 | } 35 | val view = LocalView.current 36 | if (!view.isInEditMode) { 37 | SideEffect { 38 | (view.context as? Activity)?.window?.statusBarColor = colorScheme.primary.toArgb() 39 | ViewCompat.getWindowInsetsController(view)?.isAppearanceLightStatusBars = darkTheme 40 | } 41 | } 42 | 43 | MaterialTheme( 44 | colorScheme = colorScheme, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/token/UnifyTokens.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.token 2 | 3 | import pseudoankit.droid.unify.component.textview.UnifyTextType 4 | 5 | internal object UnifyTokens { 6 | object Button { 7 | val Radius = UnifyDimens.Radius.Large 8 | val Height = UnifyDimens.Dp_58 9 | val TextType = UnifyTextType.TitleMedium 10 | } 11 | 12 | object TimePicker { 13 | val SelectedColor = UnifyColors.Blue200 14 | } 15 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/utils/DarkThemeRipple.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.utils 2 | 3 | import androidx.compose.material.ripple.RippleAlpha 4 | import androidx.compose.material.ripple.RippleTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.graphics.Color 7 | 8 | internal object DarkThemeRipple : RippleTheme { 9 | 10 | @Composable 11 | override fun defaultColor() = 12 | RippleTheme.defaultRippleColor(Color.White, lightTheme = true) 13 | 14 | @Composable 15 | override fun rippleAlpha(): RippleAlpha = 16 | RippleTheme.defaultRippleAlpha(Color.Black, lightTheme = true) 17 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/utils/Logs.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.utils 2 | 3 | internal fun logs(value: String) { 4 | println("Design system logs $value") 5 | } -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/utils/Utils.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.utils 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.focus.FocusRequester 6 | import pseudoankit.droid.unify.R 7 | 8 | typealias UnifyDrawable = R.drawable 9 | 10 | @Composable 11 | fun rememberFocusRequester() = remember { 12 | FocusRequester() 13 | } 14 | 15 | -------------------------------------------------------------------------------- /core/design-system/src/main/java/pseudoankit/droid/unify/utils/internal/Utils.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.unify.utils.internal 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.remember 6 | 7 | @Composable 8 | internal fun rememberMutableState(value: T, vararg keys: Any?) = remember(*keys) { 9 | mutableStateOf(value) 10 | } -------------------------------------------------------------------------------- /core/design-system/src/main/res/drawable/ic_add.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /core/design-system/src/main/res/drawable/ic_calendar.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /core/design-system/src/main/res/drawable/ic_notification.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /core/design-system/src/main/res/drawable/ic_task.xml: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | -------------------------------------------------------------------------------- /core/design-system/src/main/res/drawable/img_placeholder.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /core/notification-manager/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/notification-manager/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | } 4 | 5 | android { 6 | namespace = "pseudoankit.droid.notification_manager" 7 | } 8 | 9 | dependencies { 10 | implementation(projects.core.designSystem) 11 | } -------------------------------------------------------------------------------- /core/notification-manager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/notification-manager/src/main/java/pseudoankit/droid/notification_manager/TaskyNotifierConfig.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.notification_manager 2 | 3 | import java.util.Random 4 | 5 | data class TaskyNotifierConfig( 6 | val notificationId: Int = Random().nextInt(), 7 | val source: Source, 8 | val title: String = source.name, 9 | val description: String, 10 | val priority: Priority = Priority.Default, 11 | val navigationUrl: String, 12 | ) { 13 | 14 | enum class Priority( 15 | internal val priority: Int, 16 | internal val importance: Int, 17 | ) { 18 | Default(priority = 0, importance = 3), 19 | Low(priority = -1, importance = 2), 20 | Min(priority = -2, importance = 1), 21 | High(priority = 1, importance = 4), 22 | Max(priority = 2, importance = 5) 23 | } 24 | 25 | enum class Source { 26 | Reminder, 27 | Task, 28 | Event 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/notification-manager/src/main/java/pseudoankit/droid/notification_manager/di/NotifierModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.notification_manager.di 2 | 3 | import org.koin.android.ext.koin.androidApplication 4 | import org.koin.dsl.module 5 | import pseudoankit.droid.core.koin.BaseKoinModule 6 | import pseudoankit.droid.notification_manager.TaskyNotifier 7 | 8 | object NotifierModule { 9 | 10 | operator fun invoke() = module { 11 | single { TaskyNotifier(androidApplication()) } 12 | } 13 | } -------------------------------------------------------------------------------- /core/notification-manager/src/main/java/pseudoankit/droid/notification_manager/util/TaskyNotifierUtils.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.notification_manager.util 2 | 3 | import pseudoankit.droid.notification_manager.TaskyNotifierConfig 4 | import pseudoankit.droid.unify.utils.UnifyDrawable 5 | 6 | internal object TaskyNotifierUtils { 7 | 8 | } -------------------------------------------------------------------------------- /core/notification-manager/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tasky 4 | -------------------------------------------------------------------------------- /core/permission-manager/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/permission-manager/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeFeatureLib) 4 | } 5 | 6 | android { 7 | namespace = "com.example.permission_manager" 8 | } 9 | 10 | dependencies { 11 | api("com.google.accompanist:accompanist-permissions:0.26.2-beta") 12 | } -------------------------------------------------------------------------------- /core/permission-manager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/permission-manager/src/main/java/com/example/permission_manager/PermissionUtils.kt: -------------------------------------------------------------------------------- 1 | package com.example.permission_manager 2 | 3 | import android.content.Context 4 | import androidx.compose.runtime.Composable 5 | import com.google.accompanist.permissions.PermissionState 6 | import com.google.accompanist.permissions.PermissionStatus 7 | import com.google.accompanist.permissions.rememberPermissionState 8 | import com.google.accompanist.permissions.shouldShowRationale 9 | import pseudoankit.droid.coreui.util.extension.navigateToSettings 10 | import pseudoankit.droid.unify.component.button.UnifyButton 11 | import pseudoankit.droid.unify.component.button.UnifyButtonConfig 12 | 13 | fun PermissionState.requestPermission(context: Context) = when (taskyStatus) { 14 | TaskyPermissionStatus.Granted -> { /* no-op */ 15 | } 16 | TaskyPermissionStatus.DeclinedOnce -> launchPermissionRequest() 17 | TaskyPermissionStatus.DeclinedPermanently -> context.navigateToSettings() 18 | } 19 | 20 | val PermissionState.isGranted get() = this.status == PermissionStatus.Granted 21 | 22 | val PermissionState.taskyStatus 23 | get() = when (this.status) { 24 | is PermissionStatus.Denied -> { 25 | if (this.status.shouldShowRationale) { 26 | TaskyPermissionStatus.DeclinedOnce 27 | } else { 28 | TaskyPermissionStatus.DeclinedPermanently 29 | } 30 | } 31 | PermissionStatus.Granted -> { 32 | TaskyPermissionStatus.Granted 33 | } 34 | } 35 | 36 | @Composable 37 | internal fun DemoPermission() { 38 | val launcher = 39 | rememberPermissionState(permission = android.Manifest.permission.ACCESS_FINE_LOCATION) 40 | 41 | when (launcher.taskyStatus) { 42 | TaskyPermissionStatus.Granted -> {} 43 | TaskyPermissionStatus.DeclinedOnce -> {} 44 | TaskyPermissionStatus.DeclinedPermanently -> {} 45 | } 46 | 47 | UnifyButton(UnifyButtonConfig(text = "Btn")) { 48 | launcher.launchPermissionRequest() 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /core/permission-manager/src/main/java/com/example/permission_manager/TaskyPermissionStatus.kt: -------------------------------------------------------------------------------- 1 | package com.example.permission_manager 2 | 3 | enum class TaskyPermissionStatus { 4 | Granted, 5 | DeclinedOnce, 6 | DeclinedPermanently 7 | } 8 | -------------------------------------------------------------------------------- /core/preferences-manager/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/preferences-manager/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.AndroidLibrary) 3 | id(Plugins.Core) 4 | } 5 | 6 | android { 7 | namespace = "pseudoankit.droid.appprefrences" 8 | } 9 | 10 | dependencies { 11 | implementation(libs.datastore) 12 | } -------------------------------------------------------------------------------- /core/preferences-manager/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/preferences-manager/src/main/java/pseudoankit/droid/di/PreferencesModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.di 2 | 3 | import android.content.Context 4 | import androidx.datastore.core.DataStore 5 | import androidx.datastore.preferences.core.Preferences 6 | import androidx.datastore.preferences.preferencesDataStore 7 | import org.koin.android.ext.koin.androidContext 8 | import org.koin.dsl.module 9 | import pseudoankit.droid.preferencesmanager.PreferenceRepository 10 | 11 | object PreferencesModule { 12 | 13 | private val Context.dataStore: DataStore by preferencesDataStore("tasky-prefs") 14 | 15 | operator fun invoke() = module { 16 | single { PreferenceRepository(datStore = androidContext().dataStore) } 17 | } 18 | } -------------------------------------------------------------------------------- /core/preferences-manager/src/main/java/pseudoankit/droid/preferencesmanager/BasePreference.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.preferencesmanager 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import androidx.datastore.preferences.core.edit 6 | import androidx.datastore.preferences.core.emptyPreferences 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.catch 9 | import kotlinx.coroutines.flow.distinctUntilChanged 10 | import kotlinx.coroutines.flow.firstOrNull 11 | import kotlinx.coroutines.flow.map 12 | 13 | abstract class BasePreference( 14 | val prefDataStore: DataStore 15 | ) { 16 | 17 | suspend inline fun setValue(key: Preferences.Key, value: T) { 18 | prefDataStore.edit { 19 | it[key] = value 20 | } 21 | } 22 | 23 | suspend inline fun getValue(key: Preferences.Key): T? { 24 | return getValueAsFlow(key).firstOrNull() 25 | } 26 | 27 | inline fun getValueAsFlow(key: Preferences.Key): Flow { 28 | return prefDataStore.data 29 | .catch { 30 | emptyPreferences() 31 | } 32 | .map { prefs -> 33 | prefs[key] 34 | } 35 | .distinctUntilChanged() 36 | } 37 | } -------------------------------------------------------------------------------- /core/preferences-manager/src/main/java/pseudoankit/droid/preferencesmanager/Keys.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.preferencesmanager 2 | 3 | import androidx.datastore.preferences.core.booleanPreferencesKey 4 | import androidx.datastore.preferences.core.stringPreferencesKey 5 | 6 | internal object Keys { 7 | val isLoggedIn = booleanPreferencesKey("is_logged_in") 8 | } -------------------------------------------------------------------------------- /core/preferences-manager/src/main/java/pseudoankit/droid/preferencesmanager/PreferenceRepository.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.preferencesmanager 2 | 3 | import androidx.datastore.core.DataStore 4 | import androidx.datastore.preferences.core.Preferences 5 | import kotlinx.coroutines.flow.map 6 | 7 | class PreferenceRepository( 8 | datStore: DataStore 9 | ) : BasePreference(datStore) { 10 | 11 | fun isLoggedInFlow() = getValueAsFlow(Keys.isLoggedIn).map { it == true } 12 | suspend fun isLoggedIn() = getValue(Keys.isLoggedIn) 13 | suspend fun setIsLoggedIn(value: Boolean) = setValue(Keys.isLoggedIn, value) 14 | } -------------------------------------------------------------------------------- /core/test-helper/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /core/test-helper/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | } 4 | 5 | android { 6 | namespace = "com.pseudoankit.test_helper" 7 | } 8 | 9 | dependencies { 10 | implementation(libs.bundles.test) 11 | } -------------------------------------------------------------------------------- /core/test-helper/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /core/test-helper/src/main/java/com/pseudoankit/test_helper/CoroutineDispatcherRule.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.test_helper 2 | 3 | import kotlinx.coroutines.Dispatchers 4 | import kotlinx.coroutines.ExperimentalCoroutinesApi 5 | import kotlinx.coroutines.test.StandardTestDispatcher 6 | import kotlinx.coroutines.test.TestDispatcher 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 | import pseudoankit.droid.core.dispatcher.DispatcherProvider 12 | 13 | @ExperimentalCoroutinesApi 14 | class CoroutineDispatcherRule( 15 | val dispatcher: TestDispatcher = StandardTestDispatcher() 16 | ) : TestWatcher() { 17 | 18 | val dispatcherProvider: DispatcherProvider = 19 | TestCoroutineDispatcherProvider(dispatcher) 20 | 21 | override fun starting(description: Description) { 22 | super.starting(description) 23 | Dispatchers.setMain(dispatcher) 24 | } 25 | 26 | override fun finished(description: Description) { 27 | super.finished(description) 28 | Dispatchers.resetMain() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /core/test-helper/src/main/java/com/pseudoankit/test_helper/TestCoroutineDispatcherProvider.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.test_helper 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.withContext 6 | import pseudoankit.droid.core.dispatcher.DispatcherProvider 7 | 8 | class TestCoroutineDispatcherProvider( 9 | private val dispatcher: CoroutineDispatcher 10 | ) : DispatcherProvider { 11 | 12 | override val io: CoroutineDispatcher 13 | get() = dispatcher 14 | override val default: CoroutineDispatcher 15 | get() = dispatcher 16 | 17 | override suspend fun switchToIo(block: suspend CoroutineScope.() -> T): T { 18 | return withContext(dispatcher) { 19 | block() 20 | } 21 | } 22 | 23 | override suspend fun switchToDefault(block: suspend CoroutineScope.() -> T): T { 24 | return withContext(dispatcher) { 25 | block() 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /core/test-helper/src/main/java/com/pseudoankit/test_helper/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.pseudoankit.test_helper 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Job 5 | import kotlinx.coroutines.launch 6 | import kotlinx.coroutines.test.TestScope 7 | import kotlinx.coroutines.test.UnconfinedTestDispatcher 8 | 9 | fun TestScope.launchInTestScope(block: suspend CoroutineScope.() -> Unit): Job { 10 | return launch(UnconfinedTestDispatcher(testScheduler)) { 11 | block() 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /feature/agenda/event/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/agenda/event/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeFeatureLib) 4 | } 5 | 6 | ksp { 7 | arg("compose-destinations.mode", "destinations") 8 | arg("compose-destinations.moduleName", "agenda-task") 9 | } 10 | 11 | android { 12 | namespace = "pseudoankit.droid.tasky.event" 13 | } -------------------------------------------------------------------------------- /feature/agenda/event/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/agenda/reminder/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/agenda/reminder/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeFeatureLib) 4 | } 5 | 6 | ksp { 7 | arg("compose-destinations.mode", "destinations") 8 | arg("compose-destinations.moduleName", "agenda-reminder") 9 | } 10 | 11 | android { 12 | namespace = "pseudoankit.droid.tasky.reminder" 13 | } 14 | 15 | dependencies { 16 | implementation(projects.core.agendaManger) 17 | } -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/java/pseudoankit/droid/tasky/reminder/di/ReminderModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.reminder.di 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.module 6 | import pseudoankit.droid.core.koin.BaseKoinModule 7 | import pseudoankit.droid.tasky.reminder.presentation.ReminderViewModel 8 | 9 | internal object ReminderModule : BaseKoinModule() { 10 | 11 | override val modules: Module 12 | get() = module { 13 | viewModel { ReminderViewModel(get(), get(), get()) } 14 | } 15 | } -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/java/pseudoankit/droid/tasky/reminder/navigator/ActionNavTypeSerializer.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.reminder.navigator 2 | 3 | import com.ramcosta.composedestinations.navargs.DestinationsNavTypeSerializer 4 | import com.ramcosta.composedestinations.navargs.NavTypeSerializer 5 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 6 | 7 | @NavTypeSerializer 8 | object ActionNavTypeSerializer : DestinationsNavTypeSerializer { 9 | private const val CREATE = 0L 10 | 11 | override fun toRouteString(value: AgendaTypes.Action): String { 12 | return when (value) { 13 | AgendaTypes.Action.Create -> "$CREATE" 14 | is AgendaTypes.Action.Edit -> "${value.id}" 15 | } 16 | } 17 | 18 | override fun fromRouteString(routeStr: String): AgendaTypes.Action { 19 | return when (val id = routeStr.toLongOrNull()) { 20 | null, CREATE -> AgendaTypes.Action.Create 21 | else -> AgendaTypes.Action.Edit(id) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/java/pseudoankit/droid/tasky/reminder/navigator/ReminderDeepLinkProvider.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.reminder.navigator 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 4 | 5 | interface ReminderDeepLinkProvider { 6 | fun reminderScreenRoute(action: AgendaTypes.Action): String 7 | } -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/java/pseudoankit/droid/tasky/reminder/navigator/ReminderNavigator.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.reminder.navigator 2 | 3 | interface ReminderNavigator { 4 | fun navigateToHomeScreen() 5 | fun navigateUp() 6 | } -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/java/pseudoankit/droid/tasky/reminder/presentation/ReminderUiState.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.reminder.presentation 2 | 3 | import androidx.compose.runtime.Stable 4 | import kotlinx.collections.immutable.toImmutableList 5 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 6 | import pseudoankit.droid.core.model.TaskyDate 7 | import pseudoankit.droid.core.model.TaskyTime 8 | import pseudoankit.droid.core.util.TextResource 9 | import pseudoankit.droid.core.util.extension.parseToString 10 | 11 | internal interface ReminderUiState { 12 | 13 | @Stable 14 | data class State( 15 | val reminderText: String = "", 16 | val remindAllDay: Boolean = false, 17 | val selectedDate: TaskyDate = TaskyDate.Today, 18 | val selectedRepeatInterval: AgendaItem.Reminder.RepeatInterval = AgendaItem.Reminder.RepeatInterval.DoNotRepeat, 19 | private val _selectedTime: TaskyTime = TaskyTime.Now, 20 | val editId: Long? = null 21 | ) { 22 | val selectedTime get() = if (remindAllDay) null else _selectedTime 23 | val displayTime get() = selectedTime.parseToString() 24 | val displayDate get() = selectedDate.parseToString("eee, dd MMM yyyy").orEmpty() 25 | val repeatIntervalItems = 26 | AgendaItem.Reminder.RepeatInterval.values().toList().toImmutableList() 27 | } 28 | 29 | sealed interface SideEffect { 30 | object NavigateUp : SideEffect 31 | object ShowDatePicker : SideEffect 32 | object ShowTimePicker : SideEffect 33 | object ToggleRepeatIntervalSelectionView : SideEffect 34 | object ShowCustomRepeatIntervalSelector : SideEffect 35 | object NavigateToHomeScreen : SideEffect 36 | data class ShowError(val message: TextResource) : SideEffect 37 | } 38 | } -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/java/pseudoankit/droid/tasky/reminder/presentation/mapper/ReminderMapper.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.reminder.presentation.mapper 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 4 | import pseudoankit.droid.core.model.TaskyTime 5 | import pseudoankit.droid.core.util.datetime.TimeUtils.orNow 6 | import pseudoankit.droid.tasky.reminder.presentation.ReminderUiState 7 | 8 | internal object ReminderMapper { 9 | 10 | val ReminderUiState.State.mapToReminderObj 11 | get() = kotlin.run { 12 | AgendaItem.Reminder( 13 | title = reminderText, 14 | date = selectedDate, 15 | time = if (remindAllDay) { 16 | AgendaItem.Reminder.Time.AllDay 17 | } else { 18 | AgendaItem.Reminder.Time.Time(selectedTime.orNow) 19 | }, 20 | repeatInterval = selectedRepeatInterval, 21 | id = editId ?: System.currentTimeMillis() 22 | ) 23 | } 24 | 25 | val AgendaItem.Reminder.mapToUiState 26 | get() = kotlin.run { 27 | ReminderUiState.State( 28 | reminderText = title, 29 | selectedDate = date, 30 | selectedRepeatInterval = repeatInterval, 31 | remindAllDay = time == AgendaItem.Reminder.Time.AllDay, 32 | _selectedTime = when (time) { 33 | AgendaItem.Reminder.Time.AllDay -> TaskyTime.Now 34 | is AgendaItem.Reminder.Time.Time -> (time as? AgendaItem.Reminder.Time.Time)?.value.orNow 35 | }, 36 | editId = id 37 | ) 38 | } 39 | } -------------------------------------------------------------------------------- /feature/agenda/reminder/src/main/java/pseudoankit/droid/tasky/reminder/presentation/mapper/RepeatIntervalUiMapper.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.reminder.presentation.mapper 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 4 | import pseudoankit.droid.core.util.TextResource 5 | 6 | internal object RepeatIntervalUiMapper { 7 | 8 | val AgendaItem.Reminder.RepeatInterval.label 9 | get() = when (this) { 10 | AgendaItem.Reminder.RepeatInterval.DoNotRepeat -> TextResource.NormalString("Do not repeat") 11 | AgendaItem.Reminder.RepeatInterval.Daily -> TextResource.NormalString("Repeats daily") 12 | AgendaItem.Reminder.RepeatInterval.Weekly -> TextResource.NormalString("Repeats weekly") 13 | AgendaItem.Reminder.RepeatInterval.Monthly -> TextResource.NormalString("Repeats monthly") 14 | AgendaItem.Reminder.RepeatInterval.Yearly -> TextResource.NormalString("Repeats yearly") 15 | AgendaItem.Reminder.RepeatInterval.Custom -> TextResource.NormalString("Custom") 16 | } 17 | } -------------------------------------------------------------------------------- /feature/agenda/task/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/agenda/task/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeFeatureLib) 4 | } 5 | 6 | ksp { 7 | arg("compose-destinations.mode", "destinations") 8 | arg("compose-destinations.moduleName", "agenda-task") 9 | } 10 | 11 | android { 12 | namespace = "pseudoankit.droid.tasky.task" 13 | } -------------------------------------------------------------------------------- /feature/agenda/task/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/authentication/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/authentication/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeFeatureLib) 4 | } 5 | 6 | ksp { 7 | arg("compose-destinations.mode", "destinations") 8 | arg("compose-destinations.moduleName", "Authentication") 9 | } 10 | 11 | android { 12 | namespace = "pseudoankit.droid.authentication" 13 | } 14 | 15 | dependencies { 16 | implementation(projects.core.preferencesManager) 17 | } 18 | -------------------------------------------------------------------------------- /feature/authentication/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/authentication/src/main/java/pseudoankit/droid/authentication/di/LoginModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.authentication.di 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.module 6 | import pseudoankit.droid.authentication.domain.LoginUserUseCase 7 | import pseudoankit.droid.authentication.presentation.login.LoginViewModel 8 | import pseudoankit.droid.core.koin.BaseKoinModule 9 | 10 | internal object LoginModule : BaseKoinModule() { 11 | 12 | override val modules: Module 13 | get() = module { 14 | factory { LoginUserUseCase(get()) } 15 | 16 | viewModel { LoginViewModel(inject(), get()) } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /feature/authentication/src/main/java/pseudoankit/droid/authentication/di/RegistrationModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.authentication.di 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.module 6 | import pseudoankit.droid.authentication.domain.RegisterUserUseCase 7 | import pseudoankit.droid.authentication.presentation.registration.RegistrationViewModel 8 | import pseudoankit.droid.core.koin.BaseKoinModule 9 | 10 | internal object RegistrationModule : BaseKoinModule() { 11 | 12 | override val modules: Module 13 | get() = module { 14 | factory { RegisterUserUseCase() } 15 | viewModel { RegistrationViewModel(get(), get()) } 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /feature/authentication/src/main/java/pseudoankit/droid/authentication/domain/LoginUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.authentication.domain 2 | 3 | import kotlinx.coroutines.delay 4 | import pseudoankit.droid.authentication.presentation.login.LoginUiState 5 | import pseudoankit.droid.core.util.TextResource 6 | import pseudoankit.droid.core.util.extension.safeCall 7 | import pseudoankit.droid.core.util.validator.Validator 8 | import pseudoankit.droid.preferencesmanager.PreferenceRepository 9 | 10 | internal class LoginUserUseCase( 11 | private val preferenceRepository: PreferenceRepository 12 | ) { 13 | 14 | sealed interface Result { 15 | data class EmailError(val message: TextResource?) : Result 16 | data class PasswordError(val message: TextResource?) : Result 17 | data class Error(val message: TextResource) : Result 18 | object Success : Result 19 | } 20 | 21 | suspend operator fun invoke(state: LoginUiState.State): Result { 22 | return safeCall( 23 | block = { 24 | val emailError = Validator.validateEmail(email = state.emailConfig.value) 25 | if (emailError != null) { 26 | return Result.EmailError(emailError) 27 | } 28 | 29 | val passwordError = 30 | Validator.validatePassword(password = state.passwordConfig.value) 31 | if (passwordError != null) { 32 | return Result.PasswordError(passwordError) 33 | } 34 | 35 | delay(500) 36 | preferenceRepository.setIsLoggedIn(value = true) 37 | Result.Success 38 | }, 39 | onError = { 40 | Result.Error(it) 41 | } 42 | ) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /feature/authentication/src/main/java/pseudoankit/droid/authentication/domain/RegisterUserUseCase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.authentication.domain 2 | 3 | import kotlinx.coroutines.delay 4 | import pseudoankit.droid.authentication.presentation.registration.RegistrationUiState 5 | import pseudoankit.droid.core.util.TextResource 6 | import pseudoankit.droid.core.util.extension.safeCall 7 | import pseudoankit.droid.core.util.validator.Validator 8 | 9 | internal class RegisterUserUseCase { 10 | 11 | sealed interface Result { 12 | data class NameError(val message: TextResource?) : Result 13 | data class EmailError(val message: TextResource?) : Result 14 | data class PasswordError(val message: TextResource?) : Result 15 | data class Error(val message: TextResource) : Result 16 | object Success : Result 17 | } 18 | 19 | suspend operator fun invoke(state: RegistrationUiState.State): Result { 20 | return safeCall( 21 | block = { 22 | val nameError = Validator.validateName(name = state.nameConfig.value) 23 | if (nameError != null) { 24 | return Result.NameError(nameError) 25 | } 26 | 27 | val emailError = Validator.validateEmail(email = state.emailConfig.value) 28 | if (emailError != null) { 29 | return Result.EmailError(emailError) 30 | } 31 | 32 | val passwordError = 33 | Validator.validatePassword(password = state.passwordConfig.value) 34 | if (passwordError != null) { 35 | return Result.PasswordError(passwordError) 36 | } 37 | 38 | delay(500) 39 | 40 | return Result.Success 41 | }, 42 | onError = { 43 | Result.Error(it) 44 | } 45 | ) 46 | } 47 | 48 | } -------------------------------------------------------------------------------- /feature/authentication/src/main/java/pseudoankit/droid/authentication/navigator/AuthNavigator.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.authentication.navigator 2 | 3 | interface AuthNavigator { 4 | fun navigateUp() 5 | fun navigateToRegistrationScreen() 6 | fun navigateToHomeScreen() 7 | } -------------------------------------------------------------------------------- /feature/authentication/src/main/java/pseudoankit/droid/authentication/presentation/login/LoginUiState.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.authentication.presentation.login 2 | 3 | import pseudoankit.droid.coreui.model.TextFieldUiConfig 4 | import pseudoankit.droid.unify.component.button.UnifyButtonConfig 5 | 6 | object LoginUiState { 7 | 8 | // TEST remove def values later on 9 | internal data class State( 10 | val emailConfig: TextFieldUiConfig = TextFieldUiConfig( 11 | value = "lostankit7@gmail.com", 12 | errorMessage = null 13 | ), 14 | val passwordConfig: TextFieldUiConfig = TextFieldUiConfig( 15 | value = "qwerty", 16 | errorMessage = null 17 | ), 18 | val isLoginInProgress: Boolean = false 19 | ) { 20 | val buttonState 21 | get() = UnifyButtonConfig.State.fromBoolean( 22 | isValid = emailConfig.errorMessage == null && passwordConfig.errorMessage == null, 23 | isLoading = isLoginInProgress 24 | ) 25 | } 26 | 27 | internal sealed interface SideEffect { 28 | object NavigateToRegistrationScreen : SideEffect 29 | object NavigateToHomeScreen : SideEffect 30 | } 31 | } -------------------------------------------------------------------------------- /feature/authentication/src/main/java/pseudoankit/droid/authentication/presentation/registration/RegistrationUiState.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.authentication.presentation.registration 2 | 3 | import pseudoankit.droid.core.util.TextResource 4 | import pseudoankit.droid.coreui.model.TextFieldUiConfig 5 | import pseudoankit.droid.unify.component.button.UnifyButtonConfig 6 | 7 | interface RegistrationUiState { 8 | data class State( 9 | val emailConfig: TextFieldUiConfig = TextFieldUiConfig( 10 | value = "", 11 | errorMessage = null 12 | ), 13 | val passwordConfig: TextFieldUiConfig = TextFieldUiConfig( 14 | value = "", 15 | errorMessage = null 16 | ), 17 | val nameConfig: TextFieldUiConfig = TextFieldUiConfig( 18 | value = "", 19 | errorMessage = null 20 | ), 21 | val isRegistrationInProgress: Boolean = false 22 | ) { 23 | val doneBtnState = UnifyButtonConfig.State.fromBoolean( 24 | isValid = emailConfig.errorMessage == null && passwordConfig.errorMessage == null && nameConfig.errorMessage == null, 25 | isLoading = isRegistrationInProgress 26 | ) 27 | } 28 | 29 | sealed interface SideEffect { 30 | object NavigateBack : SideEffect 31 | data class ShowErrorMessage(val message: TextResource) : SideEffect 32 | } 33 | 34 | sealed interface Event { 35 | data class OnNameChanged(val value: String) : Event 36 | data class OnEmailChanged(val value: String) : Event 37 | data class OnPasswordChanged(val value: String) : Event 38 | object OnNavigateBack : Event 39 | object OnRegisterUser : Event 40 | } 41 | } -------------------------------------------------------------------------------- /feature/authentication/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Welcome Back 4 | -------------------------------------------------------------------------------- /feature/developer-tools/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/developer-tools/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | } 4 | 5 | android { 6 | namespace = "pseudoankit.tasky.developer_tools" 7 | } 8 | 9 | dependencies { 10 | debugImplementation(libs.leakCanary) 11 | } -------------------------------------------------------------------------------- /feature/developer-tools/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/home/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/home/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeFeatureLib) 4 | } 5 | 6 | // todo create a utility 7 | ksp { 8 | arg("compose-destinations.mode", "destinations") 9 | arg("compose-destinations.moduleName", "Home") 10 | } 11 | 12 | android { 13 | namespace = "pseudoankit.droid.tasky.home" 14 | } 15 | 16 | dependencies { 17 | implementation(projects.core.agendaManger) 18 | implementation(projects.core.permissionManager) 19 | } -------------------------------------------------------------------------------- /feature/home/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/di/AgendaItemScreenModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.di 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.module 6 | import pseudoankit.droid.core.koin.BaseKoinModule 7 | import pseudoankit.droid.tasky.home.presentation.taskyitems.AgendaItemsViewModel 8 | 9 | object AgendaItemScreenModule : BaseKoinModule() { 10 | override val modules: Module 11 | get() = module { 12 | viewModel { AgendaItemsViewModel() } 13 | } 14 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/di/HomeScreenModule.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.di 2 | 3 | import org.koin.androidx.viewmodel.dsl.viewModel 4 | import org.koin.core.module.Module 5 | import org.koin.dsl.module 6 | import pseudoankit.droid.agendamanger.domain.usecase.reminder.GetSavedAgendaItemsUseCase 7 | import pseudoankit.droid.core.koin.BaseKoinModule 8 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlowInstance 9 | import pseudoankit.droid.tasky.home.domain.usecase.DeleteAgendaUseCase 10 | import pseudoankit.droid.tasky.home.domain.usecase.ToggleAgendaItemCompletionUseCase 11 | import pseudoankit.droid.tasky.home.presentation.home.HomeViewModel 12 | 13 | internal object HomeScreenModule : BaseKoinModule() { 14 | 15 | override val modules: Module 16 | get() = module { 17 | factory { GetSavedAgendaItemsUseCase(get()) } 18 | factory { ToggleAgendaItemCompletionUseCase(get(), get(), get()) } 19 | factory { DeleteAgendaUseCase(get(), UpdateAppWidgetFlowInstance) } 20 | 21 | viewModel { HomeViewModel(get(), get(), get()) } 22 | } 23 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/domain/usecase/DeleteAgendaUseCase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.domain.usecase 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 4 | import pseudoankit.droid.agendamanger.domain.repository.ReminderRepository 5 | import pseudoankit.droid.core.util.TaskyResult 6 | import pseudoankit.droid.core.util.extension.safeCall 7 | import pseudoankit.droid.core.widget.UpdateAppWidgetFlow 8 | 9 | class DeleteAgendaUseCase( 10 | private val reminderRepository: ReminderRepository, 11 | private val updateAppWidgetFlow: UpdateAppWidgetFlow 12 | ) { 13 | 14 | suspend operator fun invoke(agendaItem: AgendaItem) = safeCall { 15 | when (agendaItem) { 16 | is AgendaItem.Event -> TODO() 17 | is AgendaItem.Reminder -> reminderRepository.delete(agendaItem) 18 | is AgendaItem.Task -> TODO() 19 | } 20 | 21 | updateAppWidgetFlow.emit(Unit) 22 | TaskyResult.Success(Unit) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/domain/usecase/ToggleAgendaItemCompletionUseCase.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.domain.usecase 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 4 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 5 | import pseudoankit.droid.agendamanger.domain.repository.ReminderRepository 6 | import pseudoankit.droid.agendamanger.domain.usecase.reminder.SaveReminderUseCase 7 | import pseudoankit.droid.tasky.home.navigator.HomeDeepLinkProvider 8 | 9 | internal class ToggleAgendaItemCompletionUseCase( 10 | private val reminderRepository: ReminderRepository, 11 | private val saveReminderUseCase: SaveReminderUseCase, 12 | private val deepLinkProvider: HomeDeepLinkProvider 13 | ) { 14 | 15 | suspend operator fun invoke(agenda: AgendaItem) { 16 | when (agenda) { 17 | is AgendaItem.Event -> TODO() 18 | is AgendaItem.Reminder -> saveReminderUseCase( 19 | payload = agenda.copy(completed = agenda.completed.not()), 20 | alarmDeepLink = deepLinkProvider.reminderScreenRoute(AgendaTypes.Action.Edit(agenda.id)) 21 | ) 22 | is AgendaItem.Task -> TODO() 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/navigator/HomeDeepLinkProvider.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.navigator 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 4 | 5 | interface HomeDeepLinkProvider { 6 | fun reminderScreenRoute(action: AgendaTypes.Action): String 7 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/navigator/HomeScreenNavigator.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.navigator 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 4 | 5 | interface HomeScreenNavigator { 6 | fun navigateUp() 7 | fun navigateToAgendaItemsSelectorScreen() 8 | fun navigateToAgendaScreen(agendaTypes: AgendaTypes) 9 | 10 | fun navigateToProfileScreen() 11 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/presentation/home/HomeUiState.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.presentation.home 2 | 3 | import kotlinx.collections.immutable.ImmutableList 4 | import kotlinx.collections.immutable.persistentListOf 5 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 6 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 7 | import pseudoankit.droid.core.model.TaskyDate 8 | import pseudoankit.droid.core.util.datetime.DateUtils 9 | import pseudoankit.droid.core.util.extension.parseToString 10 | 11 | internal interface HomeUiState { 12 | 13 | sealed interface SideEffect { 14 | object ShowDatePicker : SideEffect 15 | data class HighlightCurrentSelectedDate(val position: Int) : SideEffect 16 | object ShowAgendaItems : SideEffect 17 | data class NavigateToAgendaScreen(val agendaTypes: AgendaTypes) : SideEffect 18 | object ShowProfileIcon: SideEffect 19 | } 20 | 21 | data class State( 22 | val selectedDate: TaskyDate = TaskyDate.Today, 23 | val savedAgendaItems: ImmutableList = persistentListOf() 24 | ) { 25 | val selectedMonthDateRange: ImmutableList = 26 | DateUtils.getDateRangeForMonth(selectedDate) 27 | 28 | val displayHeaderDate get() = selectedDate.parseToString("MMM yyyy").orEmpty() 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/presentation/mapper/AgendaItemsUiMapper.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.presentation.mapper 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaItem 4 | import pseudoankit.droid.core.util.extension.parseToString 5 | import pseudoankit.droid.unify.token.UnifyColors 6 | 7 | internal object AgendaItemsUiMapper { 8 | val AgendaItem.Reminder.displayDateTime: String? 9 | get() = when (time) { 10 | AgendaItem.Reminder.Time.AllDay -> null 11 | is AgendaItem.Reminder.Time.Time -> { 12 | (time as? AgendaItem.Reminder.Time.Time)?.value.parseToString() 13 | } 14 | } 15 | 16 | val AgendaItem.backgroundColor 17 | get() = when (this) { 18 | is AgendaItem.Reminder -> UnifyColors.Green400 19 | is AgendaItem.Event -> UnifyColors.Blue200 20 | is AgendaItem.Task -> UnifyColors.Yellow300 21 | } 22 | 23 | val AgendaItem.tint get() = UnifyColors.White 24 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/presentation/mapper/AgendaTypesUiMapper.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.presentation.mapper 2 | 3 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 4 | import pseudoankit.droid.core.util.TextResource 5 | import pseudoankit.droid.unify.component.icon.UnifyIcons 6 | 7 | internal object AgendaTypesUiMapper { 8 | 9 | val AgendaTypes.label 10 | get() = when (this) { 11 | is AgendaTypes.Reminder -> TextResource.NormalString("Reminder") 12 | is AgendaTypes.Event -> TextResource.NormalString("Event") 13 | is AgendaTypes.Task -> TextResource.NormalString("Task") 14 | } 15 | 16 | val AgendaTypes.icon 17 | get() = when (this) { 18 | is AgendaTypes.Reminder -> UnifyIcons.Bell 19 | is AgendaTypes.Event -> UnifyIcons.Calendar 20 | is AgendaTypes.Task -> UnifyIcons.Task 21 | } 22 | 23 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/presentation/taskyitems/AgendaItemsUiState.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.presentation.taskyitems 2 | 3 | import kotlinx.collections.immutable.ImmutableList 4 | import kotlinx.collections.immutable.persistentListOf 5 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 6 | 7 | internal interface AgendaItemsUiState { 8 | 9 | data class State( 10 | val items: ImmutableList = persistentListOf( 11 | AgendaTypes.Reminder(AgendaTypes.Action.Create), 12 | AgendaTypes.Task(AgendaTypes.Action.Create), 13 | AgendaTypes.Event(AgendaTypes.Action.Create), 14 | ) 15 | ) 16 | 17 | sealed interface SideEffect { 18 | object NavigateUp : SideEffect 19 | data class NavigateToAgenda(val type: AgendaTypes) : SideEffect 20 | } 21 | } -------------------------------------------------------------------------------- /feature/home/src/main/java/pseudoankit/droid/tasky/home/presentation/taskyitems/AgendaItemsViewModel.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.tasky.home.presentation.taskyitems 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import org.orbitmvi.orbit.Container 6 | import org.orbitmvi.orbit.ContainerHost 7 | import org.orbitmvi.orbit.container 8 | import pseudoankit.droid.agendamanger.domain.model.AgendaTypes 9 | import pseudoankit.droid.coreui.util.extension.postSideEffect 10 | 11 | internal class AgendaItemsViewModel : ViewModel(), 12 | ContainerHost { 13 | 14 | override val container: Container = 15 | viewModelScope.container(AgendaItemsUiState.State()) 16 | 17 | fun onNavigateUp() = postSideEffect { 18 | AgendaItemsUiState.SideEffect.NavigateUp 19 | } 20 | 21 | fun onAgendaSelected(type: AgendaTypes) = postSideEffect { 22 | AgendaItemsUiState.SideEffect.NavigateToAgenda(type) 23 | } 24 | } -------------------------------------------------------------------------------- /feature/profile/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /feature/profile/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Plugins.CoreFeatureLib) 3 | id(Plugins.ComposeFeatureLib) 4 | } 5 | 6 | ksp { 7 | arg("compose-destinations.mode", "destinations") 8 | arg("compose-destinations.moduleName", "Profile") 9 | } 10 | 11 | android { 12 | namespace = "pseudoankit.droid.profile" 13 | } 14 | -------------------------------------------------------------------------------- /feature/profile/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /feature/profile/src/main/java/pseudoankit/droid/profile/presentation/ui/ProfileScreen.kt: -------------------------------------------------------------------------------- 1 | package pseudoankit.droid.profile.presentation.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.ramcosta.composedestinations.annotation.DeepLink 5 | import com.ramcosta.composedestinations.annotation.Destination 6 | import pseudoankit.droid.core.deeplink.TaskyDeeplink 7 | 8 | @Destination( 9 | deepLinks = [ 10 | DeepLink(uriPattern = TaskyDeeplink.profile) 11 | ] 12 | ) 13 | @Composable 14 | internal fun ProfileScreen() { 15 | 16 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app's APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Kotlin code style for this project: "official" or "obsolete": 19 | kotlin.code.style=official 20 | # Enables namespacing of each library's R class so that its R class includes only the 21 | # resources declared in the library itself and none from the library's dependencies, 22 | # thereby reducing the size of the R class for that library 23 | android.nonTransitiveRClass=true 24 | # Automatically convert third-party libraries to use AndroidX 25 | android.enableJetifier=true 26 | systemProp.sonar.sources=./src/main 27 | systemProp.sonar.host.url=https://sonarcloud.io/ 28 | systemProp.sonar.organization=pseudoankit 29 | systemProp.sonar.projectKey=pseudoankit_Tasky 30 | systemProp.sonar.projectName=Tasky 31 | android.defaults.buildfeatures.buildconfig=true 32 | android.nonFinalResIds=false -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /preview/add-reminder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/preview/add-reminder.png -------------------------------------------------------------------------------- /preview/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/preview/home.png -------------------------------------------------------------------------------- /preview/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/preview/login.png -------------------------------------------------------------------------------- /preview/shortcut.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/preview/shortcut.png -------------------------------------------------------------------------------- /preview/widget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ankitk77/tasky/5cbe268aa0bc0ccd121eeccc42c60aea6815078f/preview/widget.png -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | "group:all", 6 | ":dependencyDashboard", 7 | "schedule:daily" 8 | ], 9 | "baseBranches": [ 10 | "master" 11 | ], 12 | "commitMessageExtra": "{{{currentValue}}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}", 13 | "packageRules": [ 14 | { 15 | "matchPackagePatterns": [ 16 | "androidx.compose.compiler:compiler" 17 | ], 18 | "groupName": "kotlin" 19 | }, 20 | { 21 | "matchPackagePatterns": [ 22 | "org.jetbrains.kotlin.*" 23 | ], 24 | "groupName": "kotlin" 25 | }, 26 | { 27 | "matchPackagePatterns": [ 28 | "com.google.devtools.ksp" 29 | ], 30 | "groupName": "kotlin" 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | rootProject.name = "Tasky" 4 | include(":app") 5 | 6 | include(":core:core") 7 | include(":core:core-ui") 8 | include(":core:design-system") 9 | include(":core:agenda-manger") 10 | include(":core:database-manager") 11 | include(":core:preferences-manager") 12 | include(":core:alarm-manager") 13 | include(":core:notification-manager") 14 | include(":core:permission-manager") 15 | include(":core:app-shortcuts-n-widgets") 16 | include(":core:test-helper") 17 | 18 | include(":feature:authentication") 19 | include(":feature:home") 20 | include(":feature:agenda:event") 21 | include(":feature:agenda:reminder") 22 | include(":feature:agenda:task") 23 | include(":feature:profile") 24 | include(":feature:developer-tools") 25 | 26 | include(":benchmark") 27 | --------------------------------------------------------------------------------