├── .github
└── workflows
│ ├── Lint.yml
│ ├── Release.yml
│ └── Testing.yml
├── .gitignore
├── .idea
├── .gitignore
├── .name
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── gradle.xml
├── inspectionProfiles
│ └── Project_Default.xml
├── misc.xml
└── vcs.xml
├── LICENSE
├── README.md
├── app
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── zlagi
│ │ └── blogfy
│ │ ├── BlogfyApplication.kt
│ │ ├── di
│ │ ├── DispatcherModule.kt
│ │ ├── ImageLoaderModule.kt
│ │ ├── TaskManagerModule.kt
│ │ └── WorkerModule.kt
│ │ └── view
│ │ ├── MainActivity.kt
│ │ ├── account
│ │ ├── detail
│ │ │ └── AccountDetailFragment.kt
│ │ └── update
│ │ │ └── UpdatePasswordFragment.kt
│ │ ├── auth
│ │ ├── AuthActivity.kt
│ │ ├── onboarding
│ │ │ └── OnBoardingFragment.kt
│ │ ├── signin
│ │ │ └── SignInFragment.kt
│ │ └── signup
│ │ │ └── SignUpFragment.kt
│ │ ├── blog
│ │ ├── create
│ │ │ └── CreateBlogFragment.kt
│ │ ├── detail
│ │ │ └── BlogDetailFragment.kt
│ │ ├── feed
│ │ │ ├── FeedAdapter.kt
│ │ │ ├── FeedFragment.kt
│ │ │ ├── FeedViewHolder.kt
│ │ │ └── OnItemSelectedListener.kt
│ │ ├── search
│ │ │ ├── history
│ │ │ │ ├── SearchHistoryAdapter.kt
│ │ │ │ └── SearchHistoryFragment.kt
│ │ │ └── result
│ │ │ │ ├── SearchResultAdapter.kt
│ │ │ │ └── SearchResultFragment.kt
│ │ └── update
│ │ │ └── UpdateBlogFragment.kt
│ │ └── utils
│ │ ├── AnimationUtils.kt
│ │ ├── BottomSpacingItemDecoration.kt
│ │ ├── LoadingDialogFragment.kt
│ │ ├── MenuBottomSheetDialogFragment.kt
│ │ ├── SpringAddItemAnimator.kt
│ │ └── ViewExtensions.kt
│ └── res
│ ├── anim
│ ├── fade_in.xml
│ ├── slide_down.xml
│ ├── slide_in_left.xml
│ ├── slide_in_right.xml
│ ├── slide_out_left.xml
│ └── slide_out_right.xml
│ ├── drawable-v24
│ ├── curved_toolbar.xml
│ ├── ic_baseline_add_photo_alternate_24.xml
│ ├── ic_baseline_delete_forever_24.xml
│ ├── ic_check_green_24dp.xml
│ ├── ic_edit_black_24dp.xml
│ ├── ic_launcher_foreground.xml
│ ├── ic_lock.xml
│ ├── ic_user.xml
│ └── logo.png
│ ├── drawable
│ ├── blog_detail_image_scream.xml
│ ├── button_shape.xml
│ ├── divider.xml
│ ├── email_button_shape.xml
│ ├── ic_arrow_left.xml
│ ├── ic_back_arrow.xml
│ ├── ic_baseline_clear_24.xml
│ ├── ic_baseline_home_24.xml
│ ├── ic_baseline_lock_24.xml
│ ├── ic_baseline_mail_outline_24.xml
│ ├── ic_baseline_more_vert_24.xml
│ ├── ic_baseline_person_24.xml
│ ├── ic_baseline_settings_24.xml
│ ├── ic_google.xml
│ ├── ic_home_selector.xml
│ ├── ic_launcher_background.xml
│ ├── ic_nav_bottom_selector.xml
│ ├── ic_outline_home_24.xml
│ ├── ic_outline_search_24.xml
│ ├── ic_outline_settings_24.xml
│ ├── ic_search.xml
│ ├── ic_settings_selector.xml
│ ├── ic_up_arrow_selector.xml
│ ├── small_component_foreground.xml
│ └── splash_background.xml
│ ├── font
│ ├── gilroy_bold.ttf
│ ├── gilroy_regular.ttf
│ ├── gilroy_semibold.ttf
│ ├── opensans_bold.ttf
│ ├── opensans_light.ttf
│ ├── opensans_regular.ttf
│ ├── poppins_medium.ttf
│ ├── poppins_regular.ttf
│ ├── raleway_medium.ttf
│ ├── raleway_regular.ttf
│ └── universal_std.otf
│ ├── layout-land
│ └── fragment_on_boarding.xml
│ ├── layout
│ ├── activity_auth.xml
│ ├── activity_main.xml
│ ├── feed_item_layout.xml
│ ├── fragment_account_detail.xml
│ ├── fragment_blog_detail.xml
│ ├── fragment_create_blog.xml
│ ├── fragment_feed.xml
│ ├── fragment_loading_dialog.xml
│ ├── fragment_on_boarding.xml
│ ├── fragment_search_blog.xml
│ ├── fragment_sign_in.xml
│ ├── fragment_sign_up.xml
│ ├── fragment_update_blog.xml
│ ├── fragment_update_password.xml
│ ├── layout_search_history.xml
│ ├── layout_search_result.xml
│ ├── menu_bottom_sheet_dialog.xml
│ ├── search_history_item.xml
│ └── search_item.xml
│ ├── menu-v26
│ └── blog_detail.xml
│ ├── menu
│ ├── blog_detail.xml
│ ├── bottom_nav.xml
│ └── feed.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ └── ic_launcher_round.webp
│ ├── navigation
│ ├── nav_account.xml
│ ├── nav_auth.xml
│ ├── nav_blog_detail.xml
│ ├── nav_feed.xml
│ ├── nav_graph.xml
│ └── nav_search.xml
│ ├── raw
│ ├── image_loader.json
│ └── tumbleweed_rolling.json
│ └── values
│ ├── attrs.xml
│ ├── colors.xml
│ ├── dimens.xml
│ ├── ids.xml
│ ├── motions.xml
│ ├── strings.xml
│ ├── themes.xml
│ ├── type.xml
│ └── typography.xml
├── build.gradle
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── java
│ ├── Deps.kt
│ ├── SDKConfig.kt
│ └── Versions.kt
├── buildscripts
├── detekt.gradle
└── ktlint.gradle
├── cache
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── schemas
│ ├── com.zlagi.cache.database.BlogfyDatabase
│ │ └── 1.json
│ ├── com.zlagi.cache.test.VideosDatabase
│ │ └── 1.json
│ └── com.zlagi.local.database.BlogfyDatabase
│ │ └── 1.json
└── src
│ ├── androidTest
│ └── java
│ │ └── com
│ │ └── zlagi
│ │ └── cache
│ │ ├── HiltTestRunner.kt
│ │ ├── di
│ │ └── TestCacheModule.kt
│ │ ├── fakes
│ │ └── FakeDataGenerator.kt
│ │ └── source
│ │ ├── DefaultAccountCacheDataSourceTest.kt
│ │ ├── DefaultFeedCacheDataSourceTest.kt
│ │ ├── DefaultHistoryCacheDataSourceTest.kt
│ │ └── DefaultSearchBlogCacheDataSourceTest.kt
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── zlagi
│ └── cache
│ ├── database
│ ├── BlogfyDatabase.kt
│ ├── account
│ │ └── AccountDao.kt
│ ├── feed
│ │ └── FeedDao.kt
│ └── search
│ │ ├── SearchDao.kt
│ │ └── history
│ │ └── HistoryDao.kt
│ ├── di
│ └── CacheModule.kt
│ ├── mapper
│ ├── AccountCacheDataMapper.kt
│ ├── FeedBlogCacheDataMapper.kt
│ ├── HistoryCacheDataMapper.kt
│ └── SearchBlogCacheDataMapper.kt
│ ├── model
│ ├── AccountCacheModel.kt
│ ├── FeedBlogCacheModel.kt
│ ├── HistoryCacheModel.kt
│ └── SearchBlogCacheModel.kt
│ └── source
│ ├── account
│ └── DefaultAccountCacheDataSource.kt
│ ├── feed
│ └── DefaultFeedCacheDataSource.kt
│ └── search
│ ├── DefaultSearchBlogCacheDataSource.kt
│ └── history
│ └── DefaultHistoryCacheDataSource.kt
├── common
├── .gitignore
├── build.gradle
├── proguard-rules.pro
├── schemas
│ └── com.zlagi.common.data.cache.BlogfyDatabase
│ │ └── 1.json
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── zlagi
│ │ └── common
│ │ ├── exception
│ │ ├── CacheException.kt
│ │ ├── MappingException.kt
│ │ └── NetworkException.kt
│ │ ├── mapper
│ │ ├── ExceptionMapper.kt
│ │ ├── ExceptionMessageMapper.kt
│ │ └── Mapper.kt
│ │ ├── qualifier
│ │ ├── DefaultDispatcher.kt
│ │ ├── IoDispatcher.kt
│ │ └── MainDispatcher.kt
│ │ └── utils
│ │ ├── AuthError.kt
│ │ ├── BlogError.kt
│ │ ├── Constants.kt
│ │ ├── PreferencesConstants.kt
│ │ ├── result
│ │ ├── SignInResult.kt
│ │ ├── SignUpResult.kt
│ │ ├── UpdateBlogResult.kt
│ │ └── UpdatePasswordResult.kt
│ │ └── wrapper
│ │ └── DataResult.kt
│ └── res
│ └── values
│ └── strings.xml
├── config
└── detekt
│ └── detekt.yml
├── data
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── zlagi
│ │ └── data
│ │ ├── connectivity
│ │ └── ConnectivityChecker.kt
│ │ ├── di
│ │ └── RepositoryModule.kt
│ │ ├── mapper
│ │ ├── AccountDataDomainMapper.kt
│ │ ├── BlogDataDomainMapper.kt
│ │ ├── HistoryDataDomainMapper.kt
│ │ ├── PaginationDataDomainMapper.kt
│ │ └── TokensDataDomainMapper.kt
│ │ ├── model
│ │ ├── AccountDataModel.kt
│ │ ├── BlogDataModel.kt
│ │ ├── HistoryDataModel.kt
│ │ ├── PaginatedBlogsDataModel.kt
│ │ ├── PaginationDataModel.kt
│ │ └── TokensDataModel.kt
│ │ ├── repository
│ │ ├── account
│ │ │ └── DefaultAccountRepository.kt
│ │ ├── auth
│ │ │ └── DefaultAuthRepository.kt
│ │ ├── feed
│ │ │ └── DefaultFeedRepository.kt
│ │ └── search
│ │ │ ├── DefaultSearchBlogRepository.kt
│ │ │ └── history
│ │ │ └── DefaultHistoryRepository.kt
│ │ ├── source
│ │ ├── cache
│ │ │ ├── account
│ │ │ │ └── AccountCacheDataSource.kt
│ │ │ ├── feed
│ │ │ │ └── FeedCacheDataSource.kt
│ │ │ └── search
│ │ │ │ ├── SearchBlogCacheDataSource.kt
│ │ │ │ └── history
│ │ │ │ └── HistoryCacheDataSource.kt
│ │ ├── network
│ │ │ ├── account
│ │ │ │ └── AccountNetworkDataSource.kt
│ │ │ ├── auth
│ │ │ │ └── AuthNetworkDataSource.kt
│ │ │ └── blog
│ │ │ │ └── BlogNetworkDataSource.kt
│ │ └── preferences
│ │ │ └── PreferencesDataSource.kt
│ │ ├── taskmanager
│ │ └── DefaultTaskManager.kt
│ │ └── worker
│ │ └── RefreshDataWorker.kt
│ └── test
│ └── java
│ └── com
│ └── zlagi
│ └── data
│ ├── fakes
│ ├── FakeDataGenerator.kt
│ └── source
│ │ ├── cache
│ │ ├── FakeAccountCacheDataSource.kt
│ │ ├── FakeFeedCacheDataSource.kt
│ │ ├── FakeHistoryCacheDataSource.kt
│ │ └── FakeSearchBlogCacheDataSource.kt
│ │ ├── network
│ │ ├── FakeAccountNetworkDataSource.kt
│ │ ├── FakeAuthNetworkDataSource.kt
│ │ └── FakeBlogNetworkDataSource.kt
│ │ └── preferences
│ │ └── FakePreferences.kt
│ └── repository
│ ├── AccountRepositoryTest.kt
│ ├── AuthRepositoryTest.kt
│ ├── FeedRepositoryTest.kt
│ ├── HistoryRepositoryTest.kt
│ └── SearchBlogRepositoryTest.kt
├── domain
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ └── java
│ └── com
│ └── zlagi
│ └── domain
│ ├── model
│ ├── AccountDomainModel.kt
│ ├── BlogDomainModel.kt
│ ├── HistoryDomainModel.kt
│ ├── PaginatedBlogsDomainModel.kt
│ ├── PaginationDomainModel.kt
│ └── TokensDomainModel.kt
│ ├── repository
│ ├── account
│ │ └── AccountRepository.kt
│ ├── auth
│ │ └── AuthRepository.kt
│ ├── feed
│ │ └── FeedRepository.kt
│ └── search
│ │ ├── SearchBlogRepository.kt
│ │ └── history
│ │ └── HistoryRepository.kt
│ ├── taskmanager
│ ├── TaskManager.kt
│ └── TaskState.kt
│ ├── usecase
│ ├── account
│ │ ├── delete
│ │ │ └── DeleteAccountUseCase.kt
│ │ ├── detail
│ │ │ ├── GetAccountUseCase.kt
│ │ │ └── SyncAccountUseCase.kt
│ │ └── update
│ │ │ └── UpdatePasswordUseCase.kt
│ ├── auth
│ │ ├── deletetokens
│ │ │ └── DeleteTokensUseCase.kt
│ │ ├── signin
│ │ │ ├── email
│ │ │ │ └── SignInUseCase.kt
│ │ │ └── google
│ │ │ │ └── GoogleIdpAuthenticationInUseCase.kt
│ │ ├── signup
│ │ │ └── SignUpUseCase.kt
│ │ └── status
│ │ │ └── AuthenticationStatusUseCase.kt
│ └── blog
│ │ ├── checkauthor
│ │ └── CheckBlogAuthorUseCase.kt
│ │ ├── create
│ │ └── CreateBlogUseCase.kt
│ │ ├── dateformat
│ │ └── DateFormatUseCase.kt
│ │ ├── delete
│ │ └── DeleteBlogUseCase.kt
│ │ ├── detail
│ │ └── GetBlogUseCase.kt
│ │ ├── feed
│ │ ├── GetFeedUseCase.kt
│ │ └── RequestMoreBlogsUseCase.kt
│ │ ├── search
│ │ ├── GetSearchUseCase.kt
│ │ ├── RequestMoreBlogsUseCase.kt
│ │ └── history
│ │ │ ├── ClearHistoryUseCase.kt
│ │ │ ├── DeleteQueryUseCase.kt
│ │ │ ├── GetHistoryUseCase.kt
│ │ │ └── SaveQueryUseCase.kt
│ │ └── update
│ │ └── UpdateBlogUseCase.kt
│ └── validator
│ ├── AuthValidator.kt
│ └── BlogValidator.kt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── local.properties
├── network
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── zlagi
│ │ └── network
│ │ ├── apiservice
│ │ ├── AccountApiService.kt
│ │ ├── AuthApiService.kt
│ │ └── BlogApiService.kt
│ │ ├── connectivity
│ │ └── DefaultConnectivityChecker.kt
│ │ ├── di
│ │ ├── ConnectivityModule.kt
│ │ └── NetworkModule.kt
│ │ ├── interceptor
│ │ └── AuthenticationInterceptor.kt
│ │ ├── mapper
│ │ ├── AccountNetworkDataMapper.kt
│ │ ├── BlogNetworkDataMapper.kt
│ │ ├── PaginationNetworkDataMapper.kt
│ │ └── TokensNetworkDataMapper.kt
│ │ ├── model
│ │ ├── NetworkConstants.kt
│ │ ├── request
│ │ │ ├── GoogleSignInRequest.kt
│ │ │ ├── NotificationRequest.kt
│ │ │ ├── PasswordRequest.kt
│ │ │ ├── SignInRequest.kt
│ │ │ ├── SignUpRequest.kt
│ │ │ ├── UpdateBlogRequest.kt
│ │ │ └── UpdateTokenRequest.kt
│ │ └── response
│ │ │ ├── AccountNetworkModel.kt
│ │ │ ├── BlogNetworkModel.kt
│ │ │ ├── GenericResponse.kt
│ │ │ ├── PaginatedBlogsNetworkModel.kt
│ │ │ └── TokensNetworkModel.kt
│ │ ├── source
│ │ ├── DefaultAccountNetworkDataSource.kt
│ │ ├── DefaultAuthNetworkDataSource.kt
│ │ └── DefaultBlogNetworkDataSource.kt
│ │ └── utils
│ │ └── Extensions.kt
│ └── test
│ └── java
│ └── com
│ └── zlagi
│ └── network
│ ├── di
│ ├── TestConnectivityModule.kt
│ └── TestNetworkModule.kt
│ ├── fakes
│ ├── FakeConnectivityChecker.kt
│ └── FakeDataGenerator.kt
│ ├── model
│ ├── CheckAuthorResponseJson.kt
│ ├── CreateBlogResponseJson.kt
│ ├── DeleteBlogResponseJson.kt
│ ├── ExpiredTokenResponseJson.kt
│ ├── FeedResponseJson.kt
│ ├── GetAccountResponseJson.kt
│ ├── SignInResponseJson.kt
│ ├── SignUpResponseJson.kt
│ ├── UpdateBlogResponseJson.kt
│ └── UpdatePasswordResponseJson.kt
│ ├── source
│ ├── DefaultAccountNetworkDataSourceTest.kt
│ ├── DefaultAuthNetworkDataSourceTest.kt
│ └── DefaultBlogNetworkDataSourceTest.kt
│ └── utils
│ └── Extensions.kt
├── preferences
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ └── java
│ │ └── com
│ │ └── zlagi
│ │ └── preferences
│ │ ├── di
│ │ └── PreferencesModule.kt
│ │ └── source
│ │ └── DefaultPreferencesDataSource.kt
│ └── test
│ └── java
│ └── com
│ └── zlagi
│ └── preferences
│ ├── di
│ └── TestPreferencesModule.kt
│ └── fakes
│ └── FakePreferences.kt
├── presentation
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ ├── main
│ ├── AndroidManifest.xml
│ ├── java
│ │ └── com
│ │ │ └── zlagi
│ │ │ └── presentation
│ │ │ ├── mapper
│ │ │ ├── AccountDomainPresentationMapper.kt
│ │ │ ├── BlogDomainPresentationMapper.kt
│ │ │ └── HistoryDomainPresentationMapper.kt
│ │ │ ├── model
│ │ │ ├── AccountPresentationModel.kt
│ │ │ ├── BlogPresentationModel.kt
│ │ │ └── HistoryPresentationModel.kt
│ │ │ └── viewmodel
│ │ │ ├── account
│ │ │ ├── detail
│ │ │ │ ├── AccountDetailContract.kt
│ │ │ │ └── AccountDetailViewModel.kt
│ │ │ └── update
│ │ │ │ ├── UpdatePasswordContract.kt
│ │ │ │ └── UpdatePasswordViewModel.kt
│ │ │ ├── auth
│ │ │ ├── onboarding
│ │ │ │ ├── OnBoardingContract.kt
│ │ │ │ └── OnBoardingViewModel.kt
│ │ │ ├── signin
│ │ │ │ ├── SignInContract.kt
│ │ │ │ └── SignInViewModel.kt
│ │ │ └── signup
│ │ │ │ ├── SignUpContract.kt
│ │ │ │ └── SignUpViewModel.kt
│ │ │ └── blog
│ │ │ ├── create
│ │ │ ├── CreateBlogContract.kt
│ │ │ └── CreateBlogViewModel.kt
│ │ │ ├── detail
│ │ │ ├── BlogDetailContract.kt
│ │ │ └── BlogDetailViewModel.kt
│ │ │ ├── feed
│ │ │ ├── FeedContract.kt
│ │ │ └── FeedViewModel.kt
│ │ │ ├── search
│ │ │ ├── historyview
│ │ │ │ ├── SearchHistoryContract.kt
│ │ │ │ └── SearchHistoryViewModel.kt
│ │ │ └── resultview
│ │ │ │ ├── SearchResultContract.kt
│ │ │ │ └── SearchResultViewModel.kt
│ │ │ └── update
│ │ │ ├── UpdateBlogContract.kt
│ │ │ └── UpdateBlogViewModel.kt
│ └── res
│ │ ├── drawable
│ │ ├── ic_arrow_left.xml
│ │ └── ic_search.xml
│ │ └── values
│ │ └── strings.xml
│ └── test
│ └── java
│ └── com
│ └── zlagi
│ └── presentation
│ ├── TestCoroutineRule.kt
│ ├── account
│ ├── detail
│ │ └── AccountDetailViewModelTest.kt
│ └── update
│ │ └── UpdatePasswordViewModelTest.kt
│ ├── auth
│ ├── signin
│ │ └── SignInViewModelTest.kt
│ └── signup
│ │ └── SignUpViewModelTest.kt
│ ├── blog
│ ├── create
│ │ └── CreateBlogViewModelTest.kt
│ ├── detail
│ │ └── BlogDetailViewModelTest.kt
│ ├── feed
│ │ └── FeedViewModelTest.kt
│ ├── search
│ │ ├── SearchHistoryViewModelTest.kt
│ │ └── SearchResultViewModelTest.kt
│ └── update
│ │ └── UpdateBlogViewModelTest.kt
│ └── fakes
│ └── FakeDataGenerator.kt
└── settings.gradle
/.github/workflows/Lint.yml:
--------------------------------------------------------------------------------
1 | name: Android Lint
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | lint_job:
8 | name: Lint
9 | runs-on: ubuntu-latest
10 | continue-on-error: true
11 | steps:
12 |
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 |
16 | - name: Restore Cache
17 | uses: actions/cache@v2
18 | with:
19 | path: |
20 | ~/.gradle/caches
21 | ~/.gradle/wrapper
22 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
23 | restore-keys: |
24 | ${{ runner.os }}-gradle-
25 |
26 | - name: Make gradlew executable
27 | run: chmod +x ./gradlew
28 |
29 | - name: Run Debug Lint
30 | run: ./gradlew lintDebug
31 |
32 | - name: Upload Lint Reports
33 | if: ${{ always() }}
34 | uses: actions/upload-artifact@v2
35 | with:
36 | name: lint-report
37 | path: '**/build/reports/lint-results-*'
38 |
39 | report_job:
40 | runs-on: ubuntu-latest
41 | needs: lint_job
42 | if: ${{ always() }}
43 | steps:
44 | - name: Download Test Reports Folder
45 | uses: actions/download-artifact@v2
46 | with:
47 | name: lint-report
48 |
49 | - name: Android Test Report
50 | uses: asadmansr/android-test-report-action@v1.2.0
51 |
--------------------------------------------------------------------------------
/.github/workflows/Release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | apk:
8 | name: Generate APK ⚙️🛠
9 | runs-on: ubuntu-latest
10 | steps:
11 | - name: checkout
12 | uses: actions/checkout@v2
13 |
14 | - name: Set Up JDK 14
15 | uses: actions/setup-java@v1
16 | with:
17 | java-version: 14
18 |
19 | - name: Set execution flag for gradlew
20 | run: chmod +x gradlew
21 |
22 | - name: Build APK
23 | run: bash ./gradlew assembleDebug --stacktrace
24 |
25 | - name: Upload APK
26 | uses: actions/upload-artifact@v1
27 | with:
28 | name: apk
29 | path: app/build/outputs/apk/debug/app-debug.apk
30 |
31 | release:
32 | name: Release APK ✅
33 | needs: apk
34 | runs-on: ubuntu-latest
35 | steps:
36 | - name: Download APK from build
37 | uses: actions/download-artifact@v1
38 | with:
39 | name: apk
40 |
41 | - name: Create Release
42 | id: create_release
43 | uses: actions/create-release@v1
44 | env:
45 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46 | with:
47 | tag_name: ${{ github.run_number }}
48 | release_name: ${{ github.event.repository.name }} v${{ github.run_number }}
49 |
50 | - name: Upload Release APK
51 | id: upload_release_asset
52 | uses: actions/upload-release-asset@v1.0.1
53 | env:
54 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
55 | with:
56 | upload_url: ${{ steps.create_release.outputs.upload_url }}
57 | asset_path: apk/app-debug.apk
58 | asset_name: ${{ github.event.repository.name }}.apk
59 | asset_content_type: application/zip
60 |
--------------------------------------------------------------------------------
/.github/workflows/Testing.yml:
--------------------------------------------------------------------------------
1 | name: Testing
2 |
3 | on:
4 | pull_request:
5 |
6 | jobs:
7 | test_job:
8 | name: Testing
9 | runs-on: [ ubuntu-latest ]
10 | continue-on-error: true
11 | steps:
12 | - name: checkout
13 | uses: actions/checkout@v2
14 |
15 | - name: Set Up JDK 14
16 | uses: actions/setup-java@v1
17 | with:
18 | java-version: 14
19 |
20 | - name: Restore Cache
21 | uses: actions/cache@v2
22 | with:
23 | path: |
24 | ~/.gradle/caches
25 | ~/.gradle/wrapper
26 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
27 | restore-keys: |
28 | ${{ runner.os }}-gradle-
29 |
30 | - name: Make gradlew executable
31 | run: chmod +x ./gradlew
32 |
33 | - name: Run Tests
34 | run: ./gradlew test --stacktrace
35 |
36 | - name: Upload Test Reports
37 | if: ${{ always() }}
38 | uses: actions/upload-artifact@v2
39 | with:
40 | name: unit-tests-reports
41 | path: '**/build/reports/tests/'
42 |
43 | report:
44 | runs-on: ubuntu-latest
45 | needs: test_job
46 | if: ${{ always() }}
47 | steps:
48 | - name: Download Test Reports Folder
49 | uses: actions/download-artifact@v2
50 | with:
51 | name: unit-tests-reports
52 |
53 | - name: Android Test Report
54 | uses: asadmansr/android-test-report-action@v1.2.0
55 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /.idea/caches
4 | /.idea/libraries
5 | /.idea/modules.xml
6 | /.idea/workspace.xml
7 | /.idea/navEditor.xml
8 | /.idea/assetWizardSettings.xml
9 | .DS_Store
10 | /build
11 | /captures
12 | .externalNativeBuild
13 | .cxx
14 | # Project exclude paths
15 | /buildSrc/build/
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | blogfy
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
28 |
29 |
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/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.
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/java/com/zlagi/blogfy/BlogfyApplication.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy
2 |
3 | import android.app.Application
4 | import android.util.Log
5 | import androidx.hilt.work.HiltWorkerFactory
6 | import androidx.work.Configuration
7 | import com.onesignal.OneSignal
8 | import dagger.hilt.android.HiltAndroidApp
9 | import javax.inject.Inject
10 |
11 | /**
12 | * Core Application Class
13 | */
14 | @HiltAndroidApp
15 | class BlogfyApplication : Application(), Configuration.Provider {
16 |
17 | @Inject
18 | lateinit var workerFactory: HiltWorkerFactory
19 |
20 | override fun onCreate() {
21 | super.onCreate()
22 |
23 | OneSignal.initWithContext(this)
24 | OneSignal.setAppId(ONESIGNAL_APP_ID)
25 | }
26 |
27 | companion object {
28 | private const val ONESIGNAL_APP_ID = "6679eba8-ba98-43da-ba87-9a9c7457bd33"
29 | }
30 |
31 | override fun getWorkManagerConfiguration() =
32 | Configuration.Builder()
33 | .setWorkerFactory(workerFactory)
34 | .setMinimumLoggingLevel(Log.DEBUG)
35 | .build()
36 | }
37 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/di/DispatcherModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.di
2 |
3 | import com.zlagi.common.qualifier.DefaultDispatcher
4 | import com.zlagi.common.qualifier.IoDispatcher
5 | import com.zlagi.common.qualifier.MainDispatcher
6 | import dagger.Module
7 | import dagger.Provides
8 | import dagger.hilt.InstallIn
9 | import dagger.hilt.components.SingletonComponent
10 | import kotlinx.coroutines.CoroutineDispatcher
11 | import kotlinx.coroutines.Dispatchers
12 |
13 | @Module
14 | @InstallIn(SingletonComponent::class)
15 | object DispatcherModule {
16 |
17 | @DefaultDispatcher
18 | @Provides
19 | internal fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
20 |
21 | @IoDispatcher
22 | @Provides
23 | internal fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO
24 |
25 | @MainDispatcher
26 | @Provides
27 | internal fun providesMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
28 | }
29 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/di/ImageLoaderModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.di
2 |
3 | import android.content.Context
4 | import coil.ImageLoader
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.android.qualifiers.ApplicationContext
9 | import dagger.hilt.components.SingletonComponent
10 | import javax.inject.Singleton
11 |
12 | @Module
13 | @InstallIn(SingletonComponent::class)
14 |
15 | object ImageLoaderModule {
16 |
17 | @Provides
18 | @Singleton
19 | fun provideImageLoader(
20 | @ApplicationContext context: Context
21 | ): ImageLoader {
22 | return ImageLoader.Builder(context).build()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/di/TaskManagerModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.di
2 |
3 | import com.zlagi.data.taskmanager.DefaultTaskManager
4 | import com.zlagi.domain.taskmanager.TaskManager
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @Module
11 | @InstallIn(SingletonComponent::class)
12 | abstract class TaskManagerModule {
13 |
14 | @Binds
15 | abstract fun provideTaskManager(defaultTaskManager: DefaultTaskManager): TaskManager
16 | }
17 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/di/WorkerModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.di
2 |
3 | import android.app.Application
4 | import androidx.work.WorkManager
5 | import dagger.Module
6 | import dagger.Provides
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | object WorkerModule {
14 |
15 | @Singleton
16 | @Provides
17 | fun provideWorkManager(application: Application): WorkManager {
18 | return WorkManager.getInstance(application)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/view/auth/AuthActivity.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.view.auth
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.navigation.NavController
7 | import androidx.navigation.fragment.NavHostFragment
8 | import com.zlagi.blogfy.R
9 | import com.zlagi.blogfy.databinding.ActivityAuthBinding
10 | import com.zlagi.blogfy.view.MainActivity
11 | import dagger.hilt.android.AndroidEntryPoint
12 |
13 | @AndroidEntryPoint
14 | class AuthActivity : AppCompatActivity() {
15 |
16 | private lateinit var binding: ActivityAuthBinding
17 |
18 | private lateinit var navController: NavController
19 |
20 | override fun onCreate(savedInstanceState: Bundle?) {
21 | super.onCreate(savedInstanceState)
22 | setTheme(R.style.Theme_AuthBlogfy)
23 | binding = ActivityAuthBinding.inflate(layoutInflater)
24 | setContentView(binding.root)
25 | setupNavigationController()
26 | setupActionBar()
27 | }
28 |
29 | private fun setupNavigationController() {
30 | val navHostFragment =
31 | supportFragmentManager.findFragmentById(R.id.auth_nav_host_container) as NavHostFragment
32 | navController = navHostFragment.navController
33 | }
34 |
35 | fun navMainActivity(){
36 | val intent = Intent(this, MainActivity::class.java)
37 | intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
38 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
39 | startActivity(intent)
40 | finish()
41 | }
42 |
43 | private fun setupActionBar() {
44 | supportActionBar?.hide()
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/view/blog/feed/FeedAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.view.blog.feed
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.DiffUtil
6 | import androidx.recyclerview.widget.ListAdapter
7 | import com.google.firebase.storage.FirebaseStorage
8 | import com.zlagi.blogfy.databinding.FeedItemLayoutBinding
9 | import com.zlagi.presentation.model.BlogPresentationModel
10 |
11 |
12 | class FeedAdapter(
13 | private val firebaseStorage: FirebaseStorage,
14 | private val firestoreImageBucketUrl: String,
15 | private val listener: OnItemSelectedListener = { _, _ -> }
16 | ) : ListAdapter(DIFF_UTIL) {
17 |
18 | override fun onCreateViewHolder(
19 | parent: ViewGroup,
20 | viewType: Int
21 | ): FeedViewHolder {
22 | val binding = FeedItemLayoutBinding.inflate(
23 | LayoutInflater.from(parent.context),
24 | parent,
25 | false
26 | )
27 | return FeedViewHolder(binding, listener)
28 | }
29 |
30 | override fun onBindViewHolder(holder: FeedViewHolder, position: Int) {
31 | holder.bind(position, getItem(position), firebaseStorage, firestoreImageBucketUrl)
32 | }
33 | }
34 |
35 | private val DIFF_UTIL = object : DiffUtil.ItemCallback() {
36 | override fun areItemsTheSame(
37 | oldItem: BlogPresentationModel,
38 | newItem: BlogPresentationModel
39 | ) =
40 | oldItem.pk == newItem.pk
41 |
42 | override fun areContentsTheSame(
43 | oldItem: BlogPresentationModel,
44 | newItem: BlogPresentationModel
45 | ) =
46 | oldItem == newItem
47 | }
48 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/view/blog/feed/OnItemSelectedListener.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.view.blog.feed
2 |
3 | /** Item selection Listener */
4 | typealias OnItemSelectedListener = (position: Int, item: T) -> Unit
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/view/utils/BottomSpacingItemDecoration.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.view.utils
2 |
3 | import android.graphics.Rect
4 | import android.view.View
5 | import androidx.annotation.Px
6 | import androidx.recyclerview.widget.RecyclerView
7 |
8 | /**
9 | * A [RecyclerView.ItemDecoration] which adds the given `spacing` to the bottom of any view,
10 | * unless it is the last item.
11 | */
12 | class BottomSpacingItemDecoration(
13 | @Px private val spacing: Int
14 | ) : RecyclerView.ItemDecoration() {
15 | override fun getItemOffsets(
16 | outRect: Rect,
17 | view: View,
18 | parent: RecyclerView,
19 | state: RecyclerView.State
20 | ) {
21 | val lastItem = parent.getChildAdapterPosition(view) == state.itemCount - 1
22 | outRect.bottom = if (!lastItem) spacing else 0
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/src/main/java/com/zlagi/blogfy/view/utils/LoadingDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.blogfy.view.utils
2 |
3 | import android.app.AlertDialog
4 | import android.app.Dialog
5 | import android.os.Bundle
6 | import android.view.LayoutInflater
7 | import android.view.View
8 | import android.view.ViewGroup
9 | import androidx.appcompat.app.AppCompatDialogFragment
10 | import com.zlagi.blogfy.R
11 | import com.zlagi.blogfy.databinding.FragmentLoadingDialogBinding
12 | import dagger.hilt.android.AndroidEntryPoint
13 |
14 | @AndroidEntryPoint
15 | class LoadingDialogFragment : AppCompatDialogFragment() {
16 |
17 | private var _binding: FragmentLoadingDialogBinding? = null
18 | private val binding get() = _binding!!
19 |
20 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
21 |
22 | if (savedInstanceState != null) dismiss()
23 | _binding = FragmentLoadingDialogBinding.inflate(LayoutInflater.from(context)).apply {
24 | textView.setText(R.string.dialog_loading)
25 | }
26 | return AlertDialog.Builder(requireActivity(), R.style.MyCustomTheme)
27 | .setView(binding.root)
28 | .setCancelable(true)
29 | .create()
30 | }
31 |
32 | override fun onCreateView(
33 | inflater: LayoutInflater,
34 | container: ViewGroup?,
35 | savedInstanceState: Bundle?
36 | ): View {
37 | _binding = FragmentLoadingDialogBinding.inflate(inflater, container, false)
38 | return binding.root
39 | }
40 |
41 | override fun onStart() {
42 | super.onStart()
43 | dialog?.setCancelable(true)
44 | }
45 |
46 | override fun onDestroy() {
47 | super.onDestroy()
48 | _binding = null
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/fade_in.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_down.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_right.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/curved_toolbar.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
4 |
5 |
6 |
8 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_baseline_add_photo_alternate_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_baseline_delete_forever_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_check_green_24dp.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_edit_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_user.xml:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 |
29 |
36 |
37 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/drawable-v24/logo.png
--------------------------------------------------------------------------------
/app/src/main/res/drawable/blog_detail_image_scream.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
18 |
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/button_shape.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/divider.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/email_button_shape.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_arrow_left.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_back_arrow.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
13 |
14 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_clear_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_home_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_lock_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_mail_outline_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_more_vert_24.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_person_24.xml:
--------------------------------------------------------------------------------
1 |
7 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_settings_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_google.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
12 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_home_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_nav_bottom_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_outline_home_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_outline_search_24.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_settings_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/small_component_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
18 | -
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/splash_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
7 |
--------------------------------------------------------------------------------
/app/src/main/res/font/gilroy_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/gilroy_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/gilroy_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/gilroy_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/gilroy_semibold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/gilroy_semibold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/opensans_bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/opensans_bold.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/opensans_light.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/opensans_light.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/opensans_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/opensans_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/poppins_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/poppins_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/poppins_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_medium.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/raleway_medium.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/raleway_regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/raleway_regular.ttf
--------------------------------------------------------------------------------
/app/src/main/res/font/universal_std.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/font/universal_std.otf
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_auth.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/fragment_loading_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/layout_search_result.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
21 |
22 |
33 |
34 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/menu_bottom_sheet_dialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/app/src/main/res/menu-v26/blog_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/blog_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/bottom_nav.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/feed.xml:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_account.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_blog_detail.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
21 |
22 |
23 |
28 |
31 |
34 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_feed.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
16 |
17 |
20 |
21 |
22 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_graph.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app/src/main/res/navigation/nav_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
16 |
17 |
18 |
23 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #3E6275
4 | #7d7d7d
5 | #FF000000
6 | #C0424242
7 | #424242
8 | #000000
9 | #005DF2
10 | #0000EE
11 | #9FDAF7
12 | #25C685
13 | #FAFAFA
14 | #e22b2b
15 | #B00020
16 | #08000000
17 | #78000000
18 | #98000000
19 | #c4c4c4
20 | #9a9a9a
21 | #5c5c5c
22 | #30444E
23 |
24 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0dp
4 | 1dp
5 | 2dp
6 | 4dp
7 | 8dp
8 | 12dp
9 | 16dp
10 | 24dp
11 | 32dp
12 | 40dp
13 | 48dp
14 | 64dp
15 | 172dp
16 | 256dp
17 |
18 | 12sp
19 | 14sp
20 | 16sp
21 | 20sp
22 | 24sp
23 | 32sp
24 | 28sp
25 |
26 | 0.5
27 | 28dp
28 |
29 |
--------------------------------------------------------------------------------
/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/values/motions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 300
7 |
8 |
--------------------------------------------------------------------------------
/app/src/main/res/values/type.xml:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 |
3 | repositories {
4 | google()
5 | mavenCentral()
6 | gradlePluginPortal()
7 | }
8 |
9 | dependencies {
10 | classpath "com.android.tools.build:gradle:7.0.4"
11 | classpath "com.google.gms:google-services:${BuildPluginsVersion.FIREBASE_GMS}"
12 | classpath "com.google.firebase:firebase-crashlytics-gradle:${BuildPluginsVersion.FIREBASE_CRASHLYTICS}"
13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}"
14 | classpath "gradle.plugin.com.onesignal:onesignal-gradle-plugin:${BuildPluginsVersion.ONESIGNAL}"
15 | classpath "io.gitlab.arturbosch.detekt:detekt-gradle-plugin:${BuildPluginsVersion.DETEKT}"
16 | classpath "com.google.dagger:hilt-android-gradle-plugin:${Versions.DAGGER_HILT}"
17 | classpath "androidx.navigation:navigation-safe-args-gradle-plugin:${Versions.NAVIGATION}"
18 | }
19 | }
20 |
21 | subprojects {
22 | apply from: "../buildscripts/detekt.gradle"
23 | }
24 |
25 | task clean(type: Delete) {
26 | delete rootProject.buildDir
27 | }
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.gradle.kotlin.dsl.`kotlin-dsl`
2 |
3 | plugins {
4 | `kotlin-dsl`
5 | }
6 |
7 |
8 | repositories {
9 | mavenCentral()
10 | }
--------------------------------------------------------------------------------
/buildSrc/src/main/java/SDKConfig.kt:
--------------------------------------------------------------------------------
1 | object SDKConfig {
2 | const val applicationId = "com.zlagi.blogfy"
3 | const val compileSdkVersion = 31
4 | const val buildToolsVersion = "30.0.3"
5 | const val minSdkVersion = 23
6 | const val targetSdkVersion = 31
7 | const val versionCode = 1
8 | const val versionName = "1.0.0"
9 | const val TestInstrumentationRunner = "com.zlagi.blogfy.HiltTestRunner"
10 | }
--------------------------------------------------------------------------------
/buildscripts/detekt.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "io.gitlab.arturbosch.detekt"
2 |
3 | detekt {
4 | config = files("${rootProject.projectDir}/config/detekt/detekt.yml")
5 |
6 | reports {
7 | html.enabled = true
8 | xml.enabled = true
9 | txt.enabled = true
10 | }
11 | }
--------------------------------------------------------------------------------
/buildscripts/ktlint.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: "org.jlleitschuh.gradle.ktlint"
2 |
3 | ktlint {
4 | // https://github.com/pinterest/ktlint/releases
5 | version = "0.43.0"
6 |
7 | reporters {
8 | reporter "plain"
9 | reporter "checkstyle"
10 | reporter "html"
11 | }
12 |
13 | outputColorName = "RED"
14 | }
--------------------------------------------------------------------------------
/cache/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/cache/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/cache/src/androidTest/java/com/zlagi/cache/HiltTestRunner.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache
2 | import android.app.Application
3 | import android.content.Context
4 | import androidx.test.runner.AndroidJUnitRunner
5 | import dagger.hilt.android.testing.HiltTestApplication
6 |
7 | class HiltTestRunner : AndroidJUnitRunner() {
8 |
9 | override fun newApplication(
10 | cl: ClassLoader?,
11 | className: String?,
12 | context: Context?
13 | ): Application {
14 | return super.newApplication(cl, HiltTestApplication::class.java.name, context)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/cache/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/database/BlogfyDatabase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.database
2 |
3 | import androidx.room.Database
4 | import androidx.room.RoomDatabase
5 | import com.zlagi.cache.database.account.AccountDao
6 | import com.zlagi.cache.database.feed.FeedDao
7 | import com.zlagi.cache.database.search.SearchDao
8 | import com.zlagi.cache.database.search.history.HistoryDao
9 | import com.zlagi.cache.model.AccountCacheModel
10 | import com.zlagi.cache.model.FeedBlogCacheModel
11 | import com.zlagi.cache.model.SearchBlogCacheModel
12 | import com.zlagi.cache.model.HistoryCacheModel
13 |
14 | @Database(
15 | entities = [
16 | FeedBlogCacheModel::class,
17 | SearchBlogCacheModel::class,
18 | AccountCacheModel::class,
19 | HistoryCacheModel::class
20 | ],
21 | version = 1
22 | )
23 | abstract class BlogfyDatabase : RoomDatabase() {
24 | abstract fun feedDao(): FeedDao
25 | abstract fun searchDao(): SearchDao
26 | abstract fun historyDao(): HistoryDao
27 | abstract fun accountDao(): AccountDao
28 | }
29 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/database/account/AccountDao.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.database.account
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.zlagi.cache.model.AccountCacheModel
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface AccountDao {
12 |
13 | @Query(
14 | """
15 | SELECT * FROM account
16 | """
17 | )
18 | fun fetchAccount(): Flow
19 |
20 | @Insert(onConflict = OnConflictStrategy.REPLACE)
21 | suspend fun storeAccount(accountCacheModel: AccountCacheModel)
22 |
23 | @Query(
24 | """
25 | DELETE FROM account
26 | """
27 | )
28 | suspend fun deleteAccount()
29 | }
30 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/database/feed/FeedDao.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.database.feed
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.zlagi.cache.model.FeedBlogCacheModel
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface FeedDao {
12 |
13 | @Query(
14 | """
15 | SELECT * FROM feed
16 | WHERE title LIKE '%' || :searchQuery || '%'
17 | ORDER BY pk ASC
18 | """
19 | )
20 | fun fetchBlogsOrderByTitleDESC(
21 | searchQuery: String
22 | ): Flow>
23 |
24 | @Insert(onConflict = OnConflictStrategy.REPLACE)
25 | suspend fun storeBlogs(feedBlogCacheModels: List)
26 |
27 | @Query("SELECT * FROM feed WHERE pk = :blogPK")
28 | fun fetchBlog(blogPK: Int): Flow
29 |
30 | @Insert(onConflict = OnConflictStrategy.REPLACE)
31 | suspend fun storeBlog(feedBlogCacheModel: FeedBlogCacheModel)
32 |
33 | @Query(
34 | """
35 | UPDATE feed SET title = :blogTitle, description = :blogDescription, updated = :updated
36 | WHERE pk = :blogPk
37 | """
38 | )
39 | suspend fun updateBlog(blogPk: Int, blogTitle: String, blogDescription: String, updated: String)
40 |
41 | @Query("DELETE FROM feed WHERE pk = :blogPk")
42 | suspend fun deleteBlog(blogPk: Int)
43 |
44 | @Query(
45 | """
46 | DELETE FROM feed
47 | """
48 | )
49 | suspend fun deleteAllBlogs()
50 | }
51 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/database/search/SearchDao.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.database.search
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.zlagi.cache.model.SearchBlogCacheModel
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface SearchDao {
12 |
13 | @Query(
14 | """
15 | SELECT * FROM search_blog
16 | WHERE title LIKE '%' || :searchQuery || '%'
17 | ORDER BY pk ASC
18 | """
19 | )
20 | fun fetchBlogsOrderByTitleDESC(
21 | searchQuery: String
22 | ): Flow>
23 |
24 | @Insert(onConflict = OnConflictStrategy.REPLACE)
25 | suspend fun storeBlogs(blogCacheModels: List)
26 |
27 | @Query("DELETE FROM search_blog WHERE pk = :blogPk")
28 | suspend fun deleteBlog(blogPk: Int)
29 |
30 | @Query(
31 | """
32 | DELETE FROM search_blog
33 | """
34 | )
35 | suspend fun deleteAllBlogs()
36 | }
37 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/database/search/history/HistoryDao.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.database.search.history
2 |
3 | import androidx.room.Dao
4 | import androidx.room.Insert
5 | import androidx.room.OnConflictStrategy
6 | import androidx.room.Query
7 | import com.zlagi.cache.model.HistoryCacheModel
8 | import kotlinx.coroutines.flow.Flow
9 |
10 | @Dao
11 | interface HistoryDao {
12 |
13 | @Query(
14 | """
15 | SELECT * FROM history
16 | """
17 | )
18 | fun getHistory(): Flow>
19 |
20 | @Insert(onConflict = OnConflictStrategy.REPLACE)
21 | suspend fun saveQuery(item: HistoryCacheModel)
22 |
23 | @Query(
24 | """
25 | Delete from history WHERE `query` LIKE '%' || :query
26 | """
27 | )
28 | suspend fun deleteQuery(query: String)
29 |
30 | @Query(
31 | """
32 | DELETE from history
33 | """
34 | )
35 | suspend fun clearHistory()
36 | }
37 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/mapper/AccountCacheDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.mapper
2 |
3 | import com.zlagi.cache.model.AccountCacheModel
4 | import com.zlagi.common.mapper.Mapper
5 | import com.zlagi.data.model.AccountDataModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [AccountCacheModel] to [AccountDataModel] and vice versa
10 | */
11 | class AccountCacheDataMapper @Inject constructor() :
12 | Mapper {
13 |
14 | override fun from(i: AccountCacheModel): AccountDataModel {
15 | return AccountDataModel(
16 | pk = i.pk,
17 | email = i.email,
18 | username = i.username
19 | )
20 | }
21 |
22 | override fun to(o: AccountDataModel): AccountCacheModel {
23 | return AccountCacheModel(
24 | pk = o.pk,
25 | email = o.email,
26 | username = o.username
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/mapper/FeedBlogCacheDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.mapper
2 |
3 | import com.zlagi.cache.model.FeedBlogCacheModel
4 | import com.zlagi.common.mapper.Mapper
5 | import com.zlagi.data.model.BlogDataModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [FeedBlogCacheModel] to [BlogDataModel] and vice versa
10 | */
11 | class FeedBlogCacheDataMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: FeedBlogCacheModel): BlogDataModel {
14 | return BlogDataModel(
15 | pk = i.pk,
16 | title = i.title,
17 | description = i.description,
18 | created = i.created,
19 | updated = i.updated,
20 | username = i.username
21 | )
22 | }
23 |
24 | override fun to(o: BlogDataModel): FeedBlogCacheModel {
25 | return FeedBlogCacheModel(
26 | pk = o.pk,
27 | title = o.title,
28 | description = o.description,
29 | created = o.created,
30 | updated = o.updated,
31 | username = o.username
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/mapper/HistoryCacheDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.mapper
2 |
3 | import com.zlagi.cache.model.HistoryCacheModel
4 | import com.zlagi.common.mapper.Mapper
5 | import com.zlagi.data.model.HistoryDataModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [HistoryCacheModel] to [HistoryDataModel] and vice versa
10 | */
11 | class HistoryCacheDataMapper @Inject constructor() :
12 | Mapper {
13 |
14 | override fun from(i: HistoryCacheModel): HistoryDataModel {
15 | return HistoryDataModel(
16 | query = i.query
17 | )
18 | }
19 |
20 | override fun to(o: HistoryDataModel): HistoryCacheModel {
21 | return HistoryCacheModel(
22 | query = o.query
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/mapper/SearchBlogCacheDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.mapper
2 |
3 | import com.zlagi.cache.model.SearchBlogCacheModel
4 | import com.zlagi.common.mapper.Mapper
5 | import com.zlagi.data.model.BlogDataModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [SearchBlogCacheModel] to [BlogDataModel] and vice versa
10 | */
11 | class SearchBlogCacheDataMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: SearchBlogCacheModel): BlogDataModel {
14 | return BlogDataModel(
15 | pk = i.pk,
16 | title = i.title,
17 | description = i.description,
18 | created = i.created,
19 | updated = i.updated,
20 | username = i.username
21 | )
22 | }
23 |
24 | override fun to(o: BlogDataModel): SearchBlogCacheModel {
25 | return SearchBlogCacheModel(
26 | pk = o.pk,
27 | title = o.title,
28 | description = o.description,
29 | created = o.created,
30 | updated = o.updated,
31 | username = o.username
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/model/AccountCacheModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "account")
7 | data class AccountCacheModel(
8 | @PrimaryKey(autoGenerate = false)
9 | val pk: Int,
10 | val email: String,
11 | val username: String
12 | )
13 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/model/FeedBlogCacheModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "feed")
7 | data class FeedBlogCacheModel(
8 | @PrimaryKey(autoGenerate = false)
9 | val pk: Int,
10 | val title: String,
11 | val description: String,
12 | val created: String,
13 | val updated: String,
14 | val username: String
15 | )
16 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/model/HistoryCacheModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "history")
7 | data class HistoryCacheModel(
8 | @PrimaryKey(autoGenerate = false)
9 | val query: String
10 | )
11 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/model/SearchBlogCacheModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.model
2 |
3 | import androidx.room.Entity
4 | import androidx.room.PrimaryKey
5 |
6 | @Entity(tableName = "search_blog")
7 | data class SearchBlogCacheModel(
8 | @PrimaryKey(autoGenerate = false)
9 | val pk: Int,
10 | val title: String,
11 | val description: String,
12 | val created: String,
13 | val updated: String,
14 | val username: String
15 | )
16 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/source/account/DefaultAccountCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.source.account
2 |
3 | import com.zlagi.cache.database.account.AccountDao
4 | import com.zlagi.cache.mapper.AccountCacheDataMapper
5 | import com.zlagi.data.model.AccountDataModel
6 | import com.zlagi.data.source.cache.account.AccountCacheDataSource
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 | import javax.inject.Inject
10 |
11 | class DefaultAccountCacheDataSource @Inject constructor(
12 | private val accountDao: AccountDao,
13 | private val accountCacheDataMapper: AccountCacheDataMapper,
14 | ) : AccountCacheDataSource {
15 |
16 | override fun fetchAccount(): Flow =
17 | accountDao.fetchAccount().map {
18 | accountCacheDataMapper.from(it)
19 | }
20 |
21 | override suspend fun storeAccount(account: AccountDataModel) {
22 | accountCacheDataMapper.to(account).let {
23 | accountDao.storeAccount(it)
24 | }
25 | }
26 |
27 | override suspend fun deleteAccount() {
28 | accountDao.deleteAccount()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/source/search/DefaultSearchBlogCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.source.search
2 |
3 | import com.zlagi.cache.database.search.SearchDao
4 | import com.zlagi.cache.mapper.SearchBlogCacheDataMapper
5 | import com.zlagi.data.model.BlogDataModel
6 | import com.zlagi.data.source.cache.search.SearchBlogCacheDataSource
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 | import javax.inject.Inject
10 |
11 | class DefaultSearchBlogCacheDataSource @Inject constructor(
12 | private val searchDao: SearchDao,
13 | private val searchBlogCacheDataMapper: SearchBlogCacheDataMapper,
14 | ) : SearchBlogCacheDataSource {
15 |
16 | override fun fetchBlogs(searchQuery: String): Flow> =
17 | searchDao.fetchBlogsOrderByTitleDESC(searchQuery).map {
18 | searchBlogCacheDataMapper.fromList(it)
19 | }
20 |
21 | override suspend fun storeBlogs(blogList: List) {
22 | searchBlogCacheDataMapper.toList(blogList).let {
23 | searchDao.storeBlogs(it)
24 | }
25 | }
26 |
27 | override suspend fun deleteAllBlogs() {
28 | searchDao.deleteAllBlogs()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/cache/src/main/java/com/zlagi/cache/source/search/history/DefaultHistoryCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.cache.source.search.history
2 |
3 | import com.zlagi.cache.database.search.history.HistoryDao
4 | import com.zlagi.cache.mapper.HistoryCacheDataMapper
5 | import com.zlagi.data.model.HistoryDataModel
6 | import com.zlagi.data.source.cache.search.history.HistoryCacheDataSource
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 | import javax.inject.Inject
10 |
11 | class DefaultHistoryCacheDataSource @Inject constructor(
12 | private val historyDao: HistoryDao,
13 | private val historyCacheDataMapper: HistoryCacheDataMapper,
14 | ) : HistoryCacheDataSource {
15 |
16 | override fun getHistory(): Flow> =
17 | historyDao.getHistory().map {
18 | historyCacheDataMapper.fromList(it)
19 | }
20 |
21 | override suspend fun saveQuery(item: HistoryDataModel) {
22 | historyCacheDataMapper.to(item).let {
23 | historyDao.saveQuery(it)
24 | }
25 | }
26 |
27 | override suspend fun deleteQuery(query: String) {
28 | historyDao.deleteQuery(query)
29 | }
30 |
31 | override suspend fun clearHistory() {
32 | historyDao.clearHistory()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/common/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/common/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 |
9 | compileSdkVersion SDKConfig.compileSdkVersion
10 | buildToolsVersion SDKConfig.buildToolsVersion
11 |
12 | defaultConfig {
13 | minSdkVersion SDKConfig.minSdkVersion
14 | targetSdkVersion SDKConfig.targetSdkVersion
15 | }
16 |
17 | compileOptions {
18 | sourceCompatibility JavaVersion.VERSION_1_8
19 | targetCompatibility JavaVersion.VERSION_1_8
20 | }
21 |
22 | kotlinOptions {
23 | jvmTarget = JavaVersion.VERSION_1_8
24 | }
25 |
26 | }
27 |
28 | dependencies {
29 |
30 | // Retrofit
31 | implementation Deps.RETROFIT
32 |
33 | // Moshi
34 | implementation Deps.MOSHI
35 |
36 | // Firebase
37 | implementation Deps.FIREBASE_AUTH
38 | implementation Deps.FIREBASE_AUTH_KTX
39 |
40 | // Coroutines
41 | implementation Deps.COROUTINES_CORE
42 | implementation Deps.COROUTINES_ANDROID
43 |
44 | // DI
45 | implementation Deps.INJECT
46 |
47 | // Kotlin reflection
48 | implementation Deps.REFLECT
49 | }
--------------------------------------------------------------------------------
/common/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/common/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/exception/CacheException.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.exception
2 |
3 | sealed class CacheException: Exception() {
4 | object NoResults : NetworkException()
5 | }
6 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/exception/MappingException.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.exception
2 |
3 | class MappingException(message: String) : Exception(message)
4 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/exception/NetworkException.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.exception
2 |
3 | sealed class NetworkException : Exception() {
4 | object NetworkUnavailable : NetworkException()
5 | object Network : NetworkException()
6 | object NotFound : NetworkException()
7 | object BadRequest : NetworkException()
8 | object NotAuthorized : NetworkException()
9 | object ServiceNotWorking : NetworkException()
10 | object ServiceUnavailable : NetworkException()
11 | object NoResults : NetworkException()
12 | object Unknown : NetworkException()
13 | object UnknownError : NetworkException()
14 | }
15 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/mapper/ExceptionMessageMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.mapper
2 |
3 | import com.zlagi.common.R
4 | import com.zlagi.common.exception.NetworkException
5 |
6 | fun Throwable.getStringResId(): Int {
7 | return when (this) {
8 | is NetworkException.NetworkUnavailable -> R.string.network_unavailable_message
9 | is NetworkException.NotFound -> R.string.unknown_network_error_message
10 | is NetworkException.BadRequest -> R.string.email_unavailable_message
11 | is NetworkException.Network -> R.string.server_unreachable_message
12 | is NetworkException.NotAuthorized -> R.string.invalid_credentials_message
13 | is NetworkException.NoResults -> R.string.no_more_results
14 | is NetworkException.ServiceNotWorking,
15 | is NetworkException.ServiceUnavailable -> R.string.service_unavailable_message
16 | is NetworkException.Unknown -> R.string.unknown_network_error_message
17 | is UnknownError -> R.string.unknown_error_message
18 | else -> R.string.unknown_error_message
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/mapper/Mapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.mapper
2 |
3 | interface Mapper {
4 |
5 | fun from(i: I): O
6 |
7 | fun to(o: O): I
8 |
9 | fun fromList(list: List): List {
10 | return list.mapNotNull { from(it) }
11 | }
12 |
13 | fun toList(list: List): List {
14 | return list.mapNotNull { to(it) }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/qualifier/DefaultDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.qualifier
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Retention(AnnotationRetention.BINARY)
6 | @Qualifier
7 | annotation class DefaultDispatcher
8 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/qualifier/IoDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.qualifier
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Retention(AnnotationRetention.BINARY)
6 | @Qualifier
7 | annotation class IoDispatcher
8 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/qualifier/MainDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.qualifier
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Retention(AnnotationRetention.BINARY)
6 | @Qualifier
7 | annotation class MainDispatcher
8 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/AuthError.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils
2 |
3 | sealed class AuthError : Error() {
4 | object EmptyField : AuthError()
5 | object InputTooShort : AuthError()
6 | object InvalidEmail : AuthError()
7 | object InvalidUsername : AuthError()
8 | object InvalidPassword : AuthError()
9 | object UnmatchedPassword : AuthError()
10 | }
11 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/BlogError.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils
2 |
3 | sealed class BlogError : Error() {
4 | object InputTooShort : BlogError()
5 | object EmptyField : BlogError()
6 | }
7 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/Constants.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils
2 |
3 | object Constants {
4 | const val HAVE_PERMISSION = "You have permission to edit that"
5 | const val HAVE_NO_PERMISSION = "You don't have permission to edit that"
6 | const val PASSWORD_UPDATED = "Password updated"
7 | const val FIREBASE_IMAGE_BUCKET = "images"
8 | }
9 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/PreferencesConstants.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils
2 |
3 | object PreferencesConstants {
4 | const val KEY_ACCESS_TOKEN = "accessToken"
5 | const val KEY_REFRESH_TOKEN = "refreshToken"
6 | }
7 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/result/SignInResult.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils.result
2 |
3 | import com.zlagi.common.utils.AuthError
4 | import com.zlagi.common.utils.wrapper.SimpleResource
5 |
6 | data class SignInResult(
7 | val emailError: AuthError? = null,
8 | val passwordError: AuthError? = null,
9 | val result: SimpleResource? = null
10 | )
11 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/result/SignUpResult.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils.result
2 |
3 | import com.zlagi.common.utils.AuthError
4 | import com.zlagi.common.utils.wrapper.SimpleResource
5 |
6 | data class SignUpResult(
7 | val emailError: AuthError? = null,
8 | val usernameError: AuthError? = null,
9 | val passwordError: AuthError? = null,
10 | val confirmPasswordError: AuthError? = null,
11 | val result: SimpleResource? = null
12 | )
13 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/result/UpdateBlogResult.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils.result
2 |
3 | import com.zlagi.common.utils.BlogError
4 | import com.zlagi.common.utils.wrapper.SimpleResource
5 |
6 | data class UpdateBlogResult(
7 | val titleError: BlogError? = null,
8 | val descriptionError: BlogError? = null,
9 | val uriError: BlogError? = null,
10 | val result: SimpleResource? = null
11 | )
12 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/result/UpdatePasswordResult.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils.result
2 |
3 | import com.zlagi.common.utils.AuthError
4 | import com.zlagi.common.utils.wrapper.SimpleResource
5 |
6 | data class UpdatePasswordResult(
7 | val currentPasswordError: AuthError? = null,
8 | val newPasswordError: AuthError? = null,
9 | val confirmPasswordError: AuthError? = null,
10 | val result: SimpleResource? = null
11 | )
12 |
--------------------------------------------------------------------------------
/common/src/main/java/com/zlagi/common/utils/wrapper/DataResult.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.common.utils.wrapper
2 |
3 | typealias SimpleResource = DataResult
4 |
5 | sealed class DataResult {
6 | class Success(val data: T) : DataResult()
7 | data class Error(val exception: Exception) : DataResult()
8 | }
9 |
--------------------------------------------------------------------------------
/common/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Please check your internet connection and try again.
3 | Email already taken.
4 | Invalid credentials️.
5 | Something went wrong, please try again later…
6 | Something unexpected happened, please try again later…
7 | Service unavailable, please try again later…
8 | Server unreachable, please try again later…
9 | No results found :(
10 |
--------------------------------------------------------------------------------
/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/data/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/connectivity/ConnectivityChecker.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.connectivity
2 |
3 | interface ConnectivityChecker {
4 | fun hasInternetAccess(): Boolean
5 | }
6 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/di/RepositoryModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.di
2 |
3 | import com.zlagi.data.repository.account.DefaultAccountRepository
4 | import com.zlagi.data.repository.auth.DefaultAuthRepository
5 | import com.zlagi.data.repository.feed.DefaultFeedRepository
6 | import com.zlagi.data.repository.search.DefaultSearchBlogRepository
7 | import com.zlagi.data.repository.search.history.DefaultHistoryRepository
8 | import com.zlagi.domain.repository.account.AccountRepository
9 | import com.zlagi.domain.repository.auth.AuthRepository
10 | import com.zlagi.domain.repository.feed.FeedRepository
11 | import com.zlagi.domain.repository.search.SearchBlogRepository
12 | import com.zlagi.domain.repository.search.history.HistoryRepository
13 | import dagger.Binds
14 | import dagger.Module
15 | import dagger.hilt.InstallIn
16 | import dagger.hilt.components.SingletonComponent
17 |
18 | @Module
19 | @InstallIn(SingletonComponent::class)
20 | abstract class RepositoryModule {
21 |
22 | @Binds
23 | abstract fun provideAuthRepository(
24 | repository: DefaultAuthRepository
25 | ): AuthRepository
26 |
27 | @Binds
28 | abstract fun provideFeedRepository(
29 | repository: DefaultFeedRepository
30 | ): FeedRepository
31 |
32 | @Binds
33 | abstract fun provideSearchBlogRepository(
34 | repository: DefaultSearchBlogRepository
35 | ): SearchBlogRepository
36 |
37 | @Binds
38 | abstract fun provideHistoryRepository(
39 | repository: DefaultHistoryRepository
40 | ): HistoryRepository
41 |
42 | @Binds
43 | abstract fun provideAccountRepository(
44 | repository: DefaultAccountRepository
45 | ): AccountRepository
46 | }
47 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/mapper/AccountDataDomainMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.AccountDataModel
5 | import com.zlagi.domain.model.AccountDomainModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [AccountDataModel] to [AccountDomainModel] and vice versa
10 | */
11 | class AccountDataDomainMapper @Inject constructor() :
12 | Mapper {
13 |
14 | override fun from(i: AccountDataModel): AccountDomainModel {
15 | return AccountDomainModel(
16 | pk = i.pk,
17 | email = i.email,
18 | username = i.username
19 | )
20 | }
21 |
22 | override fun to(o: AccountDomainModel): AccountDataModel {
23 | return AccountDataModel(
24 | pk = o.pk,
25 | email = o.email,
26 | username = o.username
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/mapper/BlogDataDomainMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.BlogDataModel
5 | import com.zlagi.domain.model.BlogDomainModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [BlogDataModel] to [BlogDomainModel] and vice versa
10 | */
11 | class BlogDataDomainMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: BlogDataModel): BlogDomainModel {
14 | return BlogDomainModel(
15 | pk = i.pk,
16 | title = i.title,
17 | description = i.description,
18 | created = i.created,
19 | updated = i.updated,
20 | username = i.username
21 | )
22 | }
23 |
24 | override fun to(o: BlogDomainModel): BlogDataModel {
25 | return BlogDataModel(
26 | pk = o.pk,
27 | title = o.title,
28 | description = o.description,
29 | created = o.created,
30 | updated = o.updated,
31 | username = o.username
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/mapper/HistoryDataDomainMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.HistoryDataModel
5 | import com.zlagi.domain.model.HistoryDomainModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [HistoryDataModel] to [HistoryDomainModel] and vice versa
10 | */
11 | class HistoryDataDomainMapper @Inject constructor() :
12 | Mapper {
13 |
14 | override fun from(i: HistoryDataModel): HistoryDomainModel {
15 | return HistoryDomainModel(
16 | query = i.query
17 | )
18 | }
19 |
20 | override fun to(o: HistoryDomainModel): HistoryDataModel {
21 | return HistoryDataModel(
22 | query = o.query
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/mapper/PaginationDataDomainMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.PaginationDataModel
5 | import com.zlagi.domain.model.PaginationDomainModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [PaginationDataModel] to [PaginationDomainModel] and vice versa
10 | */
11 | class PaginationDataDomainMapper @Inject constructor() :
12 | Mapper {
13 |
14 | override fun from(i: PaginationDataModel): PaginationDomainModel {
15 | return PaginationDomainModel(
16 | currentPage = i.currentPage,
17 | totalPages = i.totalPages,
18 | )
19 | }
20 |
21 | override fun to(o: PaginationDomainModel): PaginationDataModel {
22 | return PaginationDataModel(
23 | currentPage = o.currentPage,
24 | totalPages = o.totalPages
25 | )
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/mapper/TokensDataDomainMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.TokensDataModel
5 | import com.zlagi.domain.model.TokensDomainModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [TokensDataModel] to [TokensDomainModel] and vice versa
10 | */
11 | class TokensDataDomainMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: TokensDataModel): TokensDomainModel {
14 | return TokensDomainModel(
15 | accessToken = i.accessToken,
16 | refreshToken = i.refreshToken
17 | )
18 | }
19 |
20 | override fun to(o: TokensDomainModel): TokensDataModel {
21 | return TokensDataModel(
22 | accessToken = o.accessToken,
23 | refreshToken = o.refreshToken
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/model/AccountDataModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.model
2 |
3 | data class AccountDataModel(
4 | val pk: Int,
5 | val email: String,
6 | val username: String
7 | )
8 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/model/BlogDataModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.model
2 |
3 | data class BlogDataModel(
4 | val pk: Int,
5 | val title: String,
6 | val description: String,
7 | val created: String,
8 | val updated: String,
9 | val username: String
10 | )
11 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/model/HistoryDataModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.model
2 |
3 | data class HistoryDataModel(
4 | val query: String
5 | )
6 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/model/PaginatedBlogsDataModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.model
2 |
3 | data class PaginatedBlogsDataModel(
4 | val results: List,
5 | val pagination: PaginationDataModel
6 | )
7 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/model/PaginationDataModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.model
2 |
3 | data class PaginationDataModel(
4 | val currentPage: Int,
5 | val totalPages: Int
6 | )
7 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/model/TokensDataModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.model
2 |
3 | data class TokensDataModel(
4 | val accessToken: String,
5 | val refreshToken: String
6 | )
7 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/repository/search/history/DefaultHistoryRepository.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.repository.search.history
2 |
3 | import com.zlagi.data.mapper.HistoryDataDomainMapper
4 | import com.zlagi.data.source.cache.search.history.HistoryCacheDataSource
5 | import com.zlagi.domain.model.HistoryDomainModel
6 | import com.zlagi.domain.repository.search.history.HistoryRepository
7 | import kotlinx.coroutines.flow.Flow
8 | import kotlinx.coroutines.flow.map
9 | import javax.inject.Inject
10 |
11 | class DefaultHistoryRepository @Inject constructor(
12 | private val historyCacheDataSource: HistoryCacheDataSource,
13 | private val historyDataDomainMapper: HistoryDataDomainMapper,
14 | ) : HistoryRepository {
15 |
16 | override fun getHistory(): Flow> =
17 | historyCacheDataSource.getHistory().map {
18 | historyDataDomainMapper.fromList(it)
19 | }
20 |
21 | override suspend fun saveQuery(item: HistoryDomainModel) {
22 | historyDataDomainMapper.to(item).let {
23 | historyCacheDataSource.saveQuery(it)
24 | }
25 | }
26 |
27 | override suspend fun deleteQuery(query: String) {
28 | historyCacheDataSource.deleteQuery(query)
29 | }
30 |
31 | override suspend fun clearHistory() {
32 | historyCacheDataSource.clearHistory()
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/cache/account/AccountCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.cache.account
2 |
3 | import com.zlagi.data.model.AccountDataModel
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface AccountCacheDataSource {
7 | fun fetchAccount(): Flow
8 | suspend fun storeAccount(account: AccountDataModel)
9 | suspend fun deleteAccount()
10 | }
11 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/cache/feed/FeedCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.cache.feed
2 |
3 | import com.zlagi.data.model.BlogDataModel
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface FeedCacheDataSource {
7 | fun fetchBlog(blogPk: Int): Flow
8 | fun fetchBlogs(searchQuery: String): Flow>
9 | suspend fun storeBlog(blog: BlogDataModel)
10 | suspend fun storeBlogs(blogList: List)
11 | suspend fun updateBlog(blog: BlogDataModel)
12 | suspend fun deleteBlog(blogPk: Int)
13 | suspend fun deleteAllBlogs()
14 | }
15 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/cache/search/SearchBlogCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.cache.search
2 |
3 | import com.zlagi.data.model.BlogDataModel
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface SearchBlogCacheDataSource {
7 | fun fetchBlogs(searchQuery: String): Flow>
8 | suspend fun storeBlogs(blogList: List)
9 | suspend fun deleteAllBlogs()
10 | }
11 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/cache/search/history/HistoryCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.cache.search.history
2 |
3 | import com.zlagi.data.model.HistoryDataModel
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface HistoryCacheDataSource {
7 | fun getHistory(): Flow>
8 | suspend fun saveQuery(item: HistoryDataModel)
9 | suspend fun deleteQuery(query: String)
10 | suspend fun clearHistory()
11 | }
12 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/network/account/AccountNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.network.account
2 |
3 | import com.zlagi.common.utils.wrapper.DataResult
4 | import com.zlagi.data.model.AccountDataModel
5 |
6 | interface AccountNetworkDataSource {
7 | suspend fun getAccount(): DataResult
8 | suspend fun updatePassword(
9 | currentPassword: String,
10 | newPassword: String,
11 | confirmNewPassword: String
12 | ): DataResult
13 | }
14 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/network/auth/AuthNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.network.auth
2 |
3 | import android.content.Intent
4 | import com.zlagi.common.utils.wrapper.DataResult
5 | import com.zlagi.data.model.TokensDataModel
6 |
7 | interface AuthNetworkDataSource {
8 | suspend fun signIn(email: String, password: String): DataResult
9 | suspend fun signUp(
10 | email: String,
11 | username: String,
12 | password: String,
13 | confirmPassword: String
14 | ): DataResult
15 |
16 | suspend fun googleIdpAuthentication(data: Intent): DataResult
17 | suspend fun revokeToken(token: String)
18 | }
19 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/network/blog/BlogNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.network.blog
2 |
3 | import com.zlagi.common.utils.wrapper.DataResult
4 | import com.zlagi.data.model.BlogDataModel
5 | import com.zlagi.data.model.PaginatedBlogsDataModel
6 |
7 | interface BlogNetworkDataSource {
8 | suspend fun getBlogs(
9 | searchQuery: String,
10 | page: Int,
11 | pageSize: Int
12 | ): DataResult
13 |
14 | suspend fun createBlog(
15 | blogTitle: String,
16 | blogDescription: String,
17 | creationTime: String
18 | ): DataResult
19 |
20 | suspend fun updateBlog(
21 | blogPk: Int,
22 | blogTitle: String,
23 | blogDescription: String,
24 | updateTime: String
25 | ): DataResult
26 |
27 | suspend fun deleteBlog(blogPk: Int): DataResult
28 | suspend fun checkBlogAuthor(blogPk: Int): DataResult
29 | suspend fun sendNotification(title: String)
30 | }
31 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/source/preferences/PreferencesDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.source.preferences
2 |
3 | import com.zlagi.data.model.TokensDataModel
4 |
5 | interface PreferencesDataSource {
6 | fun storeTokens(tokens: TokensDataModel)
7 | fun getAccessToken(): String
8 | fun getRefreshToken(): String
9 | fun deleteTokens()
10 | }
11 |
--------------------------------------------------------------------------------
/data/src/main/java/com/zlagi/data/worker/RefreshDataWorker.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2019 Google Inc.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.zlagi.data.worker
18 |
19 | import android.annotation.SuppressLint
20 | import android.content.Context
21 | import androidx.hilt.work.HiltWorker
22 | import androidx.work.CoroutineWorker
23 | import androidx.work.WorkerParameters
24 | import com.zlagi.common.utils.wrapper.DataResult
25 | import com.zlagi.domain.usecase.account.detail.SyncAccountUseCase
26 | import dagger.assisted.Assisted
27 | import dagger.assisted.AssistedInject
28 |
29 | @HiltWorker
30 | class RefreshDataWorker @AssistedInject constructor(
31 | @Assisted appContext: Context,
32 | @Assisted params: WorkerParameters,
33 | private val syncAccountUseCase: SyncAccountUseCase
34 |
35 | ) : CoroutineWorker(appContext, params) {
36 |
37 | @SuppressLint("RestrictedApi")
38 | override suspend fun doWork(): Result {
39 | return when (syncAccountUseCase()) {
40 | is DataResult.Success -> {
41 | Result.success()
42 | }
43 | is DataResult.Error -> {
44 | Result.Failure()
45 | }
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/cache/FakeAccountCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.cache
2 |
3 | import com.zlagi.data.model.AccountDataModel
4 | import com.zlagi.data.source.cache.account.AccountCacheDataSource
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.flow
7 |
8 | class FakeAccountCacheDataSource : AccountCacheDataSource {
9 | private val blogs = arrayListOf()
10 |
11 | override fun fetchAccount(): Flow {
12 | return flow {
13 | emit(blogs[0])
14 | }
15 | }
16 |
17 | override suspend fun storeAccount(account: AccountDataModel) {
18 | blogs.add(account)
19 | }
20 | //
21 |
22 | override suspend fun deleteAccount() {
23 | blogs.clear()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/cache/FakeFeedCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.cache
2 |
3 | import com.zlagi.data.fakes.FakeDataGenerator
4 | import com.zlagi.data.model.BlogDataModel
5 | import com.zlagi.data.source.cache.feed.FeedCacheDataSource
6 | import kotlinx.coroutines.flow.Flow
7 | import kotlinx.coroutines.flow.flow
8 |
9 | class FakeFeedCacheDataSource : FeedCacheDataSource {
10 |
11 | override fun fetchBlog(blogPk: Int): Flow {
12 | return flow {
13 | emit(FakeDataGenerator.blogCreated)
14 | }
15 | }
16 |
17 | override fun fetchBlogs(searchQuery: String): Flow> {
18 | return flow {
19 | emit(FakeDataGenerator.blogs)
20 | }
21 | }
22 |
23 | override suspend fun storeBlog(blog: BlogDataModel) {
24 | //
25 | }
26 |
27 | override suspend fun storeBlogs(blogList: List) {
28 | //
29 | }
30 |
31 | override suspend fun updateBlog(blog: BlogDataModel) {
32 | //
33 | }
34 |
35 | override suspend fun deleteBlog(blogPk: Int) {
36 | //
37 | }
38 |
39 | override suspend fun deleteAllBlogs() {
40 | //
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/cache/FakeHistoryCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.cache
2 |
3 | import com.zlagi.data.model.HistoryDataModel
4 | import com.zlagi.data.source.cache.search.history.HistoryCacheDataSource
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.flow
7 |
8 | class FakeHistoryCacheDataSource : HistoryCacheDataSource {
9 |
10 | private val cache = LinkedHashMap()
11 |
12 | override suspend fun saveQuery(item: HistoryDataModel) {
13 | cache[item.query] = item
14 | }
15 |
16 | override fun getHistory(): Flow> {
17 | return flow {
18 | emit(value = cache.values.toList())
19 | }
20 | }
21 |
22 | override suspend fun deleteQuery(query: String) {
23 | with(cache) {
24 | remove(key = query)
25 | }
26 | }
27 |
28 | override suspend fun clearHistory() {
29 | cache.clear()
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/cache/FakeSearchBlogCacheDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.cache
2 |
3 | import com.zlagi.data.model.BlogDataModel
4 | import com.zlagi.data.source.cache.search.SearchBlogCacheDataSource
5 | import kotlinx.coroutines.flow.Flow
6 | import kotlinx.coroutines.flow.flow
7 |
8 | class FakeSearchBlogCacheDataSource: SearchBlogCacheDataSource {
9 |
10 | private val cache = LinkedHashMap()
11 |
12 | override fun fetchBlogs(searchQuery: String): Flow> {
13 | return flow {
14 | emit(value = cache.values.toList())
15 | }
16 | }
17 |
18 | override suspend fun storeBlogs(blogList: List) {
19 | blogList.map {
20 | cache[it.pk.toString()] = it
21 | }
22 | }
23 |
24 | override suspend fun deleteAllBlogs() {
25 | cache.clear()
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/network/FakeAccountNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.network
2 |
3 | import com.zlagi.common.utils.wrapper.DataResult
4 | import com.zlagi.data.model.AccountDataModel
5 | import com.zlagi.data.source.network.account.AccountNetworkDataSource
6 |
7 | class FakeAccountNetworkDataSource(
8 | val account: DataResult,
9 | val updatedPassword: DataResult
10 | ) : AccountNetworkDataSource {
11 |
12 | override suspend fun getAccount(): DataResult {
13 | return account
14 | }
15 |
16 | override suspend fun updatePassword(
17 | currentPassword: String,
18 | newPassword: String,
19 | confirmNewPassword: String
20 | ): DataResult {
21 | return updatedPassword
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/network/FakeAuthNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.network
2 |
3 | import android.content.Intent
4 | import com.zlagi.common.utils.wrapper.DataResult
5 | import com.zlagi.data.model.TokensDataModel
6 | import com.zlagi.data.source.network.auth.AuthNetworkDataSource
7 |
8 | class FakeAuthNetworkDataSource(
9 | private val signInResponse: DataResult,
10 | ) : AuthNetworkDataSource {
11 |
12 | override suspend fun signIn(email: String, password: String): DataResult {
13 | return signInResponse
14 | }
15 |
16 | override suspend fun signUp(
17 | email: String,
18 | username: String,
19 | password: String,
20 | confirmPassword: String
21 | ): DataResult {
22 | return signInResponse
23 | }
24 |
25 | override suspend fun googleIdpAuthentication(data: Intent): DataResult {
26 | return signInResponse
27 | }
28 |
29 | override suspend fun revokeToken(token: String) {
30 | //
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/network/FakeBlogNetworkDataSource.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.network
2 |
3 | import com.zlagi.common.utils.wrapper.DataResult
4 | import com.zlagi.data.model.BlogDataModel
5 | import com.zlagi.data.model.PaginatedBlogsDataModel
6 | import com.zlagi.data.source.network.blog.BlogNetworkDataSource
7 |
8 | class FakeBlogNetworkDataSource(
9 | private val paginatedBlogs: DataResult,
10 | private val createdBlog: DataResult,
11 | private val updatedBlog: DataResult,
12 | private val deletedBlog: DataResult,
13 | private val checkedAuthor: DataResult
14 | ) : BlogNetworkDataSource {
15 |
16 | override suspend fun getBlogs(
17 | searchQuery: String,
18 | page: Int,
19 | pageSize: Int
20 | ): DataResult {
21 | return paginatedBlogs
22 | }
23 |
24 | override suspend fun createBlog(
25 | blogTitle: String,
26 | blogDescription: String,
27 | creationTime: String
28 | ): DataResult {
29 | return createdBlog
30 | }
31 |
32 | override suspend fun updateBlog(
33 | blogPk: Int,
34 | blogTitle: String,
35 | blogDescription: String,
36 | updateTime: String
37 | ): DataResult {
38 | return updatedBlog
39 | }
40 |
41 | override suspend fun deleteBlog(blogPk: Int): DataResult {
42 | return deletedBlog
43 | }
44 |
45 | override suspend fun checkBlogAuthor(blogPk: Int): DataResult {
46 | return checkedAuthor
47 | }
48 |
49 | override suspend fun sendNotification(title: String) {
50 | //
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/data/src/test/java/com/zlagi/data/fakes/source/preferences/FakePreferences.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.data.fakes.source.preferences
2 |
3 | import com.zlagi.common.utils.PreferencesConstants.KEY_ACCESS_TOKEN
4 | import com.zlagi.common.utils.PreferencesConstants.KEY_REFRESH_TOKEN
5 | import com.zlagi.data.model.TokensDataModel
6 | import com.zlagi.data.source.preferences.PreferencesDataSource
7 | import javax.inject.Inject
8 |
9 | class FakePreferences @Inject constructor() : PreferencesDataSource {
10 | private val preferences = mutableMapOf()
11 |
12 | override fun storeTokens(tokens: TokensDataModel) {
13 | preferences[KEY_ACCESS_TOKEN] = tokens.accessToken
14 | preferences[KEY_REFRESH_TOKEN] = tokens.refreshToken
15 | }
16 |
17 | override fun getAccessToken(): String {
18 | return ""
19 | }
20 |
21 | override fun getRefreshToken(): String {
22 | return preferences[KEY_REFRESH_TOKEN] as String
23 | }
24 |
25 | override fun deleteTokens() {
26 | //
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/domain/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/domain/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | }
6 |
7 | android {
8 |
9 | compileSdkVersion SDKConfig.compileSdkVersion
10 | buildToolsVersion SDKConfig.buildToolsVersion
11 |
12 | defaultConfig {
13 | minSdkVersion SDKConfig.minSdkVersion
14 | targetSdkVersion SDKConfig.targetSdkVersion
15 | }
16 |
17 | compileOptions {
18 | sourceCompatibility JavaVersion.VERSION_1_8
19 | targetCompatibility JavaVersion.VERSION_1_8
20 | }
21 |
22 | kotlinOptions {
23 | jvmTarget = JavaVersion.VERSION_1_8
24 | }
25 | }
26 |
27 | dependencies {
28 |
29 | implementation(project(":common"))
30 |
31 | // Coroutines
32 | implementation Deps.COROUTINES_CORE
33 | implementation Deps.COROUTINES_ANDROID
34 |
35 | // DI
36 | implementation Deps.INJECT
37 | }
--------------------------------------------------------------------------------
/domain/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/domain/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/model/AccountDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.model
2 |
3 | data class AccountDomainModel(
4 | val pk: Int,
5 | val email: String,
6 | val username: String
7 | )
8 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/model/BlogDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.model
2 |
3 | data class BlogDomainModel(
4 | val pk: Int,
5 | val title: String,
6 | val description: String,
7 | val created: String,
8 | val updated: String,
9 | val username: String
10 | )
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/model/HistoryDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.model
2 |
3 | data class HistoryDomainModel(
4 | val query: String
5 | )
6 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/model/PaginatedBlogsDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.model
2 |
3 | data class PaginatedBlogsDomainModel(
4 | val results: List,
5 | val pagination: PaginationDomainModel
6 | )
7 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/model/PaginationDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.model
2 |
3 | data class PaginationDomainModel(
4 | val currentPage: Int,
5 | val totalPages: Int
6 | ){
7 |
8 | companion object {
9 | const val DEFAULT_PAGE_SIZE = 10
10 | }
11 |
12 | val canLoadMore: Boolean
13 | get() = currentPage < totalPages
14 | }
15 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/model/TokensDomainModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.model
2 |
3 | data class TokensDomainModel(
4 | val accessToken: String,
5 | val refreshToken: String
6 | )
7 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/repository/account/AccountRepository.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.repository.account
2 |
3 | import com.zlagi.common.utils.wrapper.DataResult
4 | import com.zlagi.domain.model.AccountDomainModel
5 | import kotlinx.coroutines.flow.Flow
6 |
7 | interface AccountRepository {
8 | suspend fun requestAccount(): DataResult
9 | suspend fun storeAccount(account: AccountDomainModel)
10 | fun getAccount(): Flow
11 | suspend fun updatePassword(
12 | currentPassword: String,
13 | newPassword: String,
14 | confirmNewPassword: String
15 | ): DataResult
16 | suspend fun deleteAccount()
17 | }
18 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/repository/auth/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.repository.auth
2 |
3 | import android.content.Intent
4 | import com.zlagi.common.utils.wrapper.DataResult
5 | import com.zlagi.domain.model.TokensDomainModel
6 |
7 | interface AuthRepository {
8 | suspend fun signIn(email: String, password: String): DataResult
9 | suspend fun signUp(
10 | email: String,
11 | username: String,
12 | password: String,
13 | confirmPassword: String
14 | ): DataResult
15 |
16 | suspend fun googleIdpAuthentication(data: Intent): DataResult
17 | suspend fun fetchTokens(): TokensDomainModel
18 | suspend fun storeTokens(tokens: TokensDomainModel)
19 | suspend fun deleteTokens()
20 | suspend fun revokeToken(token: String)
21 | fun authenticationStatus(): Boolean
22 | }
23 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/repository/feed/FeedRepository.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.repository.feed
2 |
3 | import com.zlagi.common.utils.wrapper.DataResult
4 | import com.zlagi.domain.model.BlogDomainModel
5 | import com.zlagi.domain.model.PaginatedBlogsDomainModel
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface FeedRepository {
9 | suspend fun requestMoreBlogs(
10 | searchQuery: String,
11 | page: Int,
12 | pageSize: Int
13 | ): DataResult
14 |
15 | fun getBlog(blogPk: Int): Flow
16 | fun getBlogs(searchQuery: String): Flow>
17 | suspend fun storeBlogs(blogList: List)
18 | suspend fun createBlog(
19 | blogTitle: String,
20 | blogDescription: String,
21 | creationTime: String
22 | ): DataResult
23 |
24 | suspend fun updateBlog(
25 | blogPk: Int,
26 | blogTitle: String,
27 | blogDescription: String,
28 | updateTime: String
29 | ): DataResult
30 |
31 | suspend fun deleteBlog(blogPk: Int): DataResult
32 | suspend fun deleteAllBlogs()
33 | suspend fun checkBlogAuthor(blogPk: Int): DataResult
34 | }
35 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/repository/search/SearchBlogRepository.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.repository.search
2 |
3 | import com.zlagi.common.utils.wrapper.DataResult
4 | import com.zlagi.domain.model.BlogDomainModel
5 | import com.zlagi.domain.model.PaginatedBlogsDomainModel
6 | import kotlinx.coroutines.flow.Flow
7 |
8 | interface SearchBlogRepository {
9 | suspend fun requestMoreBlogs(searchQuery: String, page: Int, pageSize: Int): DataResult
10 | fun getBlogs(searchQuery: String): Flow>
11 | suspend fun storeBlogs(blogList: List)
12 | suspend fun deleteAllBlogs()
13 | }
14 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/repository/search/history/HistoryRepository.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.repository.search.history
2 |
3 | import com.zlagi.domain.model.HistoryDomainModel
4 | import kotlinx.coroutines.flow.Flow
5 |
6 | interface HistoryRepository {
7 | fun getHistory(): Flow>
8 | suspend fun saveQuery(item: HistoryDomainModel)
9 | suspend fun deleteQuery(query: String)
10 | suspend fun clearHistory()
11 | }
12 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/taskmanager/TaskManager.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.taskmanager
2 |
3 | import kotlinx.coroutines.flow.Flow
4 | import java.util.*
5 |
6 | interface TaskManager {
7 | fun syncAccount(): UUID
8 | fun observeTask(taskId: UUID): Flow
9 | fun abortAllTasks()
10 | }
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/taskmanager/TaskState.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.taskmanager
2 |
3 | enum class TaskState {
4 | SCHEDULED, CANCELLED, FAILED, COMPLETED;
5 | }
6 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/account/delete/DeleteAccountUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.account.delete
2 |
3 | import com.zlagi.domain.repository.account.AccountRepository
4 | import javax.inject.Inject
5 |
6 | class DeleteAccountUseCase @Inject constructor(
7 | private val accountRepository: AccountRepository
8 | ) {
9 | suspend operator fun invoke() = accountRepository.deleteAccount()
10 | }
11 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/account/detail/GetAccountUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.account.detail
2 |
3 | import com.zlagi.domain.repository.account.AccountRepository
4 | import kotlinx.coroutines.flow.first
5 | import javax.inject.Inject
6 |
7 | class GetAccountUseCase @Inject constructor(
8 | private val accountRepository: AccountRepository
9 | ) {
10 | suspend operator fun invoke() = accountRepository.getAccount().first()
11 | }
12 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/account/detail/SyncAccountUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.account.detail
2 |
3 | import com.zlagi.common.qualifier.IoDispatcher
4 | import com.zlagi.common.utils.wrapper.DataResult
5 | import com.zlagi.domain.repository.account.AccountRepository
6 | import kotlinx.coroutines.CoroutineDispatcher
7 | import kotlinx.coroutines.withContext
8 | import javax.inject.Inject
9 |
10 | class SyncAccountUseCase @Inject constructor(
11 | private val accountRepository: AccountRepository,
12 | @IoDispatcher private val dispatcher: CoroutineDispatcher
13 | ) {
14 | suspend operator fun invoke(): DataResult {
15 | return when (
16 | val result = withContext(dispatcher) {
17 | accountRepository.requestAccount()
18 | }) {
19 | is DataResult.Success -> {
20 | accountRepository.storeAccount(result.data)
21 | DataResult.Success(Unit)
22 | }
23 | is DataResult.Error -> DataResult.Error(result.exception)
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/auth/deletetokens/DeleteTokensUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.auth.deletetokens
2 |
3 | import com.zlagi.domain.repository.auth.AuthRepository
4 | import javax.inject.Inject
5 |
6 | class DeleteTokensUseCase @Inject constructor(
7 | private val authRepository: AuthRepository
8 | ) {
9 | suspend operator fun invoke() {
10 | authRepository.fetchTokens().refreshToken.let {
11 | authRepository.revokeToken(it)
12 | }
13 | authRepository.deleteTokens()
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/auth/signin/email/SignInUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.auth.signin.email
2 |
3 | import com.zlagi.common.qualifier.IoDispatcher
4 | import com.zlagi.domain.validator.AuthValidator
5 | import com.zlagi.common.utils.result.SignInResult
6 | import com.zlagi.common.utils.wrapper.DataResult
7 | import com.zlagi.domain.repository.auth.AuthRepository
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.withContext
10 | import javax.inject.Inject
11 |
12 | class SignInUseCase @Inject constructor(
13 | private val authRepository: AuthRepository,
14 | @IoDispatcher private val dispatcher: CoroutineDispatcher
15 | ) {
16 | suspend operator fun invoke(email: String, password: String): SignInResult {
17 |
18 | val emailError = AuthValidator.emailError(email)
19 | val passwordError = AuthValidator.passwordError(password)
20 |
21 | if (emailError != null || passwordError != null) {
22 | return SignInResult(emailError, passwordError)
23 | }
24 |
25 | return when (
26 | val result = withContext(dispatcher) {
27 | authRepository.signIn(email, password)
28 | }) {
29 | is DataResult.Success -> {
30 | authRepository.storeTokens(result.data)
31 | SignInResult(result = DataResult.Success(Unit))
32 | }
33 | is DataResult.Error -> {
34 | SignInResult(result = DataResult.Error(result.exception))
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/auth/signin/google/GoogleIdpAuthenticationInUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.auth.signin.google
2 |
3 | import android.content.Intent
4 | import com.zlagi.common.qualifier.IoDispatcher
5 | import com.zlagi.common.utils.wrapper.DataResult
6 | import com.zlagi.domain.repository.auth.AuthRepository
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.withContext
9 | import javax.inject.Inject
10 |
11 | class GoogleIdpAuthenticationInUseCase @Inject constructor(
12 | private val authRepository: AuthRepository,
13 | @IoDispatcher private val dispatcher: CoroutineDispatcher
14 | ) {
15 | suspend operator fun invoke(data: Intent): DataResult {
16 | return when (
17 | val result = withContext(dispatcher) {
18 | authRepository.googleIdpAuthentication(data)
19 | }) {
20 | is DataResult.Success -> {
21 | authRepository.storeTokens(result.data)
22 | DataResult.Success(Unit)
23 | }
24 | is DataResult.Error -> {
25 | DataResult.Error(result.exception)
26 | }
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/auth/status/AuthenticationStatusUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.auth.status
2 |
3 | import com.zlagi.domain.repository.auth.AuthRepository
4 | import javax.inject.Inject
5 |
6 | class AuthenticationStatusUseCase @Inject constructor(
7 | private val authRepository: AuthRepository
8 | ) {
9 | operator fun invoke(): Boolean {
10 | return authRepository.authenticationStatus()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/checkauthor/CheckBlogAuthorUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.checkauthor
2 |
3 | import com.zlagi.common.qualifier.IoDispatcher
4 | import com.zlagi.common.utils.Constants
5 | import com.zlagi.common.utils.wrapper.DataResult
6 | import com.zlagi.domain.repository.feed.FeedRepository
7 | import kotlinx.coroutines.CoroutineDispatcher
8 | import kotlinx.coroutines.withContext
9 | import javax.inject.Inject
10 |
11 | class CheckBlogAuthorUseCase @Inject constructor(
12 | private val feedRepository: FeedRepository,
13 | @IoDispatcher private val dispatcher: CoroutineDispatcher
14 | ) {
15 | suspend operator fun invoke(
16 | blogPk: Int
17 | ): DataResult {
18 | return when (
19 | val result = withContext(dispatcher) {
20 | feedRepository.checkBlogAuthor(blogPk)
21 | }) {
22 | is DataResult.Success -> {
23 | if (result.data == Constants.HAVE_PERMISSION) DataResult.Success(true)
24 | else DataResult.Success(false)
25 | }
26 | is DataResult.Error -> DataResult.Error(result.exception)
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/dateformat/DateFormatUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.dateformat
2 |
3 | import android.annotation.SuppressLint
4 | import java.text.SimpleDateFormat
5 | import java.util.*
6 | import javax.inject.Inject
7 |
8 | class DateFormatUseCase @Inject constructor() {
9 | operator fun invoke(): String {
10 | @SuppressLint("SimpleDateFormat")
11 | val formatter = SimpleDateFormat("'Date: 'yyyy-MM-dd' Time: 'HH:mm:ss")
12 | val now = Date()
13 | return formatter.format(now)
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/delete/DeleteBlogUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.delete
2 |
3 | import com.zlagi.common.qualifier.IoDispatcher
4 | import com.zlagi.domain.repository.feed.FeedRepository
5 | import kotlinx.coroutines.CoroutineDispatcher
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 |
9 | class DeleteBlogUseCase @Inject constructor(
10 | private val feedRepository: FeedRepository,
11 | @IoDispatcher private val dispatcher: CoroutineDispatcher
12 | ) {
13 | suspend operator fun invoke(
14 | blogPk: Int
15 | ) = withContext(dispatcher) {
16 | feedRepository.deleteBlog(blogPk)
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/detail/GetBlogUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.detail
2 |
3 | import com.zlagi.domain.repository.feed.FeedRepository
4 | import kotlinx.coroutines.flow.first
5 | import javax.inject.Inject
6 |
7 | class GetBlogUseCase @Inject constructor(
8 | private val feedRepository: FeedRepository
9 | ) {
10 | suspend operator fun invoke(
11 | blogPk: Int
12 | ) = feedRepository.getBlog(blogPk).first()
13 | }
14 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/feed/GetFeedUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.feed
2 |
3 | import com.zlagi.domain.repository.feed.FeedRepository
4 | import kotlinx.coroutines.flow.first
5 | import javax.inject.Inject
6 |
7 | class GetFeedUseCase @Inject constructor(
8 | private val feedRepository: FeedRepository
9 | ) {
10 | suspend operator fun invoke(
11 | query: String
12 | ) = feedRepository.getBlogs(query).first()
13 | }
14 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/feed/RequestMoreBlogsUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.feed
2 |
3 | import com.zlagi.common.exception.NetworkException
4 | import com.zlagi.common.qualifier.IoDispatcher
5 | import com.zlagi.common.utils.wrapper.DataResult
6 | import com.zlagi.domain.model.PaginatedBlogsDomainModel
7 | import com.zlagi.domain.repository.feed.FeedRepository
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.withContext
10 | import javax.inject.Inject
11 |
12 | class RequestMoreBlogsUseCase @Inject constructor(
13 | private val feedRepository: FeedRepository,
14 | @IoDispatcher private val dispatcher: CoroutineDispatcher
15 | ) {
16 | suspend operator fun invoke(
17 | refreshLoad: Boolean,
18 | searchQuery: String,
19 | page: Int,
20 | pageSize: Int
21 | ): DataResult {
22 | return when (val result =
23 | withContext(dispatcher) {
24 | feedRepository.requestMoreBlogs(
25 | searchQuery,
26 | page,
27 | pageSize
28 | )
29 | }) {
30 | is DataResult.Success -> {
31 | if (refreshLoad) feedRepository.deleteAllBlogs()
32 | val feed = result.data.results
33 | if (feed.isEmpty()) return DataResult.Error(NetworkException.NoResults)
34 | feedRepository.storeBlogs(feed)
35 | DataResult.Success(result.data)
36 | }
37 | is DataResult.Error -> {
38 | DataResult.Error(result.exception)
39 | }
40 | }
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/search/GetSearchUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.search
2 |
3 | import com.zlagi.domain.model.BlogDomainModel
4 | import com.zlagi.domain.repository.search.SearchBlogRepository
5 | import kotlinx.coroutines.flow.first
6 | import javax.inject.Inject
7 |
8 | class GetSearchUseCase @Inject constructor(
9 | private val searchBlogRepository: SearchBlogRepository
10 | ) {
11 | suspend operator fun invoke(
12 | query: String
13 | ): List = searchBlogRepository.getBlogs(query).first()
14 | }
15 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/search/RequestMoreBlogsUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.search
2 |
3 | import com.zlagi.common.exception.NetworkException
4 | import com.zlagi.common.qualifier.IoDispatcher
5 | import com.zlagi.common.utils.wrapper.DataResult
6 | import com.zlagi.domain.model.PaginatedBlogsDomainModel
7 | import com.zlagi.domain.repository.search.SearchBlogRepository
8 | import kotlinx.coroutines.CoroutineDispatcher
9 | import kotlinx.coroutines.withContext
10 | import javax.inject.Inject
11 |
12 | class RequestMoreBlogsUseCase @Inject constructor(
13 | private val searchBlogRepository: SearchBlogRepository,
14 | @IoDispatcher private val dispatcher: CoroutineDispatcher
15 | ) {
16 | suspend operator fun invoke(
17 | refreshLoad: Boolean,
18 | searchQuery: String,
19 | page: Int,
20 | pageSize: Int
21 | ): DataResult {
22 | return when (
23 | val result = withContext(dispatcher) {
24 | searchBlogRepository.requestMoreBlogs(searchQuery, page, pageSize)
25 | }) {
26 | is DataResult.Success -> {
27 | if (refreshLoad) searchBlogRepository.deleteAllBlogs()
28 | val results = result.data.results
29 | if (results.isEmpty()) return DataResult.Error(NetworkException.NoResults)
30 | searchBlogRepository.storeBlogs(results)
31 | DataResult.Success(result.data)
32 | }
33 | is DataResult.Error -> {
34 | DataResult.Error(result.exception)
35 | }
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/search/history/ClearHistoryUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.search.history
2 |
3 | import com.zlagi.domain.repository.search.history.HistoryRepository
4 | import javax.inject.Inject
5 |
6 | class ClearHistoryUseCase @Inject constructor(
7 | private val historyRepository: HistoryRepository
8 | ) {
9 | suspend operator fun invoke() {
10 | historyRepository.clearHistory()
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/search/history/DeleteQueryUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.search.history
2 |
3 | import com.zlagi.domain.repository.search.history.HistoryRepository
4 | import javax.inject.Inject
5 |
6 | class DeleteQueryUseCase @Inject constructor(
7 | private val historyRepository: HistoryRepository
8 | ) {
9 | suspend operator fun invoke(query: String) {
10 | historyRepository.deleteQuery(query)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/search/history/GetHistoryUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.search.history
2 |
3 | import com.zlagi.common.exception.CacheException
4 | import com.zlagi.common.utils.wrapper.DataResult
5 | import com.zlagi.domain.model.HistoryDomainModel
6 | import com.zlagi.domain.repository.search.history.HistoryRepository
7 | import kotlinx.coroutines.flow.first
8 | import javax.inject.Inject
9 |
10 | class GetHistoryUseCase @Inject constructor(
11 | private val historyRepository: HistoryRepository
12 | ) {
13 | companion object {
14 | private const val historyItems = 5
15 | }
16 | suspend operator fun invoke(): DataResult> {
17 | val data = historyRepository.getHistory().first()
18 | .takeLast(historyItems).reversed()
19 | if (data.isEmpty()) {
20 | return DataResult.Error(CacheException.NoResults)
21 | }
22 | return DataResult.Success(data)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/usecase/blog/search/history/SaveQueryUseCase.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.usecase.blog.search.history
2 |
3 | import com.zlagi.domain.model.HistoryDomainModel
4 | import com.zlagi.domain.repository.search.history.HistoryRepository
5 | import kotlinx.coroutines.flow.*
6 | import javax.inject.Inject
7 |
8 | class SaveQueryUseCase @Inject constructor(
9 | private val historyRepository: HistoryRepository
10 | ) {
11 | suspend operator fun invoke(
12 | query: String
13 | ) {
14 | val data = historyRepository.getHistory().first()
15 | val exists = data.find {
16 | it.query == query
17 | }
18 | if (exists == null) historyRepository.saveQuery(HistoryDomainModel(query = query))
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/domain/src/main/java/com/zlagi/domain/validator/BlogValidator.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.domain.validator
2 |
3 | import android.net.Uri
4 | import com.zlagi.common.utils.BlogError
5 |
6 | object BlogValidator {
7 |
8 | private const val titleLength = 2
9 | private const val descriptionLength = 8
10 |
11 | fun blogTitleError(title: String): BlogError? {
12 | return if (!isValidTitle(title)) BlogError.InputTooShort else null
13 | }
14 |
15 | fun blogDescriptionError(description: String): BlogError? {
16 | return if (!isValidDescription(description)) BlogError.InputTooShort else null
17 | }
18 |
19 | fun blogImageError(image: Uri?): BlogError? {
20 | return if (!isValidImage(image)) BlogError.EmptyField else null
21 | }
22 |
23 | private fun isValidTitle(input: String): Boolean =
24 | input.count() > titleLength
25 |
26 | private fun isValidDescription(input: String): Boolean =
27 | input.count() > descriptionLength
28 |
29 | private fun isValidImage(input: Uri?): Boolean =
30 | input != null
31 | }
32 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/gradle.properties
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/HaythemMejerbi/Blogfy/dffd07e8533c285bb5ca9de4e9676985aa0e87cc/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Nov 23 23:31:30 WAT 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/local.properties:
--------------------------------------------------------------------------------
1 | ## This file is automatically generated by Android Studio.
2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED!
3 | #
4 | # This file should *NOT* be checked into Version Control Systems,
5 | # as it contains information specific to your local configuration.
6 | #
7 | # Location of the SDK. This is only used by Gradle.
8 | # For customization when using a Version Control System, please read the
9 | # header note.
10 | sdk.dir=C\:\\Users\\Haythem\\AppData\\Local\\Android\\Sdk
11 | FIREBASE_KEY=838355569325-upi38j8btaun1p60nvcd3uptvahli9fq.apps.googleusercontent.com
--------------------------------------------------------------------------------
/network/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/network/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/network/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/apiservice/AccountApiService.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.apiservice
2 |
3 | import com.zlagi.network.model.NetworkConstants
4 | import com.zlagi.network.model.request.PasswordRequest
5 | import com.zlagi.network.model.response.AccountNetworkModel
6 | import com.zlagi.network.model.response.GenericResponse
7 | import retrofit2.http.Body
8 | import retrofit2.http.GET
9 | import retrofit2.http.PUT
10 |
11 | interface AccountApiService {
12 |
13 | @GET(NetworkConstants.ACCOUNT_ENDPOINT)
14 | suspend fun getAccount(
15 | ): AccountNetworkModel
16 |
17 | @PUT(NetworkConstants.PASSWORD_ENDPOINT)
18 | suspend fun updatePassword(
19 | @Body passwordRequest: PasswordRequest
20 | ): GenericResponse
21 | }
22 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/connectivity/DefaultConnectivityChecker.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.connectivity
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.net.ConnectivityManager
6 | import android.net.NetworkCapabilities
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import javax.inject.Inject
9 |
10 | class DefaultConnectivityChecker @Inject constructor(
11 | @ApplicationContext private val context: Context
12 | ) : com.zlagi.data.connectivity.ConnectivityChecker {
13 | @SuppressLint("MissingPermission")
14 | override fun hasInternetAccess(): Boolean {
15 | val connectivityManager =
16 | context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
17 | val activeNetwork = connectivityManager.activeNetwork
18 | val networkCapabilities = connectivityManager.getNetworkCapabilities(activeNetwork)
19 | return networkCapabilities != null &&
20 | networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/di/ConnectivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.di
2 |
3 | import com.zlagi.data.connectivity.ConnectivityChecker
4 | import com.zlagi.network.connectivity.DefaultConnectivityChecker
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | abstract class ConnectivityModule {
14 |
15 | @Binds
16 | @Singleton
17 | abstract fun bindConnectivityChecker(
18 | connectivityChecker: DefaultConnectivityChecker
19 | ): ConnectivityChecker
20 | }
21 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/mapper/AccountNetworkDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.mapper
2 |
3 | import com.zlagi.common.exception.MappingException
4 | import com.zlagi.common.mapper.Mapper
5 | import com.zlagi.data.model.AccountDataModel
6 | import com.zlagi.network.model.response.AccountNetworkModel
7 | import javax.inject.Inject
8 |
9 | /**
10 | * Mapper class for convert [AccountNetworkModel] to [AccountDataModel] and vice versa
11 | */
12 | class AccountNetworkDataMapper @Inject constructor() : Mapper {
13 |
14 | override fun from(i: AccountNetworkModel): AccountDataModel {
15 | return AccountDataModel(
16 | pk = i.id ?: throw MappingException("Account pk cannot be null"),
17 | email = i.email.orEmpty(),
18 | username = i.username.orEmpty()
19 | )
20 | }
21 |
22 | override fun to(o: AccountDataModel): AccountNetworkModel {
23 | return AccountNetworkModel(
24 | id = o.pk,
25 | email = o.email,
26 | username = o.username
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/mapper/BlogNetworkDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.BlogDataModel
5 | import com.zlagi.network.model.response.BlogNetworkModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [BlogNetworkModel] to [BlogDataModel] and vice versa
10 | */
11 | class BlogNetworkDataMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: BlogNetworkModel): BlogDataModel {
14 | return BlogDataModel(
15 | pk = i.pk ?: 0,
16 | title = i.title.orEmpty(),
17 | description = i.description.orEmpty(),
18 | created = i.created.orEmpty(),
19 | updated = i.updated.orEmpty(),
20 | username = i.username.orEmpty()
21 | )
22 | }
23 |
24 | override fun to(o: BlogDataModel): BlogNetworkModel {
25 | return BlogNetworkModel(
26 | pk = o.pk,
27 | title = o.title,
28 | description = o.description,
29 | updated = o.updated,
30 | created = o.created,
31 | username = o.username
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/mapper/PaginationNetworkDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.PaginationDataModel
5 | import com.zlagi.network.model.response.PaginationNetworkModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [PaginationNetworkModel] to [PaginationDataModel] and vice versa
10 | */
11 | class PaginationNetworkDataMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: PaginationNetworkModel?): PaginationDataModel {
14 | return PaginationDataModel(
15 | currentPage = i?.current_page ?: 0,
16 | totalPages = i?.total_pages ?: 0,
17 | )
18 | }
19 |
20 | override fun to(o: PaginationDataModel): PaginationNetworkModel {
21 | return PaginationNetworkModel(
22 | current_page = o.currentPage,
23 | total_pages = o.totalPages
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/mapper/TokensNetworkDataMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.data.model.TokensDataModel
5 | import com.zlagi.network.model.response.TokensNetworkModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [TokensNetworkModel] to [TokensDataModel] and vice versa
10 | */
11 | class TokensNetworkDataMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: TokensNetworkModel): TokensDataModel {
14 | return TokensDataModel(
15 | accessToken = i.access_token.orEmpty(),
16 | refreshToken = i.refresh_token.orEmpty()
17 | )
18 | }
19 |
20 | override fun to(o: TokensDataModel): TokensNetworkModel {
21 | return TokensNetworkModel(
22 | access_token = o.accessToken,
23 | refresh_token = o.refreshToken
24 | )
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/NetworkConstants.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | object NetworkConstants {
4 | const val BASE_ENDPOINT = "https://blogfy-server.herokuapp.com/"
5 | const val SIGNIN_ENDPOINT = "auth/signin"
6 | const val SIGNUP_ENDPOINT = "auth/signup"
7 | const val GOOGLE_AUTHENTICATION_ENDPOINT = "auth/idp/google"
8 | const val REFRESH_TOKEN_ENDPOINT = "auth/token/refresh"
9 | const val REVOKE_TOKEN_ENDPOINT = "auth/token/revoke"
10 | const val BLOGS_ENDPOINT ="blog/list"
11 | const val NOTIFICATION_ENDPOINT ="blog/notification"
12 | const val BLOG_ENDPOINT ="blog"
13 | const val CHECK_AUTHOR_ENDPOINT = "blog/{blogId}/is_author"
14 | const val UPDATE_ENDPOINT = "blog/{blogId}"
15 | const val DELETE_ENDPOINT = "blog/{blogId}"
16 | const val ACCOUNT_ENDPOINT = "auth/account"
17 | const val PASSWORD_ENDPOINT = "auth/account/password"
18 | }
19 |
20 | object NetworkParameters {
21 | const val TOKEN_TYPE = "Bearer "
22 | const val AUTH_HEADER = "Authorization"
23 | const val CUSTOM_HEADER = "@"
24 | const val NO_AUTH = "NoAuth"
25 | const val SEARCH_QUERY = "search_query"
26 | const val BLOG_PK = "blogId"
27 | const val PAGE = "page"
28 | const val LIMIT = "limit"
29 | }
30 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/request/GoogleSignInRequest.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.request
2 |
3 | data class GoogleSignInRequest(val username: String?)
4 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/request/NotificationRequest.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.request
2 |
3 | data class NotificationRequest(val title: String)
4 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/request/PasswordRequest.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.request
2 |
3 | data class PasswordRequest(
4 | val currentPassword: String,
5 | val newPassword: String,
6 | val confirmNewPassword: String
7 | )
8 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/request/SignInRequest.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.request
2 |
3 | data class SignInRequest(val email: String, val password: String)
4 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/request/SignUpRequest.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.request
2 |
3 | data class SignUpRequest(
4 | val email: String,
5 | val username: String,
6 | val password: String,
7 | val confirmPassword: String
8 | )
9 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/request/UpdateBlogRequest.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.request
2 |
3 | data class UpdateBlogRequest(val title: String, val description: String, val creationTime: String)
4 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/request/UpdateTokenRequest.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.request
2 |
3 | data class UpdateTokenRequest(val token: String)
4 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/response/AccountNetworkModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.response
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class AccountNetworkModel(
8 | @field:Json(name = "id") val id: Int?,
9 | @field:Json(name = "email") val email: String?,
10 | @field:Json(name = "username") val username: String?
11 | )
12 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/response/BlogNetworkModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.response
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class BlogNetworkModel(
8 | @field:Json(name ="pk") val pk: Int?,
9 | @field:Json(name ="username") val username: String?,
10 | @field:Json(name ="title") val title: String?,
11 | @field:Json(name ="description") val description: String?,
12 | @field:Json(name ="created") val created: String?,
13 | @field:Json(name ="updated") val updated: String?
14 | )
15 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/response/GenericResponse.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.response
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 |
7 | @JsonClass(generateAdapter = true)
8 | data class GenericResponse(
9 | @field:Json(name = "status") val status: String,
10 | @field:Json(name = "message") val message: String
11 | )
12 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/response/PaginatedBlogsNetworkModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.response
2 |
3 | import com.squareup.moshi.Json
4 | import com.squareup.moshi.JsonClass
5 |
6 | @JsonClass(generateAdapter = true)
7 | data class PaginatedBlogsNetworkModel(
8 | @field:Json(name = "results") val results: List?,
9 | @field:Json(name = "pagination") val pagination: PaginationNetworkModel?
10 | )
11 |
12 | @JsonClass(generateAdapter = true)
13 | data class PaginationNetworkModel(
14 | @field:Json(name = "current_page") val current_page: Int?,
15 | @field:Json(name = "total_pages") val total_pages: Int?
16 | )
17 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/model/response/TokensNetworkModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model.response
2 |
3 | import android.annotation.SuppressLint
4 | import com.squareup.moshi.Json
5 | import com.squareup.moshi.JsonClass
6 |
7 | @JsonClass(generateAdapter = true)
8 | @SuppressLint("NewApi")
9 | data class TokensNetworkModel(
10 | @field:Json(name = "access_token") val access_token: String?,
11 | @field:Json(name = "refresh_token") val refresh_token: String?
12 | )
13 |
--------------------------------------------------------------------------------
/network/src/main/java/com/zlagi/network/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.utils
2 |
3 | import com.squareup.moshi.Moshi
4 | import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
5 |
6 | object Extensions {
7 | val moshi: Moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
8 | }
9 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/di/TestConnectivityModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.di
2 |
3 | import com.zlagi.data.connectivity.ConnectivityChecker
4 | import com.zlagi.network.fakes.FakeConnectivityCheckReturnSuccess
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.components.SingletonComponent
8 | import dagger.hilt.testing.TestInstallIn
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @TestInstallIn(
13 | components = [SingletonComponent::class],
14 | replaces = [ConnectivityModule::class]
15 | )
16 | abstract class TestConnectivityModule {
17 |
18 | @Binds
19 | @Singleton
20 | abstract fun bindConnectivityChecker(
21 | connectivityChecker: FakeConnectivityCheckReturnSuccess
22 | ): ConnectivityChecker
23 | }
24 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/fakes/FakeConnectivityChecker.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.fakes
2 |
3 | import com.zlagi.data.connectivity.ConnectivityChecker
4 | import javax.inject.Inject
5 |
6 | class FakeConnectivityCheckReturnSuccess @Inject constructor() : ConnectivityChecker {
7 | override fun hasInternetAccess(): Boolean = true
8 | }
9 |
10 | class FakeConnectivityCheckReturnError : ConnectivityChecker {
11 | override fun hasInternetAccess(): Boolean = false
12 | }
13 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/CheckAuthorResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val AUTHOR_HAVE_PERMISSION_RESPONSE = """
4 | {
5 | "status": "Success",
6 | "message": "You have permission to edit that"
7 | }
8 | """
9 |
10 | const val AUTHOR_HAVE_NO_PERMISSION_RESPONSE = """
11 | {
12 | "status": "Success",
13 | "message": "You don't have permission to edit that"
14 | }
15 | """
16 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/CreateBlogResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val CREATE_BLOG_RESPONSE_JSON = """
4 | {
5 | "pk": 1,
6 | "title": "test1 created",
7 | "description": "some random text for testing",
8 | "created": "Date: 2022-01-25 Time: 18:40:14",
9 | "updated": null,
10 | "username": "Alex"
11 | }
12 | """
13 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/DeleteBlogResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val BLOG_DELETED_RESPONSE_JSON = """
4 | {
5 | "status": "Success",
6 | "message": "Deleted"
7 | }
8 | """
9 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/ExpiredTokenResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val EXPIRED_TOKEN_RESPONSE_JSON = """
4 | {
5 | "status": "FAILED",
6 | "message": "Authentication failed: Access token expired"
7 | }
8 | """
9 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/FeedResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val FEED_RESPONSE_JSON = """
4 | {
5 | "pagination": {
6 | "total_count": 3,
7 | "current_page": 1,
8 | "total_pages": 1,
9 | "_links": {}
10 | },
11 | "results": [
12 | {
13 | "pk": 5788,
14 | "title": "test1",
15 | "description": "some random text for testing",
16 | "created": "Date: 2022-01-25 Time: 18:40:14",
17 | "updated": "Date: 2022-01-25 Time: 18:50:14",
18 | "username": "Zlagii"
19 | },
20 | {
21 | "pk": 5791,
22 | "title": "test2",
23 | "description": "some random text for testing",
24 | "created": "Date: 2022-01-25 Time: 18:40:14",
25 | "updated": "Date: 2022-01-25 Time: 18:59:12",
26 | "username": "Zlagii"
27 | },
28 | {
29 | "pk": 5792,
30 | "title": "test3",
31 | "description": "some random text for testing",
32 | "created": "Date: 2022-01-25 Time: 18:40:14",
33 | "updated": "Date: 2022-01-25 Time: 19:19:12",
34 | "username": "Frank"
35 | }
36 | ]
37 | }
38 | """
39 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/GetAccountResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val GET_ACCOUNT_RESPONSE_JSON = """
4 | {
5 | "id": 1,
6 | "email": "test@gmail.com",
7 | "username": "testtest"
8 | }
9 | """
10 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/SignInResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val SIGN_IN_SUCCESS_RESPONSE_JSON = """
4 | {
5 | "status": "SUCCESS",
6 | "message": "Sign in successfully",
7 | "access_token": "a4U8_gH4HhFghqhuq7'4HgjhHhqFfhgjqhg",
8 | "refresh_token": "faaf33HHf3-6jJJfhFiK4j__FfHFHhf'4HgjhHhqFfhgjqhg"
9 | }
10 | """
11 |
12 | const val SIGN_IN_FAIL_RESPONSE_JSON = """
13 | {
14 | "status": "UNAUTHORIZED",
15 | "message": "Authentication failed: Invalid credentials"
16 | }
17 | """
18 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/SignUpResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val SIGN_UP_SUCCESS_RESPONSE_JSON = """
4 | {
5 | "status": "SUCCESS",
6 | "message": "Sign in successfully",
7 | "access_token": "a4U8_gH4HhFghqhuq7'4HgjhHhqFfhgjqhg",
8 | "refresh_token": "faaf33HHf3-6jJJfhFiK4j__FfHFHhf'4HgjhHhqFfhgjqhg"
9 | }
10 | """
11 |
12 | const val SIGN_UP_FAIL_RESPONSE_JSON = """
13 | {
14 | "status": "FAILED",
15 | "message": "Authentication failed: Email is already taken"
16 | }
17 | """
18 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/UpdateBlogResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val UPDATE_BLOG_RESPONSE_JSON = """
4 | {
5 | "pk": 1,
6 | "title": "test1 updated",
7 | "description": "some random text for testing",
8 | "created": "Date: 2022-01-25 Time: 18:40:14",
9 | "updated": "Date: 2022-01-25 Time: 18:50:14",
10 | "username": "Alex"
11 | }
12 | """
13 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/model/UpdatePasswordResponseJson.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network.model
2 |
3 | const val UPDATE_PASSWORD_SUCCESS_RESPONSE_JSON = """
4 | {
5 | "status": "SUCCESS",
6 | "message": "Password updated"
7 | }
8 | """
9 | const val UPDATE_PASSWORD_FAIL_RESPONSE_JSON = """
10 | {
11 | "status": "SUCCESS",
12 | "message": "Authentication failed: Invalid credentials"
13 | }
14 | """
15 |
16 |
17 |
--------------------------------------------------------------------------------
/network/src/test/java/com/zlagi/network/utils/Extensions.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.network
2 |
3 | import okhttp3.mockwebserver.MockResponse
4 | import okhttp3.mockwebserver.MockWebServer
5 |
6 | fun MockWebServer.enqueueResponse(response: String, code: Int) {
7 | enqueue(
8 | MockResponse()
9 | .setResponseCode(code)
10 | .setBody(response)
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/preferences/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/preferences/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'kotlin-android'
4 | id 'kotlin-kapt'
5 | id 'dagger.hilt.android.plugin'
6 | }
7 |
8 | android {
9 |
10 | compileSdkVersion SDKConfig.compileSdkVersion
11 | buildToolsVersion SDKConfig.buildToolsVersion
12 |
13 | defaultConfig {
14 | minSdkVersion SDKConfig.minSdkVersion
15 | targetSdkVersion SDKConfig.targetSdkVersion
16 | }
17 |
18 | compileOptions {
19 | sourceCompatibility JavaVersion.VERSION_1_8
20 | targetCompatibility JavaVersion.VERSION_1_8
21 | }
22 |
23 | kotlinOptions {
24 | jvmTarget = JavaVersion.VERSION_1_8
25 | }
26 | }
27 |
28 | dependencies {
29 |
30 | implementation(project(":common"))
31 | implementation(project(":data"))
32 |
33 | // Dagger - Hilt
34 | implementation Deps.DAGGER_HILT
35 | kapt AnnotationDeps.DAGGER_HILT_COMPILER
36 | // Hilt - testing
37 | testImplementation UnitTestDeps.HILT_TEST
38 |
39 | // JetPack Security
40 | implementation Deps.JETPACK_SECURITY
41 |
42 | }
--------------------------------------------------------------------------------
/preferences/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/preferences/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/preferences/src/main/java/com/zlagi/preferences/di/PreferencesModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.preferences.di
2 |
3 | import com.zlagi.data.source.preferences.PreferencesDataSource
4 | import com.zlagi.preferences.source.DefaultPreferencesDataSource
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.InstallIn
8 | import dagger.hilt.components.SingletonComponent
9 |
10 | @Module
11 | @InstallIn(SingletonComponent::class)
12 | abstract class PreferencesModule {
13 |
14 | @Binds
15 | abstract fun providePreferences(
16 | datasource: DefaultPreferencesDataSource
17 | ): PreferencesDataSource
18 | }
19 |
--------------------------------------------------------------------------------
/preferences/src/test/java/com/zlagi/preferences/di/TestPreferencesModule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.preferences.di
2 |
3 | import com.zlagi.data.source.preferences.PreferencesDataSource
4 | import com.zlagi.preferences.fakes.FakePreferences
5 | import dagger.Binds
6 | import dagger.Module
7 | import dagger.hilt.components.SingletonComponent
8 | import dagger.hilt.testing.TestInstallIn
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @TestInstallIn(
13 | components = [SingletonComponent::class],
14 | replaces = [PreferencesModule::class]
15 | )
16 | abstract class TestPreferencesModule {
17 |
18 | @Binds
19 | @Singleton
20 | abstract fun providePreferences(preferencesDataSource: FakePreferences): PreferencesDataSource
21 | }
22 |
--------------------------------------------------------------------------------
/preferences/src/test/java/com/zlagi/preferences/fakes/FakePreferences.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.preferences.fakes
2 |
3 | import com.zlagi.common.utils.PreferencesConstants.KEY_ACCESS_TOKEN
4 | import com.zlagi.common.utils.PreferencesConstants.KEY_REFRESH_TOKEN
5 | import com.zlagi.data.model.TokensDataModel
6 | import com.zlagi.data.source.preferences.PreferencesDataSource
7 | import javax.inject.Inject
8 |
9 | class FakePreferences @Inject constructor() : PreferencesDataSource {
10 | private val preferences = mutableMapOf()
11 |
12 | override fun storeTokens(tokens: TokensDataModel) {
13 | preferences[KEY_ACCESS_TOKEN] = tokens.accessToken
14 | preferences[KEY_REFRESH_TOKEN] = tokens.refreshToken
15 | }
16 |
17 | override fun getAccessToken(): String {
18 | return preferences[KEY_ACCESS_TOKEN] as String
19 | }
20 |
21 | override fun getRefreshToken(): String {
22 | return preferences[KEY_REFRESH_TOKEN] as String
23 | }
24 |
25 | override fun deleteTokens() {
26 | with (preferences) {
27 | remove(KEY_ACCESS_TOKEN)
28 | remove(KEY_REFRESH_TOKEN)
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/presentation/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/presentation/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/presentation/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/mapper/AccountDomainPresentationMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.domain.model.AccountDomainModel
5 | import com.zlagi.presentation.model.AccountPresentationModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [AccountDomainModel] to [AccountPresentationModel] and vice versa
10 | */
11 | class AccountDomainPresentationMapper @Inject constructor() :
12 | Mapper {
13 |
14 | override fun from(i: AccountDomainModel): AccountPresentationModel {
15 | return AccountPresentationModel(
16 | pk = i.pk,
17 | email = i.email,
18 | username = i.username
19 | )
20 | }
21 |
22 | override fun to(o: AccountPresentationModel): AccountDomainModel {
23 | return AccountDomainModel(
24 | pk = o.pk,
25 | email = o.email,
26 | username = o.username
27 | )
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/mapper/BlogDomainPresentationMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.domain.model.BlogDomainModel
5 | import com.zlagi.presentation.model.BlogPresentationModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [BlogDomainModel] to [BlogPresentationModel] and vice versa
10 | */
11 | class BlogDomainPresentationMapper @Inject constructor() : Mapper {
12 |
13 | override fun from(i: BlogDomainModel): BlogPresentationModel {
14 | return BlogPresentationModel(
15 | pk = i.pk,
16 | title = i.title,
17 | description = i.description,
18 | created = i.created,
19 | updated = i.updated,
20 | username = i.username
21 | )
22 | }
23 |
24 | override fun to(o: BlogPresentationModel): BlogDomainModel {
25 | return BlogDomainModel(
26 | pk = o.pk,
27 | title = o.title,
28 | description = o.description,
29 | created = o.created,
30 | updated = o.updated,
31 | username = o.username
32 | )
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/mapper/HistoryDomainPresentationMapper.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.mapper
2 |
3 | import com.zlagi.common.mapper.Mapper
4 | import com.zlagi.domain.model.HistoryDomainModel
5 | import com.zlagi.presentation.model.HistoryPresentationModel
6 | import javax.inject.Inject
7 |
8 | /**
9 | * Mapper class for convert [HistoryDomainModel] to [HistoryPresentationModel] and vice versa
10 | */
11 | class HistoryDomainPresentationMapper @Inject constructor() :
12 | Mapper {
13 |
14 | override fun from(i: HistoryDomainModel): HistoryPresentationModel {
15 | return HistoryPresentationModel(
16 | query = i.query
17 | )
18 | }
19 |
20 | override fun to(o: HistoryPresentationModel): HistoryDomainModel {
21 | return HistoryDomainModel(
22 | query = o.query
23 | )
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/model/AccountPresentationModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.model
2 |
3 | class AccountPresentationModel(
4 | val pk: Int,
5 | val email: String,
6 | val username: String
7 | )
8 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/model/BlogPresentationModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.model
2 |
3 | data class BlogPresentationModel(
4 | val pk: Int,
5 | val title: String,
6 | val description: String,
7 | val created: String,
8 | val updated: String,
9 | val username: String
10 | )
11 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/model/HistoryPresentationModel.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.model
2 |
3 | data class HistoryPresentationModel(
4 | val query: String
5 | )
6 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/account/detail/AccountDetailContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.account.detail
2 |
3 | import com.zlagi.presentation.model.AccountPresentationModel
4 |
5 | class AccountDetailContract {
6 |
7 | sealed class AccountDetailEvent {
8 | object Initialization : AccountDetailEvent()
9 | object UpdatePasswordButtonClicked : AccountDetailEvent()
10 | object SignOutButtonClicked: AccountDetailEvent()
11 | object ConfirmDialogButtonClicked: AccountDetailEvent()
12 | }
13 |
14 | sealed class AccountDetailViewEffect {
15 | data class ShowSnackBarError(val message: Int): AccountDetailViewEffect()
16 | object ShowDiscardChangesDialog: AccountDetailViewEffect()
17 | object NavigateToUpdatePassword: AccountDetailViewEffect()
18 | object NavigateToAuth: AccountDetailViewEffect()
19 | }
20 |
21 | data class AccountDetailViewState(
22 | val loading: Boolean = true,
23 | val isSigningOut: Boolean = false,
24 | val account: AccountPresentationModel? = null
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/account/update/UpdatePasswordContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.account.update
2 |
3 | import androidx.annotation.StringRes
4 | import com.zlagi.presentation.R
5 |
6 | class UpdatePasswordContract {
7 |
8 | sealed class UpdatePasswordEvent {
9 |
10 | data class CurrentPasswordChanged(
11 | val currentPassword: String
12 | ) : UpdatePasswordEvent()
13 |
14 | data class NewPasswordChanged(
15 | val newPassword: String
16 | ) : UpdatePasswordEvent()
17 |
18 | data class ConfirmNewPassword(
19 | val confirmNewPassword: String
20 | ) : UpdatePasswordEvent()
21 |
22 | object ConfirmUpdatePasswordButtonClicked : UpdatePasswordEvent()
23 |
24 | object CancelUpdatePasswordButtonClicked : UpdatePasswordEvent()
25 |
26 | object ConfirmDialogButtonClicked : UpdatePasswordEvent()
27 | }
28 |
29 | sealed class UpdatePasswordViewEffect {
30 | data class ShowSnackBarError(val message: Int) : UpdatePasswordViewEffect()
31 | object ShowDiscardChangesDialog: UpdatePasswordViewEffect()
32 | object NavigateUp : UpdatePasswordViewEffect()
33 | object ShowToast : UpdatePasswordViewEffect()
34 | }
35 |
36 | data class UpdatePasswordViewState(
37 | val loading: Boolean = false,
38 | val currentPassword: String = "",
39 | val newPassword: String = "",
40 | val confirmNewPassword: String = "",
41 | @StringRes val currentPasswordError: Int = R.string.no_error_message,
42 | @StringRes val newPasswordError: Int = R.string.no_error_message,
43 | @StringRes val confirmNewPasswordError: Int = R.string.no_error_message
44 | )
45 |
46 | }
47 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/auth/onboarding/OnBoardingContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.auth.onboarding
2 |
3 | import android.content.Intent
4 |
5 | class OnBoardingContract {
6 |
7 | sealed class OnBoardingEvent {
8 | data class GoogleSignInButtonClicked(val data: Intent) : OnBoardingEvent()
9 | object EmailSignInButtonClicked : OnBoardingEvent()
10 | }
11 |
12 | sealed class OnBoardingViewEffect {
13 | data class ShowSnackBarError(val message: Int): OnBoardingViewEffect()
14 | object NavigateToFeed: OnBoardingViewEffect()
15 | object NavigateToSignIn: OnBoardingViewEffect()
16 | }
17 |
18 | data class OnBoardingViewState(
19 | val loading: Boolean = false
20 | )
21 | }
22 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/auth/signin/SignInContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.auth.signin
2 |
3 | import androidx.annotation.StringRes
4 | import com.zlagi.presentation.R
5 |
6 | class SignInContract {
7 |
8 | sealed class SignInEvent {
9 | data class EmailChanged(val email: String) : SignInEvent()
10 | data class PasswordChanged(val password: String) : SignInEvent()
11 | object SignInButtonClicked : SignInEvent()
12 | object SignUpTextViewClicked : SignInEvent()
13 | }
14 |
15 | sealed class SignInViewEffect {
16 | data class ShowSnackBarError(val message: Int): SignInViewEffect()
17 | object NavigateToSignUp: SignInViewEffect()
18 | object NavigateToFeed: SignInViewEffect()
19 | }
20 |
21 | data class SignInViewState(
22 | val email: String = "",
23 | val password: String = "",
24 | val loading: Boolean = false,
25 | @StringRes val emailError: Int = R.string.no_error_message,
26 | @StringRes val passwordError: Int = R.string.no_error_message,
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/auth/signup/SignUpContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.auth.signup
2 |
3 | import androidx.annotation.StringRes
4 | import com.zlagi.presentation.R
5 |
6 | class SignUpContract {
7 |
8 | sealed class SignUpEvent {
9 | data class EmailChanged(val email: String) : SignUpEvent()
10 | data class UsernameChanged(val username: String) : SignUpEvent()
11 | data class PasswordChanged(val password: String) : SignUpEvent()
12 | data class ConfirmPasswordChanged(val confirmPassword: String) : SignUpEvent()
13 | object SignUpButtonClicked : SignUpEvent()
14 | }
15 |
16 | sealed class SignUpViewEffect {
17 | data class ShowSnackBarError(val message: Int): SignUpViewEffect()
18 | object NavigateToFeed: SignUpViewEffect()
19 | }
20 |
21 | data class SignUpViewState(
22 | val email: String = "",
23 | val username: String = "",
24 | val password: String = "",
25 | val confirmPassword: String = "",
26 | val loading: Boolean = false,
27 | @StringRes val emailError: Int = R.string.no_error_message,
28 | @StringRes val usernameError: Int = R.string.no_error_message,
29 | @StringRes val passwordError: Int = R.string.no_error_message,
30 | @StringRes val confirmPasswordError: Int = R.string.no_error_message,
31 | )
32 | }
33 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/blog/create/CreateBlogContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.blog.create
2 |
3 | import android.net.Uri
4 |
5 | class CreateBlogContract {
6 |
7 | sealed class CreateBlogEvent {
8 | data class TitleChanged(
9 | val title: String,
10 | ) : CreateBlogEvent()
11 |
12 | data class DescriptionChanged(
13 | val description: String,
14 | ) : CreateBlogEvent()
15 |
16 | data class OriginalUriChanged(
17 | val uri: Uri?
18 | ) : CreateBlogEvent()
19 |
20 | data class ConfirmCreateButtonClicked(val imageUri: Uri?) : CreateBlogEvent()
21 |
22 | object CancelCreateButtonClicked : CreateBlogEvent()
23 |
24 | object ConfirmDialogButtonClicked: CreateBlogEvent()
25 | }
26 |
27 | sealed class CreateBlogViewEffect {
28 | data class ShowSnackBarError(val message: Int): CreateBlogViewEffect()
29 | object ShowToast: CreateBlogViewEffect()
30 | object ShowDiscardChangesDialog: CreateBlogViewEffect()
31 | object NavigateUp: CreateBlogViewEffect()
32 | }
33 |
34 | data class CreateBlogViewState(
35 | val title: String = "",
36 | val description: String = "",
37 | val originalUri: Uri? = null,
38 | val loading: Boolean = false,
39 | )
40 | }
41 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/blog/detail/BlogDetailContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.blog.detail
2 |
3 | import com.zlagi.presentation.model.BlogPresentationModel
4 |
5 | class BlogDetailContract {
6 |
7 | sealed class BlogDetailEvent {
8 | object Initialization : BlogDetailEvent()
9 | object CheckBlogAuthor : BlogDetailEvent()
10 | object UpdateBlogButtonClicked : BlogDetailEvent()
11 | object DeleteBlogButtonClicked : BlogDetailEvent()
12 | object ConfirmDialogButtonClicked : BlogDetailEvent()
13 | object RefreshData : BlogDetailEvent()
14 | }
15 |
16 | sealed class BlogDetailViewEffect {
17 | data class NavigateToUpdateBlog(val pk: Int?, val title: String?, val description: String?) :
18 | BlogDetailViewEffect()
19 |
20 | object ShowDeleteBlogDialog : BlogDetailViewEffect()
21 | data class NavigateUp(val refreshLoad: Boolean) : BlogDetailViewEffect()
22 | data class ShowSnackBarError(val message: Int) : BlogDetailViewEffect()
23 | }
24 |
25 | data class BlogDetailViewState(
26 | val loading: Boolean = false,
27 | val isAuthor: Boolean = false,
28 | val blog: BlogPresentationModel? = null
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/blog/feed/FeedContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.blog.feed
2 |
3 | import com.zlagi.presentation.model.BlogPresentationModel
4 |
5 | class FeedContract {
6 |
7 | sealed class FeedEvent {
8 | object Initialization : FeedEvent()
9 | object NextPage : FeedEvent()
10 | object SwipeRefresh : FeedEvent()
11 | object CreateBlogButtonClicked : FeedEvent()
12 | data class BlogItemClicked(val blogPk: Int?) : FeedEvent()
13 | }
14 |
15 | sealed class FeedViewEffect {
16 | data class Navigate(val blogPk: Int?) : FeedViewEffect()
17 | data class ShowSnackBarError(val error: Int) : FeedViewEffect()
18 | }
19 |
20 | data class FeedViewState(
21 | val loading: Boolean = false,
22 | val noResults: Boolean = false,
23 | val results: List = emptyList()
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/blog/search/historyview/SearchHistoryContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.blog.search.historyview
2 |
3 | import androidx.annotation.DrawableRes
4 | import com.zlagi.presentation.R
5 | import com.zlagi.presentation.model.HistoryPresentationModel
6 |
7 | class SearchHistoryContract {
8 |
9 | sealed class SearchHistoryEvent {
10 | object LoadHistory : SearchHistoryEvent()
11 | data class HistoryItemClicked(val query: String) : SearchHistoryEvent()
12 | data class DeleteHistoryItem(val query: String) : SearchHistoryEvent()
13 | object ClearHistory : SearchHistoryEvent()
14 | data class UpdateFocusState(val state: Boolean) : SearchHistoryEvent()
15 | data class UpdateQuery(val query: String) : SearchHistoryEvent()
16 | object NavigateTo : SearchHistoryEvent()
17 | object NavigateUp : SearchHistoryEvent()
18 | }
19 |
20 | sealed class SearchHistoryViewEffect {
21 | data class NavigateTo(val query: String) : SearchHistoryViewEffect()
22 | data class NavigateUp(val query: String) : SearchHistoryViewEffect()
23 | }
24 |
25 | data class SearchHistoryViewState(
26 | val expansion: Boolean = true,
27 | val focus: Boolean = false,
28 | val query: String = "",
29 | val data: List = emptyList(),
30 | val emptyHistory: Boolean = true,
31 | @DrawableRes val icon: Int = R.drawable.ic_search,
32 | )
33 |
34 | }
35 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/blog/search/resultview/SearchResultContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.blog.search.resultview
2 |
3 | import androidx.annotation.DrawableRes
4 | import com.zlagi.presentation.R
5 | import com.zlagi.presentation.model.BlogPresentationModel
6 |
7 |
8 | class SearchResultContract {
9 |
10 | sealed class SearchResultEvent {
11 | object UpdateIcon : SearchResultEvent()
12 | data class UpdateQuery(val query: String) : SearchResultEvent()
13 | data class ExecuteSearch(val initSearch: Boolean, val query: String) : SearchResultEvent()
14 | object NextPage : SearchResultEvent()
15 | data class NavigateUp(val icon: Int?) : SearchResultEvent()
16 | }
17 |
18 | sealed class SearchResultViewEffect {
19 | data class ShowSnackBarError(val message: Int) : SearchResultViewEffect()
20 | object ClearFocus: SearchResultViewEffect()
21 | object HideKeyboard: SearchResultViewEffect()
22 | data class NavigateUp(val query: String) : SearchResultViewEffect()
23 | }
24 |
25 | data class SearchResultViewState(
26 | val loading: Boolean = false,
27 | val query: String = "",
28 | @DrawableRes val icon: Int = R.drawable.ic_search,
29 | val blogs: List = emptyList(),
30 | val showEmptyBlogs: Boolean = false
31 | )
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/presentation/src/main/java/com/zlagi/presentation/viewmodel/blog/update/UpdateBlogContract.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation.viewmodel.blog.update
2 |
3 | import android.net.Uri
4 | import com.zlagi.presentation.model.BlogPresentationModel
5 |
6 | class UpdateBlogContract {
7 |
8 | sealed class UpdateBlogEvent {
9 |
10 | data class Initialization(val pk: Int) : UpdateBlogEvent()
11 |
12 | data class TitleChanged(
13 | val title: String
14 | ) : UpdateBlogEvent()
15 |
16 | data class DescriptionChanged(
17 | val description: String,
18 | ) : UpdateBlogEvent()
19 |
20 | data class OriginalUriChanged(
21 | val uri: Uri?
22 | ) : UpdateBlogEvent()
23 |
24 | data class ConfirmUpdateButtonClicked(val imageUri: Uri?) : UpdateBlogEvent()
25 |
26 | object CancelUpdateButtonClicked : UpdateBlogEvent()
27 |
28 | object ConfirmDialogButtonClicked : UpdateBlogEvent()
29 | }
30 |
31 | sealed class UpdateBlogViewEffect {
32 | data class ShowSnackBarError(val message: Int) : UpdateBlogViewEffect()
33 | object ShowDiscardChangesDialog : UpdateBlogViewEffect()
34 | object NavigateUp : UpdateBlogViewEffect()
35 | object ShowToast : UpdateBlogViewEffect()
36 | }
37 |
38 | data class UpdateBlogViewState(
39 | val loading: Boolean = false,
40 | var blog: BlogPresentationModel? = null,
41 | val originalUri: Uri? = null
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/ic_arrow_left.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/presentation/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
3 |
7 |
8 |
--------------------------------------------------------------------------------
/presentation/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Please enter a valid email address
3 | No special characters allowed in username
4 | Username must be at least 3 characters
5 | Password must be at least 8 characters
6 | Empty field
7 |
8 | Title must be at least 3 characters
9 | Description must be at least 8 characters
10 | You must select an image.
11 | "Passwords do not match."
12 | Synchronization failed
13 |
--------------------------------------------------------------------------------
/presentation/src/test/java/com/zlagi/presentation/TestCoroutineRule.kt:
--------------------------------------------------------------------------------
1 | package com.zlagi.presentation
2 |
3 | import kotlinx.coroutines.Dispatchers
4 | import kotlinx.coroutines.ExperimentalCoroutinesApi
5 | import kotlinx.coroutines.test.*
6 | import org.junit.rules.TestRule
7 | import org.junit.runner.Description
8 | import org.junit.runners.model.Statement
9 |
10 | @ExperimentalCoroutinesApi
11 | class TestCoroutineRule : TestRule {
12 |
13 | private val testCoroutineDispatcher = TestCoroutineDispatcher()
14 | private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
15 |
16 | override fun apply(base: Statement, description: Description?) = object : Statement() {
17 | @Throws(Throwable::class)
18 | override fun evaluate() {
19 | Dispatchers.setMain(testCoroutineDispatcher)
20 |
21 | base.evaluate()
22 |
23 | Dispatchers.resetMain()
24 | testCoroutineScope.cleanupTestCoroutines()
25 | }
26 | }
27 |
28 | fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
29 | testCoroutineScope.runBlockingTest { block() }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | jcenter()
5 | google()
6 | mavenCentral()
7 | maven { url 'https://jitpack.io' }
8 | }
9 | }
10 | rootProject.name = "blogfy"
11 | include ':app'
12 | include ':common'
13 | include ':data'
14 | include ':domain'
15 | include ':network'
16 | include ':cache'
17 | include ':preferences'
18 | include ':presentation'
19 |
--------------------------------------------------------------------------------