├── .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 | 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 | 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 | 4 | 8 | 9 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/menu/blog_detail.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/menu/bottom_nav.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 9 | 14 | 15 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/menu/feed.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------