├── tests
├── .gitignore
├── consumer-rules.pro
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ └── com
│ │ └── bubbble
│ │ └── tests
│ │ ├── mockserver
│ │ ├── ApiTest.kt
│ │ ├── request
│ │ │ ├── MockRequest.kt
│ │ │ ├── BubbbleMockWebServer.kt
│ │ │ ├── QueryParams.kt
│ │ │ └── MockBody.kt
│ │ ├── MockResponseHandler.kt
│ │ ├── MockRequestsDispatcher.kt
│ │ └── MockWebServerExtensions.kt
│ │ ├── extensions
│ │ ├── FileExtensions.kt
│ │ ├── StriktExtensions.kt
│ │ ├── MiscExtensions.kt
│ │ └── TestApiExtensions.kt
│ │ └── di
│ │ └── TestModule.kt
├── proguard-rules.pro
└── build.gradle.kts
├── core
├── core
│ ├── .gitignore
│ ├── consumer-rules.pro
│ ├── build.gradle.kts
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ └── com
│ │ │ └── bubbble
│ │ │ └── core
│ │ │ ├── network
│ │ │ └── NoNetworkException.kt
│ │ │ └── AppContext.kt
│ └── proguard-rules.pro
├── data
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── bubbble
│ │ │ │ └── data
│ │ │ │ ├── di
│ │ │ │ └── DribbbleWebSite.kt
│ │ │ │ ├── shots
│ │ │ │ ├── parser
│ │ │ │ │ ├── ShotsDataNotFoundException.kt
│ │ │ │ │ ├── ShotAdditionalRaw.kt
│ │ │ │ │ └── ShotRaw.kt
│ │ │ │ ├── search
│ │ │ │ │ └── SearchPageParser.kt
│ │ │ │ ├── feed
│ │ │ │ │ └── FeedShotsParser.kt
│ │ │ │ └── ShotsRepository.kt
│ │ │ │ ├── global
│ │ │ │ ├── parsing
│ │ │ │ │ ├── PageDownloadException.kt
│ │ │ │ │ ├── ParsingRegex.kt
│ │ │ │ │ ├── PageDownloader.kt
│ │ │ │ │ ├── PageParserManager.kt
│ │ │ │ │ └── PageParser.kt
│ │ │ │ ├── paging
│ │ │ │ │ ├── PagingParams.kt
│ │ │ │ │ └── CommonPagingSource.kt
│ │ │ │ ├── UserUrlParser.kt
│ │ │ │ ├── prefs
│ │ │ │ │ ├── TempPreferences.kt
│ │ │ │ │ └── TempDataRepository.kt
│ │ │ │ └── filesystem
│ │ │ │ │ └── UrlImageSaver.kt
│ │ │ │ ├── users
│ │ │ │ ├── UsersRepository.kt
│ │ │ │ └── FollowersRepository.kt
│ │ │ │ ├── images
│ │ │ │ └── ImagesRepository.kt
│ │ │ │ └── comments
│ │ │ │ └── CommentsRepository.kt
│ │ └── test
│ │ │ └── java
│ │ │ └── com
│ │ │ └── bubbble
│ │ │ └── data
│ │ │ ├── ComponentHolder.kt
│ │ │ ├── ParserTestComponent.kt
│ │ │ ├── TestModule.kt
│ │ │ └── search
│ │ │ └── SearchPageParserTest.kt
│ └── build.gradle.kts
├── di
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── bubbble
│ │ │ └── di
│ │ │ └── injector
│ │ │ ├── ComponentApi.kt
│ │ │ ├── ComponentDependencies.kt
│ │ │ ├── ComponentHolderManager.kt
│ │ │ ├── ComponentFactory.kt
│ │ │ ├── ComponentHolder.kt
│ │ │ └── ComponentManager.kt
│ │ └── AndroidManifest.xml
├── models
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── bubbble
│ │ │ └── core
│ │ │ └── models
│ │ │ ├── Like.kt
│ │ │ ├── shot
│ │ │ ├── UserType.kt
│ │ │ ├── ShotSortType.kt
│ │ │ ├── ShotDetailsParams.kt
│ │ │ ├── ShotCommentsParams.kt
│ │ │ ├── ShotImage.kt
│ │ │ ├── Shot.kt
│ │ │ └── ShotDetails.kt
│ │ │ ├── search
│ │ │ ├── SearchType.kt
│ │ │ └── SearchParams.kt
│ │ │ ├── user
│ │ │ ├── Links.kt
│ │ │ ├── UserShotsParams.kt
│ │ │ ├── Follow.kt
│ │ │ ├── UserFollowersParams.kt
│ │ │ ├── Team.kt
│ │ │ └── User.kt
│ │ │ ├── Comment.kt
│ │ │ └── feed
│ │ │ └── ShotsFeedParams.kt
│ │ └── AndroidManifest.xml
├── ui
│ ├── .gitignore
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── res
│ │ │ ├── drawable
│ │ │ │ ├── ic_search.xml
│ │ │ │ ├── ic_share.xml
│ │ │ │ └── ic_bucket.xml
│ │ │ ├── values
│ │ │ │ ├── strings.xml
│ │ │ │ └── colors.xml
│ │ │ └── values-ru
│ │ │ │ └── strings.xml
│ │ │ └── java
│ │ │ └── com
│ │ │ └── bubbble
│ │ │ └── ui
│ │ │ └── extensions
│ │ │ └── ui_extensions.kt
│ └── build.gradle.kts
└── network
│ ├── .gitignore
│ ├── src
│ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── bubbble
│ │ │ └── core
│ │ │ └── network
│ │ │ ├── di
│ │ │ ├── BaseApiUrl.kt
│ │ │ ├── ApiInterceptors.kt
│ │ │ └── ApiNetworkInterceptors.kt
│ │ │ ├── exceptions
│ │ │ ├── SessionExpiredException.java
│ │ │ └── HttpException.java
│ │ │ ├── utils
│ │ │ └── extensions.kt
│ │ │ ├── NetworkChecker.java
│ │ │ ├── GsonProvider.kt
│ │ │ ├── interceptors
│ │ │ ├── DribbbleTokenInterceptor.java
│ │ │ └── NetworkCheckInterceptor.java
│ │ │ ├── OkHttpProvider.kt
│ │ │ ├── ApiConstants.kt
│ │ │ └── ApiBuilder.kt
│ │ └── AndroidManifest.xml
│ └── build.gradle.kts
├── app-mvp
├── app
│ ├── .gitignore
│ ├── src
│ │ ├── main
│ │ │ ├── res
│ │ │ │ ├── mipmap-hdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-mdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values
│ │ │ │ │ ├── ids.xml
│ │ │ │ │ ├── dimens.xml
│ │ │ │ │ └── strings.xml
│ │ │ │ ├── mipmap-xxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── mipmap-xxxhdpi
│ │ │ │ │ └── ic_launcher.png
│ │ │ │ ├── values-v19
│ │ │ │ │ └── dimens.xml
│ │ │ │ ├── drawable
│ │ │ │ │ ├── ic_user.xml
│ │ │ │ │ ├── ic_share_arrow.xml
│ │ │ │ │ └── ic_view.xml
│ │ │ │ └── values-ru
│ │ │ │ │ └── strings.xml
│ │ │ ├── java
│ │ │ │ └── com
│ │ │ │ │ └── bubbble
│ │ │ │ │ ├── di
│ │ │ │ │ ├── qualifiers
│ │ │ │ │ │ ├── OkHttpInterceptors.kt
│ │ │ │ │ │ └── OkHttpNetworkInterceptors.kt
│ │ │ │ │ ├── AppEntryPoint.kt
│ │ │ │ │ └── AppModule.kt
│ │ │ │ │ ├── BubbbleApplication.kt
│ │ │ │ │ └── presentation
│ │ │ │ │ └── global
│ │ │ │ │ └── navigation
│ │ │ │ │ └── AppSettingsScreen.kt
│ │ │ └── AndroidManifest.xml
│ │ ├── unitTests
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── bubbble
│ │ │ │ ├── BubbbleTestApplication.java
│ │ │ │ ├── test
│ │ │ │ ├── TestSchedulersProvider.java
│ │ │ │ └── BubbbleTestRunner.java
│ │ │ │ └── presentation
│ │ │ │ └── mvp
│ │ │ │ └── presenters
│ │ │ │ └── MainPresenterTest.java
│ │ ├── debug
│ │ │ ├── AndroidManifest.xml
│ │ │ └── java
│ │ │ │ └── com
│ │ │ │ └── bubbble
│ │ │ │ ├── DebugBubbleApplication.java
│ │ │ │ └── di
│ │ │ │ └── modules
│ │ │ │ └── OkHttpInterceptorsModule.kt
│ │ └── release
│ │ │ └── java
│ │ │ └── com
│ │ │ └── bubbble
│ │ │ └── di
│ │ │ └── modules
│ │ │ └── OkHttpInterceptorsModule.java
│ ├── proguard-rules.pro
│ └── build.gradle.kts
├── core-ui
│ ├── .gitignore
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── bubbble
│ │ │ │ └── coreui
│ │ │ │ ├── permissions
│ │ │ │ ├── PermissionResult.kt
│ │ │ │ └── PermissionsManager.kt
│ │ │ │ ├── resourcesmanager
│ │ │ │ ├── ResourcesManager.kt
│ │ │ │ └── AndroidResourcesManager.kt
│ │ │ │ ├── navigationargs
│ │ │ │ ├── FragmentNavDataProperty.kt
│ │ │ │ ├── ActivityNavDataProperty.kt
│ │ │ │ ├── NavDataProperty.kt
│ │ │ │ ├── BundleNavDataExtractor.kt
│ │ │ │ ├── BundleNavDataSaver.kt
│ │ │ │ └── NavData.kt
│ │ │ │ ├── mvp
│ │ │ │ ├── BaseMvpView.kt
│ │ │ │ └── ErrorHandler.kt
│ │ │ │ ├── ui
│ │ │ │ ├── views
│ │ │ │ │ └── dribbbletextview
│ │ │ │ │ │ ├── UserSpan.kt
│ │ │ │ │ │ ├── LinkSpan.java
│ │ │ │ │ │ └── TouchableUrlSpan.kt
│ │ │ │ ├── commons
│ │ │ │ │ ├── SearchQueryListener.java
│ │ │ │ │ └── glide
│ │ │ │ │ │ └── GlideCircleTransform.java
│ │ │ │ ├── base
│ │ │ │ │ ├── BaseMvpDialogFragment.kt
│ │ │ │ │ ├── BaseMvpFragment.kt
│ │ │ │ │ └── BaseMvpActivity.kt
│ │ │ │ └── adapters
│ │ │ │ │ ├── LoadMoreViewHolder.java
│ │ │ │ │ ├── LoadMoreAdapter.java
│ │ │ │ │ └── LoadingStateAdapter.kt
│ │ │ │ ├── utils
│ │ │ │ ├── ShotComparator.kt
│ │ │ │ └── AppUtils.java
│ │ │ │ └── di
│ │ │ │ ├── CoreUiScope.java
│ │ │ │ ├── CoreUiEntrypoint.kt
│ │ │ │ └── CoreUiModule.kt
│ │ │ └── res
│ │ │ ├── drawable
│ │ │ ├── ic_arrow_back.xml
│ │ │ └── dribbble_lined_logo_pink.xml
│ │ │ ├── values
│ │ │ ├── styles.xml
│ │ │ ├── strings.xml
│ │ │ └── badgedimageview.xml
│ │ │ ├── layout
│ │ │ ├── view_loading.xml
│ │ │ ├── view_empty_list.xml
│ │ │ ├── item_network_state.xml
│ │ │ └── view_network_error.xml
│ │ │ └── values-ru
│ │ │ └── strings.xml
│ └── build.gradle.kts
├── feature-shots
│ ├── .gitignore
│ ├── consumer-rules.pro
│ ├── src
│ │ └── main
│ │ │ ├── AndroidManifest.xml
│ │ │ ├── java
│ │ │ └── com
│ │ │ │ └── bubbble
│ │ │ │ └── shots
│ │ │ │ ├── main
│ │ │ │ ├── MainView.kt
│ │ │ │ ├── MainPresenter.kt
│ │ │ │ └── ShotsPagerAdapter.kt
│ │ │ │ ├── shotslist
│ │ │ │ ├── ShotsScreenData.kt
│ │ │ │ ├── ShotsView.kt
│ │ │ │ └── ShotsPresenter.kt
│ │ │ │ └── api
│ │ │ │ └── ShotsNavigationFactory.kt
│ │ │ └── res
│ │ │ ├── menu
│ │ │ └── main.xml
│ │ │ └── layout
│ │ │ ├── item_shot.xml
│ │ │ ├── fragment_shots.xml
│ │ │ └── activity_main.xml
│ ├── build.gradle.kts
│ └── proguard-rules.pro
├── feature-shot-details
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── res
│ │ ├── drawable
│ │ │ ├── bg_shot_tag.xml
│ │ │ ├── shot_tag_foreground.xml
│ │ │ ├── transparent_toolbar_background.xml
│ │ │ ├── ic_download.xml
│ │ │ ├── ic_close.xml
│ │ │ ├── ic_copy.xml
│ │ │ ├── ic_like.xml
│ │ │ ├── ic_time.xml
│ │ │ └── ic_browser.xml
│ │ ├── menu
│ │ │ ├── shot_details.xml
│ │ │ └── shot_zoom.xml
│ │ ├── layout
│ │ │ └── item_no_comments.xml
│ │ ├── values
│ │ │ └── strings.xml
│ │ └── values-ru
│ │ │ └── strings.xml
│ │ ├── java
│ │ └── com
│ │ │ └── bubbble
│ │ │ └── shotdetails
│ │ │ ├── ShotDescriptionViewHolder.kt
│ │ │ ├── api
│ │ │ ├── ShotDetailsNavigationFactory.kt
│ │ │ ├── ShotDetailsScreen.kt
│ │ │ └── ShotImageZoomScreen.kt
│ │ │ ├── comments
│ │ │ ├── NoCommentsViewHolder.kt
│ │ │ └── CommentViewHolder.kt
│ │ │ ├── shotzoom
│ │ │ └── ShotZoomView.kt
│ │ │ └── ShotDetailsView.kt
│ │ └── AndroidManifest.xml
├── feature-shots-search
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ │ └── main
│ │ ├── java
│ │ └── com
│ │ │ └── bubbble
│ │ │ └── shotsearch
│ │ │ ├── api
│ │ │ ├── ShotSearchNavigationFactory.kt
│ │ │ └── ShotsSearchScreen.kt
│ │ │ └── ShotsSearchView.kt
│ │ ├── AndroidManifest.xml
│ │ └── res
│ │ ├── menu
│ │ └── shots_search.xml
│ │ └── layout
│ │ ├── item_search_shot.xml
│ │ └── activity_shots_search.xml
└── feature-user-profile
│ ├── .gitignore
│ ├── build.gradle.kts
│ └── src
│ └── main
│ ├── java
│ └── com
│ │ └── bubbble
│ │ └── userprofile
│ │ ├── shots
│ │ ├── UserShotsScreenData.kt
│ │ └── UserShotsView.kt
│ │ ├── details
│ │ ├── UserDetailsScreenData.kt
│ │ ├── UserDetailsView.kt
│ │ └── UserDetailsPresenter.kt
│ │ ├── followers
│ │ ├── UserFollowersScreenData.kt
│ │ └── UserFollowersView.kt
│ │ ├── api
│ │ ├── UserProfileNavigationFactory.kt
│ │ └── UserProfileScreen.kt
│ │ ├── UserProfileView.kt
│ │ └── UserProfilePagerAdapter.kt
│ ├── res
│ ├── values
│ │ ├── dimens.xml
│ │ └── strings.xml
│ ├── drawable
│ │ ├── circle_button_pink_selector.xml
│ │ ├── ic_folder.xml
│ │ ├── ic_follow.xml
│ │ ├── ic_location.xml
│ │ ├── ic_like.xml
│ │ ├── circle_button_pink.xml
│ │ ├── circle_button_pink_pressed.xml
│ │ ├── ic_user_lined.xml
│ │ ├── ic_web.xml
│ │ ├── ic_twitter.xml
│ │ └── ic_shot.xml
│ ├── menu
│ │ └── user_profile.xml
│ ├── values-ru
│ │ └── strings.xml
│ └── layout
│ │ ├── fragment_user_shots.xml
│ │ └── fragment_followers.xml
│ └── AndroidManifest.xml
├── keystore.jks
├── art
├── banner.png
├── screenshot_1.png
├── screenshot_2.png
├── screenshot_3.png
└── screenshot_4.png
├── gradle
├── base
│ ├── module-config.gradle.kts
│ ├── app-config.gradle.kts
│ └── base-config.gradle.kts
├── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── libs
│ └── hilt-config.gradle.kts
├── mvp-feature-module.gradle.kts
└── tests
│ ├── unit-tests-config.gradle.kts
│ └── ui-tests-config.gradle.kts
├── .gitignore
├── settings.gradle.kts
├── gradle.properties
└── commons
└── dribbble_lined_logo.svg
/tests/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/core/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/core/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/core/data/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/di/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/models/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/ui/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/tests/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app-mvp/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/network/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app-mvp/feature-shots/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app-mvp/feature-shots/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/core/di/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 |
3 | }
--------------------------------------------------------------------------------
/core/models/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 | api(Dependencies.gson)
3 | }
--------------------------------------------------------------------------------
/keystore.jks:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/keystore.jks
--------------------------------------------------------------------------------
/art/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/art/banner.png
--------------------------------------------------------------------------------
/art/screenshot_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/art/screenshot_1.png
--------------------------------------------------------------------------------
/art/screenshot_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/art/screenshot_2.png
--------------------------------------------------------------------------------
/art/screenshot_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/art/screenshot_3.png
--------------------------------------------------------------------------------
/art/screenshot_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/art/screenshot_4.png
--------------------------------------------------------------------------------
/core/core/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("kapt")
3 | }
4 |
5 | dependencies {
6 |
7 | }
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/Like.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models
2 |
3 | class Like
--------------------------------------------------------------------------------
/core/ui/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/tests/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/core/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/gradle/base/module-config.gradle.kts:
--------------------------------------------------------------------------------
1 | apply(plugin="com.android.library")
2 | baseScriptFrom(BuildScript.baseConfig)
3 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/core/di/src/main/java/com/bubbble/di/injector/ComponentApi.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.injector
2 |
3 | interface ComponentApi
--------------------------------------------------------------------------------
/core/di/src/main/java/com/bubbble/di/injector/ComponentDependencies.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.injector
2 |
3 | interface ComponentDependencies
--------------------------------------------------------------------------------
/core/di/src/main/java/com/bubbble/di/injector/ComponentHolderManager.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.injector
2 |
3 | class ComponentHolderManager {
4 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/app-mvp/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/app-mvp/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/app-mvp/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/values/ids.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/*
5 | .DS_Store
6 | /build
7 | /captures
8 | .externalNativeBuild
9 | /gradle.properties
10 |
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/app-mvp/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ImangazalievM/Bubbble/HEAD/app-mvp/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/shot/UserType.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.shot
2 |
3 | enum class UserType {
4 | DEFAULT, TEAM, PRO
5 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation(project(Modules.Core.data))
3 | implementation(project(Modules.AppMvp.coreUi))
4 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation(project(Modules.Core.data))
3 | implementation(project(Modules.AppMvp.coreUi))
4 | }
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/shot/ShotSortType.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.shot
2 |
3 | enum class ShotSortType {
4 | POPULAR, RECENT
5 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/values-v19/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 24dp
4 |
5 |
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/search/SearchType.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.search
2 |
3 | enum class SearchType {
4 | SHOT, MEMBERS, TEAM
5 | }
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/shot/ShotDetailsParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.shot
2 |
3 | class ShotDetailsParams(
4 | val shotSlug: String
5 | )
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/user/Links.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.user
2 |
3 | class Links(
4 | val web: String,
5 | val twitter: String
6 | )
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/shots/UserShotsScreenData.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.shots
2 |
3 | class UserShotsScreenData(
4 | val userName: String
5 | )
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/di/BaseApiUrl.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | annotation class BaseApiUrl
7 |
--------------------------------------------------------------------------------
/core/data/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/details/UserDetailsScreenData.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.details
2 |
3 | class UserDetailsScreenData(
4 | val userName: String
5 | )
--------------------------------------------------------------------------------
/core/di/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/di/ApiInterceptors.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | annotation class ApiInterceptors
7 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/core/di/src/main/java/com/bubbble/di/injector/ComponentFactory.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.injector
2 |
3 | interface ComponentFactory {
4 | fun create(componentManager: ComponentManager): T
5 | }
--------------------------------------------------------------------------------
/core/models/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/followers/UserFollowersScreenData.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.followers
2 |
3 | class UserFollowersScreenData(
4 | val userName: String
5 | )
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/di/ApiNetworkInterceptors.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @Qualifier
6 | annotation class ApiNetworkInterceptors
7 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation(project(Modules.Core.data))
3 | implementation(project(Modules.AppMvp.coreUi))
4 |
5 | implementation(Dependencies.hashtagView)
6 | }
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/user/UserShotsParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.user
2 |
3 | class UserShotsParams(
4 | val userName: String,
5 | val page: Int,
6 | val pageSize: Int
7 | )
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/exceptions/SessionExpiredException.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.exceptions;
2 |
3 | public class SessionExpiredException extends RuntimeException {
4 |
5 | }
6 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/permissions/PermissionResult.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.permissions
2 |
3 | class PermissionResult(
4 | val isGranted: Boolean,
5 | val isBlockedFromAsking: Boolean
6 | )
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/shot/ShotCommentsParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.shot
2 |
3 | class ShotCommentsParams(
4 | val shotSlug: String,
5 | val page: Int,
6 | val pageSize: Int
7 | )
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/user/Follow.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.user
2 |
3 | import java.util.*
4 |
5 | class Follow(
6 | val id: Long,
7 | val dateCreated: Date,
8 | val follower: User
9 | )
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/user/UserFollowersParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.user
2 |
3 | class UserFollowersParams(
4 | val userName: String,
5 | val page: Int,
6 | val pageSize: Int
7 | )
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 112dp
4 | 112dp
5 |
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/shot/ShotImage.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.shot
2 |
3 | class ShotImage(
4 | val imageUrl: String,
5 | val size: Size
6 | ) {
7 | class Size(val width: Int, val height: Int)
8 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/ApiTest.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver
2 |
3 | import com.bubbble.tests.mockserver.request.BubbbleMockWebServer
4 |
5 | class ApiTest(
6 | val webServer: BubbbleMockWebServer
7 | )
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/di/DribbbleWebSite.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.di
2 |
3 | import javax.inject.Qualifier
4 |
5 | @MustBeDocumented
6 | @Qualifier
7 | @Retention(AnnotationRetention.RUNTIME)
8 | annotation class DribbbleWebSite
--------------------------------------------------------------------------------
/app-mvp/app/src/main/java/com/bubbble/di/qualifiers/OkHttpInterceptors.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | @MustBeDocumented
6 | @Qualifier
7 | @Retention(AnnotationRetention.RUNTIME)
8 | annotation class OkHttpInterceptors
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/shots/parser/ShotsDataNotFoundException.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.shots.parser
2 |
3 | import java.lang.IllegalArgumentException
4 |
5 | class ShotsDataNotFoundException(message: String? = null) : IllegalArgumentException(message)
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/extensions/FileExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.extensions
2 |
3 | fun Any.readTextFile(path: String): String {
4 | val loader = javaClass.classLoader!!
5 | return loader.getResourceAsStream(path).reader().readText()
6 | }
--------------------------------------------------------------------------------
/core/core/src/main/java/com/bubbble/core/network/NoNetworkException.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network
2 |
3 | import java.io.IOException
4 |
5 | class NoNetworkException(
6 | message: String? = null,
7 | error: Throwable? = null
8 | ) : IOException(message, error)
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/extensions/StriktExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.extensions
2 |
3 | import strikt.api.expectThat
4 | import strikt.assertions.isEqualTo
5 |
6 | fun T.assertEquals(value: T?) {
7 | expectThat(this).isEqualTo(value)
8 | }
9 |
--------------------------------------------------------------------------------
/app-mvp/app/src/main/java/com/bubbble/di/qualifiers/OkHttpNetworkInterceptors.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.qualifiers
2 |
3 | import javax.inject.Qualifier
4 |
5 | @MustBeDocumented
6 | @Qualifier
7 | @Retention(AnnotationRetention.RUNTIME)
8 | annotation class OkHttpNetworkInterceptors
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/java/com/bubbble/shots/main/MainView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shots.main
2 |
3 | import com.bubbble.coreui.mvp.BaseMvpView
4 | import moxy.MvpView
5 | import moxy.viewstate.strategy.alias.AddToEndSingle
6 |
7 | @AddToEndSingle
8 | interface MainView : BaseMvpView
--------------------------------------------------------------------------------
/app-mvp/app/src/main/java/com/bubbble/di/AppEntryPoint.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di
2 |
3 | import dagger.hilt.EntryPoint
4 | import dagger.hilt.InstallIn
5 | import dagger.hilt.components.SingletonComponent
6 |
7 | @EntryPoint
8 | @InstallIn(SingletonComponent::class)
9 | interface AppEntryPoint
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/resourcesmanager/ResourcesManager.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.resourcesmanager
2 |
3 | interface ResourcesManager {
4 |
5 | fun getString(resourceId: Int, vararg args: Any): String
6 |
7 | fun getInteger(resourceId: Int): Int
8 |
9 | }
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Aug 24 22:55:49 MSK 2021
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/user/Team.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.user
2 |
3 | class Team {
4 | var id: Long? = null
5 | var name: String? = null
6 | var username: String? = null
7 | var htmlUrl: String? = null
8 | var avatarUrl: String? = null
9 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/src/main/java/com/bubbble/shotsearch/api/ShotSearchNavigationFactory.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotsearch.api
2 |
3 | import com.github.terrakok.cicerone.Screen
4 |
5 | interface ShotSearchNavigationFactory {
6 |
7 | fun shotDetailsScreen(shotSlug: String): Screen
8 |
9 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/java/com/bubbble/shots/shotslist/ShotsScreenData.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shots.shotslist
2 |
3 | import com.bubbble.core.models.feed.ShotsFeedParams
4 | import java.io.Serializable
5 |
6 | class ShotsScreenData(
7 | val sort: ShotsFeedParams.Sorting
8 | ) : Serializable
--------------------------------------------------------------------------------
/app-mvp/feature-shots/build.gradle.kts:
--------------------------------------------------------------------------------
1 | dependencies {
2 | implementation(project(Modules.Core.data))
3 | implementation(project(Modules.AppMvp.coreUi))
4 |
5 | implementation(Dependencies.materialDrawer) {
6 | isTransitive = true
7 | exclude(group = "com.android.support")
8 | }
9 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/api/UserProfileNavigationFactory.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.api
2 |
3 | import com.github.terrakok.cicerone.Screen
4 |
5 | interface UserProfileNavigationFactory {
6 |
7 | fun shotDetailsScreen(shotSlug: String): Screen
8 |
9 | }
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/search/SearchParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.search
2 |
3 | import com.bubbble.core.models.shot.ShotSortType
4 |
5 | class SearchParams(
6 | val searchQuery: String,
7 | val searchType: SearchType = SearchType.SHOT,
8 | val sort: ShotSortType
9 | )
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/bg_shot_tag.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/core/data/src/test/java/com/bubbble/data/ComponentHolder.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data
2 |
3 | import com.bubbble.core.network.Dribbble
4 |
5 | object ComponentHolder {
6 |
7 | val component = DaggerParserTestComponent.builder()
8 | .testModule(TestModule(Dribbble.URL))
9 | .build()
10 |
11 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/ShotDescriptionViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails
2 |
3 | import android.view.View
4 | import androidx.recyclerview.widget.RecyclerView
5 |
6 | internal class ShotDescriptionViewHolder(
7 | itemView: View
8 | ) : RecyclerView.ViewHolder(itemView)
--------------------------------------------------------------------------------
/gradle/libs/hilt-config.gradle.kts:
--------------------------------------------------------------------------------
1 | apply {
2 | plugin("kotlin-kapt")
3 | }
4 |
5 | val implementation by configurations
6 | val kapt by configurations
7 |
8 | dependencies {
9 | implementation(Dependencies.hiltAndroid)
10 | kapt(Dependencies.hiltCompiler)
11 | }
12 |
13 | apply(plugin = Build.Plugins.hilt)
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/parsing/PageDownloadException.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.parsing
2 |
3 | class PageDownloadException(
4 | message: String,
5 | val httpCode: Int,
6 | val response: String?,
7 | ) : RuntimeException("$message\n\nHTTP code: $httpCode. \nResponse: $response") {
8 |
9 | }
--------------------------------------------------------------------------------
/core/network/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("kapt")
3 | }
4 |
5 | dependencies {
6 | implementation(project(Modules.Core.core))
7 | implementation(project(Modules.Core.models))
8 |
9 | api(Dependencies.okHttp)
10 | api(Dependencies.retrofit)
11 | api(Dependencies.retrofitGsonConverter)
12 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app-mvp/app/src/unitTests/java/com/bubbble/BubbbleTestApplication.java:
--------------------------------------------------------------------------------
1 | package com.bubbble;
2 |
3 | import com.bubbble.di.AppEntryPoint;
4 |
5 | public class BubbbleTestApplication extends BubbbleApplication {
6 |
7 | @Override
8 | public ApplicationComponent buildComponent() {
9 | return null;
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/java/com/bubbble/shots/api/ShotsNavigationFactory.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shots.api
2 |
3 | import com.github.terrakok.cicerone.Screen
4 |
5 | interface ShotsNavigationFactory {
6 |
7 | fun shotDetailsScreen(shotSlug: String): Screen
8 |
9 | fun shotsSearchScreen(query: String): Screen
10 |
11 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0dp
4 | 14dp
5 | 300dp
6 | 168dp
7 |
8 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/shot_tag_foreground.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
7 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/paging/PagingParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.paging
2 |
3 | import androidx.paging.PagingSource
4 |
5 | class PagingParams(
6 | val page: Int,
7 | val pageSize: Int
8 | )
9 |
10 | val PagingSource.LoadParams.pagingParams: PagingParams
11 | get() = PagingParams(key ?: 1, loadSize)
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/navigationargs/FragmentNavDataProperty.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.navigationargs
2 |
3 | import android.os.Bundle
4 | import androidx.fragment.app.Fragment
5 |
6 | class FragmentNavDataProperty : NavDataProperty() {
7 | override fun getData(thisRef: Fragment): Bundle? = thisRef.arguments
8 | }
--------------------------------------------------------------------------------
/core/network/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/core/ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("kapt")
3 | }
4 |
5 | android {
6 | buildFeatures.viewBinding = true
7 | }
8 |
9 | dependencies {
10 | api(Dependencies.androidxCoreKtx)
11 | api(Dependencies.androidxAppCompat)
12 | api(Dependencies.googleMaterial)
13 | api(Dependencies.paging)
14 |
15 | api(project(Modules.Core.models))
16 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/navigationargs/ActivityNavDataProperty.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.navigationargs
2 |
3 | import android.app.Activity
4 | import android.os.Bundle
5 |
6 | class ActivityNavDataProperty : NavDataProperty() {
7 | override fun getData(thisRef: Activity): Bundle? = thisRef.intent.extras
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/Comment.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models
2 |
3 | import com.bubbble.core.models.user.User
4 | import java.util.*
5 |
6 | class Comment(
7 | val id: Long,
8 | val user: User,
9 | val body: String,
10 | val createdAt: Date,
11 | val updatedAt: Date,
12 | val likesUrl: String,
13 | val likeCount: Int
14 | )
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/transparent_toolbar_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/extensions/MiscExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.extensions
2 |
3 | import kotlinx.coroutines.CoroutineScope
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.GlobalScope
6 | import kotlinx.coroutines.launch
7 |
8 | fun Any.launchCo(
9 | block: suspend CoroutineScope.() -> Unit
10 | ) = GlobalScope.launch(Dispatchers.Default) { block() }
--------------------------------------------------------------------------------
/core/core/src/main/java/com/bubbble/core/AppContext.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Application
5 | import android.content.Context
6 |
7 | @SuppressLint("StaticFieldLeak")
8 | object AppContext {
9 | lateinit var instance: Context
10 | private set
11 |
12 | fun setContext(app: Application) {
13 | instance = app
14 | }
15 |
16 | }
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | include(
2 | ":core:core",
3 | ":core:di",
4 | ":core:models",
5 | ":core:network",
6 | ":core:data",
7 | ":core:ui",
8 |
9 | //app-mvp
10 | ":app-mvp:app",
11 | ":app-mvp:core-ui",
12 | ":app-mvp:feature-shots",
13 | ":app-mvp:feature-shots-search",
14 | ":app-mvp:feature-shot-details",
15 | ":app-mvp:feature-user-profile",
16 |
17 | ":tests"
18 | )
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/ic_download.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/circle_button_pink_selector.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/UserUrlParser.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global
2 |
3 | import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
4 | import javax.inject.Inject
5 |
6 | class UserUrlParser @Inject constructor() {
7 |
8 | fun getUserName(url: String): String? {
9 | return url.toHttpUrlOrNull()
10 | ?.pathSegments
11 | ?.get(0)
12 | }
13 |
14 | }
15 |
16 |
17 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/shots/parser/ShotAdditionalRaw.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.shots.parser
2 |
3 | import com.bubbble.core.models.shot.UserType
4 |
5 | data class ShotAdditionalRaw(
6 | val imageUrl: String,
7 | val shotUrl: String,
8 | val userName: String,
9 | val userDisplayName: String,
10 | val userAvatarUrl: String,
11 | val userUrl: String,
12 | val userType: UserType
13 | )
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/drawable/ic_arrow_back.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/feed/ShotsFeedParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.feed
2 |
3 | class ShotsFeedParams(
4 | val sort: Sorting?
5 | ) {
6 |
7 | enum class Sorting {
8 | PERSONAL, POPULAR, RECENT;
9 |
10 | companion object {
11 | fun find(name: String) = values().firstOrNull {
12 | it.name == name
13 | }
14 | }
15 | }
16 |
17 | }
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/parsing/ParsingRegex.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.parsing
2 |
3 | object ParsingRegex {
4 | const val JS_OBJECT_REGEX =
5 | "(\\{(?:(?>[^{}\"'\\/]+)|(?>\"(?:(?>[^\\\\\"]+)|\\\\.)*\")|(?>'(?:(?>[^\\\\']+)|\\\\.)*')|(?>\\/\\/.*\\n)|(?>\\/\\*.*?\\*\\/))*\\})"
6 | const val JS_ARRAY_REGEX = "\\[($JS_OBJECT_REGEX(,?))*\\]"
7 | const val JS_OBJECT_KEY_REGEX = "([a-z_]*)(\\:.*,?\\n)"
8 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/di/TestModule.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.di
2 |
3 | import com.bubbble.core.network.GsonProvider
4 | import com.google.gson.Gson
5 | import dagger.Module
6 | import dagger.Provides
7 | import javax.inject.Singleton
8 |
9 | @Module
10 | class TestModule {
11 |
12 | @Provides
13 | @Singleton
14 | fun provideGson(provider: GsonProvider): Gson {
15 | return provider.create()
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/layout/view_loading.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
12 |
13 |
--------------------------------------------------------------------------------
/gradle/mvp-feature-module.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.jetbrains.kotlin.gradle.plugin.KaptExtension
2 | import com.android.build.gradle.BaseExtension
3 |
4 | apply {
5 | plugin("kotlin-kapt")
6 | }
7 | baseScriptFrom(BuildScript.moduleConfig)
8 | baseScriptFrom(BuildScript.hiltConfig)
9 |
10 | configure {
11 | buildFeatures.viewBinding = true
12 | }
13 |
14 | val kapt by configurations
15 | dependencies {
16 | kapt(Dependencies.Mvp.moxyCompiler)
17 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/mvp/BaseMvpView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.mvp
2 |
3 | import moxy.MvpView
4 | import moxy.viewstate.strategy.OneExecutionStateStrategy
5 | import moxy.viewstate.strategy.StateStrategyType
6 | import moxy.viewstate.strategy.alias.OneExecution
7 |
8 | interface BaseMvpView : MvpView {
9 |
10 | @OneExecution
11 | fun showMessage(text: String)
12 |
13 | @OneExecution
14 | fun showMessage(textResId: Int)
15 |
16 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/ic_close.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/drawable/ic_user.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_folder.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/data/src/test/java/com/bubbble/data/ParserTestComponent.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data
2 |
3 | import com.bubbble.core.network.di.BaseApiUrl
4 | import com.bubbble.data.global.parsing.PageParserManager
5 | import dagger.Component
6 | import javax.inject.Singleton
7 |
8 | @Singleton
9 | @Component(modules = [TestModule::class])
10 | interface ParserTestComponent {
11 |
12 | fun pageParserManager(): PageParserManager
13 |
14 | @BaseApiUrl
15 | fun baseUrl(): String
16 |
17 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/drawable/ic_share_arrow.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/views/dribbbletextview/UserSpan.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.views.dribbbletextview
2 |
3 | import android.content.res.ColorStateList
4 | import android.view.View
5 |
6 | abstract class UserSpan(
7 | url: String,
8 | textColor: ColorStateList
9 | ) : TouchableUrlSpan(url, textColor) {
10 |
11 | override fun onClick(widget: View) {
12 | onClick(url)
13 | }
14 |
15 | abstract fun onClick(url: String?)
16 |
17 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Popular
3 | Recent
4 |
5 | Share via
6 |
7 | Unknown error
8 | Internet connection error
9 | Server connection error. Please check the time on your device
10 |
11 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/api/ShotDetailsNavigationFactory.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails.api
2 |
3 | import com.github.terrakok.cicerone.Screen
4 |
5 | interface ShotDetailsNavigationFactory {
6 |
7 | fun userProfileScreen(userName: String): Screen
8 |
9 | fun shotImageScreen(
10 | title: String,
11 | shotUrl: String,
12 | imageUrl: String
13 | ): Screen
14 |
15 | fun appSettingsScreen(): Screen
16 |
17 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/res/menu/main.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/comments/NoCommentsViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails.comments
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.recyclerview.widget.RecyclerView
6 | import com.bubbble.shotdetails.R
7 |
8 | internal class NoCommentsViewHolder(
9 | layoutInflater: LayoutInflater,
10 | parent: ViewGroup
11 | ) : RecyclerView.ViewHolder(layoutInflater.inflate(R.layout.item_no_comments, parent, false))
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/src/main/res/menu/shots_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/commons/SearchQueryListener.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.commons;
2 |
3 | import androidx.appcompat.widget.SearchView;
4 |
5 | public class SearchQueryListener implements SearchView.OnQueryTextListener {
6 |
7 | @Override
8 | public boolean onQueryTextSubmit(String query) {
9 | return false;
10 | }
11 |
12 | @Override
13 | public boolean onQueryTextChange(String newText) {
14 | return false;
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/utils/ShotComparator.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.utils
2 |
3 | import androidx.recyclerview.widget.DiffUtil
4 | import com.bubbble.core.models.shot.Shot
5 |
6 | object ShotComparator : DiffUtil.ItemCallback() {
7 | override fun areItemsTheSame(oldItem: Shot, newItem: Shot): Boolean {
8 | return oldItem.id == newItem.id
9 | }
10 |
11 | override fun areContentsTheSame(oldItem: Shot, newItem: Shot): Boolean {
12 | return oldItem == newItem
13 | }
14 | }
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/users/UsersRepository.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.users
2 |
3 | import com.bubbble.core.models.user.User
4 | import com.bubbble.core.network.DribbbleApi
5 | import javax.inject.Inject
6 | import javax.inject.Singleton
7 |
8 | @Singleton
9 | class UsersRepository @Inject constructor(
10 | private val dribbbleApi: DribbbleApi
11 | ) {
12 |
13 | suspend fun getUser(userName: String): User {
14 | TODO()
15 | //return dribbbleApi.getUser(userName)
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/utils/extensions.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.utils
2 |
3 | import com.bubbble.core.network.Dribbble
4 | import okhttp3.HttpUrl
5 | import java.net.URL
6 |
7 | fun urlBuilder(url: String): HttpUrl.Builder {
8 | val url = URL(Dribbble.Search.path)
9 | val builder = HttpUrl.Builder()
10 | .scheme(url.protocol)
11 | .host(url.host)
12 | url.path.split("/").forEach {
13 | builder.addPathSegment(it)
14 | }
15 | return builder
16 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/di/CoreUiScope.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.di;
2 |
3 | import java.lang.annotation.Documented;
4 | import java.lang.annotation.Retention;
5 |
6 | import javax.inject.Scope;
7 |
8 | import static java.lang.annotation.RetentionPolicy.RUNTIME;
9 |
10 | /**
11 | * Identifies a type that the injector only instantiates once. Not inherited.
12 | *
13 | * @see javax.inject.Scope @Scope
14 | */
15 | @Scope
16 | @Documented
17 | @Retention(RUNTIME)
18 | public @interface CoreUiScope {}
19 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Популярные
3 | Недавние
4 |
5 | Поделиться с помощью
6 |
7 | Неизвестная ошибка
8 | Ошибка подключения к интернету
9 | Ошибка при подключении к серверу. Пожалуйста, проверьте время на вашем устройстве.
10 |
11 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/ic_copy.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/ic_like.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_follow.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_location.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_like.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/menu/shot_details.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/ic_time.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/gradle/base/app-config.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.BaseExtension
2 |
3 | apply {
4 | plugin("com.android.application")
5 | plugin("kotlin-kapt")
6 | }
7 |
8 | baseScriptFrom(BuildScript.baseConfig)
9 | baseScriptFrom(BuildScript.hiltConfig)
10 |
11 | configure {
12 | defaultConfig {
13 | applicationId = "com.bubbble"
14 | versionCode = Build.Versions.appVersionCode
15 | versionName = Build.Versions.appVersion
16 |
17 | multiDexEnabled = true
18 | }
19 |
20 | buildFeatures.viewBinding = true
21 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/java/com/bubbble/BubbbleApplication.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble
2 |
3 | import android.app.Application
4 | import com.bubbble.core.AppContext
5 | import dagger.hilt.android.HiltAndroidApp
6 |
7 | @HiltAndroidApp
8 | open class BubbbleApplication : Application() {
9 |
10 | companion object {
11 | lateinit var instance: BubbbleApplication
12 | private set
13 |
14 | }
15 |
16 | override fun onCreate() {
17 | super.onCreate()
18 |
19 | AppContext.setContext(this)
20 |
21 | instance = this
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/views/dribbbletextview/LinkSpan.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.views.dribbbletextview;
2 |
3 | import android.content.res.ColorStateList;
4 | import android.view.View;
5 |
6 | public abstract class LinkSpan extends TouchableUrlSpan {
7 |
8 | public LinkSpan(String url, ColorStateList textColor) {
9 | super(url, textColor);
10 | }
11 |
12 | @Override
13 | public void onClick(View widget) {
14 | onClick(getURL());
15 | }
16 |
17 | public abstract void onClick(String url);
18 |
19 |
20 | }
--------------------------------------------------------------------------------
/core/data/src/test/java/com/bubbble/data/TestModule.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data
2 |
3 | import com.bubbble.core.network.di.BaseApiUrl
4 | import dagger.Module
5 | import dagger.Provides
6 | import okhttp3.OkHttpClient
7 | import javax.inject.Singleton
8 |
9 | @Module
10 | class TestModule(
11 | private val dribbbleUrl: String
12 | ) {
13 |
14 | @Provides
15 | @Singleton
16 | @BaseApiUrl
17 | fun dribbbleUrl(): String = dribbbleUrl
18 |
19 | @Provides
20 | @Singleton
21 | fun okHttpBuilder(): OkHttpClient = OkHttpClient.Builder().build()
22 |
23 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/java/com/bubbble/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di
2 |
3 | import android.content.Context
4 | import dagger.Module
5 | import dagger.Provides
6 | import dagger.hilt.InstallIn
7 | import dagger.hilt.android.qualifiers.ApplicationContext
8 | import dagger.hilt.components.SingletonComponent
9 | import javax.inject.Singleton
10 |
11 | @Module
12 | @InstallIn(SingletonComponent::class)
13 | class AppModule {
14 |
15 | @Provides
16 | @Singleton
17 | fun applicationContext(
18 | @ApplicationContext context: Context
19 | ): Context = context
20 |
21 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/drawable/ic_view.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/circle_button_pink.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/images/ImagesRepository.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.images
2 |
3 | import com.bubbble.data.global.filesystem.UrlImageSaver
4 | import kotlinx.coroutines.Dispatchers
5 | import kotlinx.coroutines.withContext
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class ImagesRepository @Inject constructor(
11 | private val urlImageSaver: UrlImageSaver
12 | ) {
13 |
14 | suspend fun saveImage(shotImageUrl: String) = withContext(Dispatchers.IO) {
15 | urlImageSaver.saveImage(shotImageUrl)
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/circle_button_pink_pressed.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | -
5 |
6 |
7 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/navigationargs/NavDataProperty.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.navigationargs
2 |
3 | import android.os.Bundle
4 | import kotlin.properties.ReadOnlyProperty
5 | import kotlin.reflect.KProperty
6 |
7 | abstract class NavDataProperty : ReadOnlyProperty {
8 |
9 | private val extractor = BundleNavDataExtractor()
10 |
11 | override fun getValue(thisRef: Source, property: KProperty<*>): Data {
12 | return extractor.getData(getData(thisRef))
13 | }
14 |
15 | abstract fun getData(thisRef: Source): Bundle?
16 |
17 | }
--------------------------------------------------------------------------------
/core/ui/src/main/res/drawable/ic_search.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/resourcesmanager/AndroidResourcesManager.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.resourcesmanager
2 |
3 | import android.content.Context
4 | import javax.inject.Inject
5 |
6 | class AndroidResourcesManager @Inject constructor(
7 | private val context: Context
8 | ) : ResourcesManager {
9 |
10 | override fun getString(resourceId: Int, vararg args: Any): String {
11 | return context.resources.getString(resourceId, *args)
12 | }
13 |
14 | override fun getInteger(resourceId: Int): Int {
15 | return context.resources.getInteger(resourceId)
16 | }
17 |
18 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/shots/UserShotsView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.shots
2 |
3 | import com.bubbble.core.models.shot.Shot
4 | import com.bubbble.coreui.mvp.BaseMvpView
5 | import moxy.viewstate.strategy.alias.AddToEndSingle
6 |
7 | @AddToEndSingle
8 | interface UserShotsView : BaseMvpView {
9 |
10 | fun showNewShots(newShots: List)
11 |
12 | fun showShotsLoadingProgress(isVisible: Boolean)
13 |
14 | fun showShotsLoadingMoreProgress(isVisible: Boolean)
15 |
16 | fun showNoNetworkLayout(isVisible: Boolean)
17 |
18 | fun showLoadMoreError()
19 |
20 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/src/main/java/com/bubbble/shotsearch/ShotsSearchView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotsearch
2 |
3 | import androidx.paging.PagingData
4 | import com.bubbble.core.models.shot.Shot
5 | import com.bubbble.coreui.mvp.BaseMvpView
6 | import moxy.viewstate.strategy.alias.AddToEndSingle
7 |
8 | @AddToEndSingle
9 | interface ShotsSearchView : BaseMvpView {
10 |
11 | fun showPagingData(pagingData: PagingData)
12 |
13 | fun updateListState(
14 | isProgressBarVisible: Boolean,
15 | isRetryVisible: Boolean,
16 | isErrorMsgVisible: Boolean
17 | )
18 |
19 | fun retryLoading()
20 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/request/MockRequest.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver.request
2 |
3 | import com.google.gson.Gson
4 | import okhttp3.mockwebserver.RecordedRequest
5 |
6 | class MockRequest(
7 | private val gson: Gson,
8 | private val request: RecordedRequest
9 | ) {
10 |
11 | val path: String by lazy { request.path!! }
12 | val method: String? by lazy { request.method }
13 | val queryParams: QueryParams by lazy { QueryParams(path) }
14 | val body: MockBody by lazy { MockBody(gson, request.body) }
15 |
16 | fun getHeader(name: String) = request.getHeader(name)
17 |
18 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/details/UserDetailsView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.details
2 |
3 | import com.bubbble.core.models.user.User
4 | import com.bubbble.coreui.mvp.BaseMvpView
5 | import moxy.viewstate.strategy.alias.AddToEndSingle
6 | import moxy.viewstate.strategy.alias.OneExecution
7 |
8 | @AddToEndSingle
9 | interface UserDetailsView : BaseMvpView {
10 |
11 | fun showUserInfo(user: User)
12 |
13 | fun showLoadingProgress(isVisible: Boolean)
14 |
15 | fun showNoNetworkLayout(isVisible: Boolean)
16 |
17 | @OneExecution
18 | fun openInBrowser(url: String)
19 |
20 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/menu/user_profile.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/core/ui/src/main/java/com/bubbble/ui/extensions/ui_extensions.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.ui.extensions
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.annotation.LayoutRes
7 | import androidx.core.view.isVisible
8 | import androidx.viewbinding.ViewBinding
9 |
10 | fun ViewGroup.inflate(@LayoutRes layoutRes: Int, attachToRoot: Boolean = false): View {
11 | return LayoutInflater.from(context).inflate(layoutRes, this, attachToRoot)
12 | }
13 |
14 | var ViewBinding.isVisible: Boolean
15 | get() = root.isVisible
16 | set(value) {
17 | root.isVisible = value
18 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_user_lined.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/followers/UserFollowersView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.followers
2 |
3 | import com.bubbble.core.models.user.Follow
4 | import com.bubbble.coreui.mvp.BaseMvpView
5 | import moxy.viewstate.strategy.alias.AddToEndSingle
6 |
7 | @AddToEndSingle
8 | interface UserFollowersView : BaseMvpView {
9 |
10 | fun showNewFollowers(newFollowers: List)
11 |
12 | fun showFollowersLoadingProgress(isVisible: Boolean)
13 |
14 | fun showFollowersLoadingMoreProgress(isVisible: Boolean)
15 |
16 | fun showNoNetworkLayout(isVisible: Boolean)
17 |
18 | fun showLoadMoreError()
19 |
20 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/permissions/PermissionsManager.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.permissions
2 |
3 | import com.afollestad.assent.Permission
4 |
5 | interface PermissionsManager {
6 |
7 | suspend fun requestPermission(
8 | permission: Permission
9 | ): PermissionResult
10 |
11 | suspend fun requestPermissions(
12 | vararg permissions: Permission
13 | ): List
14 |
15 | suspend fun checkPermission(
16 | vararg permissions: Permission
17 | ): List
18 |
19 | suspend fun isGranted(
20 | vararg permissions: Permission
21 | ): Boolean
22 |
23 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/java/com/bubbble/shots/shotslist/ShotsView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shots.shotslist
2 |
3 | import androidx.paging.PagingData
4 | import com.bubbble.core.models.shot.Shot
5 | import com.bubbble.coreui.mvp.BaseMvpView
6 | import moxy.viewstate.strategy.alias.AddToEndSingle
7 | import moxy.viewstate.strategy.alias.OneExecution
8 |
9 | @AddToEndSingle
10 | interface ShotsView : BaseMvpView {
11 |
12 | fun showPagingData(pagingData: PagingData)
13 |
14 | fun updateListState(
15 | isProgressBarVisible: Boolean,
16 | isRetryVisible: Boolean,
17 | isErrorMsgVisible: Boolean
18 | )
19 |
20 | fun retryLoading()
21 |
22 | }
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/comments/CommentsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.comments
2 |
3 | import com.bubbble.core.models.Comment
4 | import com.bubbble.core.models.shot.ShotCommentsParams
5 | import com.bubbble.core.network.DribbbleApi
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class CommentsRepository @Inject constructor(
11 | private val dribbbleApi: DribbbleApi
12 | ) {
13 |
14 | suspend fun getComments(
15 | requestParams: ShotCommentsParams
16 | ): List {
17 | TODO()
18 | //https://dribbble.com/shots/16933705/comments?page=1&sort=recent&format=json
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/layout/item_no_comments.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
16 |
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/MockResponseHandler.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver
2 |
3 | import okhttp3.mockwebserver.MockResponse
4 | import okhttp3.mockwebserver.RecordedRequest
5 | import com.bubbble.tests.mockserver.request.MockRequest
6 | import com.google.gson.Gson
7 |
8 | typealias ResponseBuilder = MockResponse.(request: MockRequest) -> Unit
9 |
10 | class MockResponseHandler(
11 | private val gson: Gson,
12 | private val body: ResponseBuilder
13 | ) {
14 |
15 | fun handle(request: RecordedRequest): MockResponse {
16 | val mockRequest = MockRequest(gson, request)
17 | return MockResponse().apply { body(mockRequest) }
18 | }
19 |
20 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/java/com/bubbble/presentation/global/navigation/AppSettingsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.presentation.global.navigation
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.provider.Settings
7 | import com.github.terrakok.cicerone.androidx.ActivityScreen
8 |
9 | class AppSettingsScreen : ActivityScreen {
10 |
11 | override fun createIntent(context: Context): Intent {
12 | val intent = Intent()
13 | intent.action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS
14 | val uri = Uri.fromParts("package", context.packageName, null)
15 | intent.data = uri
16 | return intent
17 | }
18 |
19 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/src/main/java/com/bubbble/shotsearch/api/ShotsSearchScreen.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotsearch.api
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.bubbble.shotsearch.ShotsSearchActivity
6 | import com.bubbble.coreui.navigationargs.buildIntent
7 | import com.github.terrakok.cicerone.androidx.ActivityScreen
8 | import java.io.Serializable
9 |
10 | class ShotsSearchScreen(
11 | private val query: String
12 | ) : ActivityScreen {
13 |
14 | override fun createIntent(context: Context): Intent {
15 | return buildIntent(context, Data(query))
16 | }
17 |
18 | class Data(val query: String) : Serializable
19 |
20 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_web.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/api/ShotDetailsScreen.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails.api
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.bubbble.shotdetails.ShotDetailsActivity
6 | import com.bubbble.coreui.navigationargs.buildIntent
7 | import com.github.terrakok.cicerone.androidx.ActivityScreen
8 | import java.io.Serializable
9 |
10 | class ShotDetailsScreen(
11 | private val shotSlug: String
12 | ) : ActivityScreen {
13 |
14 | override fun createIntent(context: Context): Intent {
15 | return buildIntent(context, Data(shotSlug))
16 | }
17 |
18 | class Data(val shotSlug: String) : Serializable
19 |
20 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/drawable/ic_browser.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/api/UserProfileScreen.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.api
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.bubbble.userprofile.UserProfileActivity
6 | import com.bubbble.coreui.navigationargs.buildIntent
7 | import com.github.terrakok.cicerone.androidx.ActivityScreen
8 | import java.io.Serializable
9 |
10 | class UserProfileScreen(
11 | private val userName: String
12 | ) : ActivityScreen {
13 |
14 | override fun createIntent(context: Context): Intent {
15 | return buildIntent(context, Data(userName))
16 | }
17 |
18 | class Data(val userName: String) : Serializable
19 |
20 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/UserProfileView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile
2 |
3 | import com.bubbble.core.models.user.User
4 | import com.bubbble.coreui.mvp.BaseMvpView
5 | import moxy.viewstate.strategy.alias.AddToEndSingle
6 | import moxy.viewstate.strategy.alias.OneExecution
7 |
8 | @AddToEndSingle
9 | interface UserProfileView : BaseMvpView {
10 |
11 | fun showUser(user: User)
12 |
13 | fun showLoadingProgress(isVisible: Boolean)
14 |
15 | fun showNoNetworkLayout(isVisible: Boolean)
16 |
17 | @OneExecution
18 | fun openInBrowser(shotUrl: String)
19 |
20 | @OneExecution
21 | fun showUserProfileSharing(user: User)
22 |
23 | fun closeScreen()
24 |
25 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/res/layout/item_shot.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
16 |
17 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/users/FollowersRepository.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.users
2 |
3 | import com.bubbble.core.models.user.Follow
4 | import com.bubbble.core.models.user.UserFollowersParams
5 | import com.bubbble.core.network.DribbbleApi
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class FollowersRepository @Inject constructor(
11 | private val dribbbleApi: DribbbleApi
12 | ) {
13 |
14 | suspend fun getUserFollowers(params: UserFollowersParams): List {
15 | TODO()
16 | //return dribbbleApi.getUserFollowers(
17 | // params.userName,
18 | // params.page,
19 | // params.pageSize
20 | //)
21 | }
22 |
23 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/src/main/res/layout/item_search_shot.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
16 |
17 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/base/BaseMvpDialogFragment.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.base
2 |
3 | import android.widget.Toast
4 | import moxy.MvpAppCompatDialogFragment
5 |
6 | abstract class BaseMvpDialogFragment : MvpAppCompatDialogFragment() {
7 |
8 | //protected val router by lazy { getGlobal() }
9 | //protected val screenResolver by lazy { getGlobal() }
10 |
11 | fun showMessage(text: String) {
12 | Toast.makeText(requireContext(), text, Toast.LENGTH_SHORT).show()
13 | }
14 |
15 | fun showMessage(textResId: Int) {
16 | showMessage(getString(textResId))
17 | }
18 |
19 | //protected fun getScreen() = screenResolver.getScreen(this)
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/gradle/tests/unit-tests-config.gradle.kts:
--------------------------------------------------------------------------------
1 | apply(plugin="de.mannodermaus.android-junit5")
2 |
3 | tasks.withType {
4 | useJUnitPlatform()
5 | }
6 |
7 | val testImplementation by configurations
8 |
9 | dependencies {
10 | testImplementation(Dependencies.Tests.kotlinReflect)
11 | testImplementation(kotlin(Dependencies.Tests.stdJdk))
12 |
13 | testImplementation(Dependencies.Tests.kotestRunner)
14 | testImplementation(Dependencies.Tests.kotestCore)
15 | testImplementation(Dependencies.Tests.mockk)
16 | testImplementation(Dependencies.Tests.strikt)
17 |
18 | testImplementation(Dependencies.Tests.okhttpMockServer)
19 | testImplementation(Dependencies.Tests.jsonObject)
20 |
21 | testImplementation(project(Modules.tests))
22 | }
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/shot/Shot.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.shot
2 |
3 | import java.util.*
4 |
5 | data class Shot(
6 | val id: Long,
7 | val shotSlug: String,
8 | val title: String,
9 | val imageUrl: String,
10 | val viewsCount: Int,
11 | val likesCount: Int,
12 | val commentsCount: Int,
13 | val createdAt: Date?,
14 | val updatedAt: Date?,
15 | val shotUrl: String,
16 | val user: User,
17 | val hasMultipleImages: Boolean,
18 | val isAnimated: Boolean
19 | ) {
20 |
21 | data class User(
22 | val displayName: String,
23 | val userName: String,
24 | val avatarUrl: String,
25 | val userUrl: String,
26 | val type: UserType
27 | )
28 |
29 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/NetworkChecker.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network;
2 |
3 | import android.content.Context;
4 | import android.net.ConnectivityManager;
5 | import android.net.NetworkInfo;
6 |
7 | import javax.inject.Inject;
8 |
9 | public class NetworkChecker {
10 |
11 | private Context context;
12 |
13 | @Inject
14 | public NetworkChecker(Context context) {
15 | this.context = context;
16 | }
17 |
18 | public boolean isConnected() {
19 | ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
20 | NetworkInfo netInfo = cm.getActiveNetworkInfo();
21 | return netInfo != null && netInfo.isConnectedOrConnecting();
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/core/ui/src/main/res/drawable/ic_share.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/shot/ShotDetails.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.shot
2 |
3 | import java.util.*
4 |
5 | data class ShotDetails(
6 | val urlSlug: String,
7 | val title: String,
8 | val description: String,
9 | val imageUrl: String,
10 | val viewsCount: Int,
11 | val likesCount: Int,
12 | val commentsCount: Int,
13 | val createdAt: Date?,
14 | val updatedAt: Date?,
15 | val shotUrl: String,
16 | val user: User,
17 | val hasMultipleImages: Boolean,
18 | val isAnimated: Boolean
19 | ) {
20 |
21 | data class User(
22 | val displayName: String,
23 | val userName: String,
24 | val avatarUrl: String,
25 | val userUrl: String,
26 | val type: UserType
27 | )
28 |
29 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/java/com/bubbble/shots/main/MainPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shots.main
2 |
3 | import com.bubbble.coreui.mvp.BasePresenter
4 | import com.bubbble.shots.api.ShotsNavigationFactory
5 | import dagger.assisted.AssistedFactory
6 | import dagger.assisted.AssistedInject
7 | import moxy.InjectViewState
8 | import moxy.MvpPresenter
9 |
10 | @InjectViewState
11 | class MainPresenter @AssistedInject constructor(
12 | private val navigationFactory: ShotsNavigationFactory
13 | ) : BasePresenter() {
14 |
15 | fun onSearchQuery(searchQuery: String) {
16 | router.navigateTo(navigationFactory.shotsSearchScreen(searchQuery))
17 | }
18 |
19 | @AssistedFactory
20 | interface Factory {
21 | fun create(): MainPresenter
22 | }
23 |
24 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/comments/CommentViewHolder.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails.comments
2 |
3 | import android.view.View
4 | import android.widget.ImageView
5 | import android.widget.TextView
6 | import androidx.recyclerview.widget.RecyclerView
7 | import com.bubbble.coreui.ui.views.dribbbletextview.DribbbleTextView
8 | import com.bubbble.shotdetails.R
9 |
10 | internal class CommentViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
11 |
12 | val userName: TextView = itemView.findViewById(R.id.userName)
13 | val userAvatar: ImageView = itemView.findViewById(R.id.userAvatar)
14 | val commentText: DribbbleTextView = itemView.findViewById(R.id.text)
15 | val likesCount: TextView = itemView.findViewById(R.id.likes_count)
16 |
17 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/extensions/TestApiExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.extensions
2 |
3 | import com.bubbble.core.network.GsonProvider
4 | import okhttp3.OkHttpClient
5 | import retrofit2.Retrofit
6 | import retrofit2.converter.gson.GsonConverterFactory
7 | import kotlin.reflect.KClass
8 |
9 | fun createApi(apiClass: KClass): T {
10 | //val client = okHttpProvider.create(interceptors, getNetworkInterceptors()).build()
11 | val gsonProvider = GsonProvider()
12 | val host = "http://localhost:9999/"
13 | val retrofit = Retrofit.Builder()
14 | .addConverterFactory(GsonConverterFactory.create(gsonProvider.create()))
15 | .client(OkHttpClient())
16 | .baseUrl(host)
17 | .build()
18 |
19 | return retrofit.create(apiClass.java)
20 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/navigationargs/BundleNavDataExtractor.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.navigationargs
2 |
3 | import android.os.Bundle
4 | import android.os.Parcelable
5 |
6 | internal const val NAV_DATA_KEY = "com.bubbble.NAV_DATA_KEY"
7 |
8 | class BundleNavDataExtractor {
9 |
10 | fun getData(bundle: Bundle?) : ScreenData {
11 | if (bundle == null) throw IllegalArgumentException("Screen data not found")
12 |
13 | val parcelable = bundle.getParcelable(NAV_DATA_KEY)
14 | if (parcelable != null) return parcelable as ScreenData
15 |
16 | val serializable = bundle.getSerializable(NAV_DATA_KEY)
17 | if (serializable != null) return serializable as ScreenData
18 |
19 | throw IllegalArgumentException("Screen data not found")
20 | }
21 |
22 | }
--------------------------------------------------------------------------------
/tests/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
--------------------------------------------------------------------------------
/core/core/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-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/shotzoom/ShotZoomView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails.shotzoom
2 |
3 | import com.bubbble.coreui.mvp.BaseMvpView
4 | import moxy.viewstate.strategy.alias.AddToEndSingle
5 | import moxy.viewstate.strategy.alias.OneExecution
6 |
7 | @AddToEndSingle
8 | internal interface ShotZoomView : BaseMvpView {
9 |
10 | fun showShotImage(imageUrl: String)
11 |
12 | fun showLoadingProgress(isVisible: Boolean)
13 |
14 | fun showErrorLayout()
15 |
16 | fun hideErrorLayout()
17 |
18 | fun showImageSavedMessage()
19 |
20 | fun showStorageAccessRationaleMessage()
21 |
22 | fun showAllowStorageAccessMessage()
23 |
24 | @OneExecution
25 | fun showShotSharing(shotTitle: String, url: String)
26 |
27 | @OneExecution
28 | fun openInBrowser(shotUrl: String)
29 |
30 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/menu/shot_zoom.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app-mvp/feature-shots/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
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/GsonProvider.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.GsonBuilder
5 | import javax.inject.Inject
6 | import kotlin.reflect.KClass
7 |
8 | open class GsonProvider @Inject constructor() {
9 |
10 | fun create(): Gson {
11 | val typeAdapters = listOf(
12 | *getTypeAdapters().toTypedArray()
13 | )
14 | return GsonBuilder()
15 | .also { build ->
16 | typeAdapters.forEach { adapter ->
17 | build.registerTypeAdapter(adapter.first.java, adapter.second)
18 | }
19 | }
20 | .create()
21 | }
22 |
23 | open fun getTypeAdapters(): List, Any>> {
24 | return emptyList()
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/core/ui/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Search
5 | Open in browser
6 |
7 | Check network connection
8 | Try again
9 | An error occurred
10 |
11 |
12 | - %s following
13 | - %s following
14 | - %s following
15 |
16 |
17 |
18 | - %s projects
19 | - %s project
20 | - %s projects
21 |
22 |
23 |
--------------------------------------------------------------------------------
/core/di/src/main/java/com/bubbble/di/injector/ComponentHolder.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.injector
2 |
3 | internal class ComponentHolder(
4 | private val componentFactory: ComponentFactory,
5 | private val componentsManager: ComponentManager
6 | ) {
7 |
8 | private var component: T? = null
9 |
10 | @Synchronized
11 | fun getOrCreate(): T {
12 | if (component == null) {
13 | component = componentFactory.create(componentsManager)
14 | }
15 | return component!!
16 | }
17 |
18 | @Synchronized
19 | fun getWithoutRef(): T {
20 | if (component == null) {
21 | throw IllegalStateException("Component isn't created yet!")
22 | }
23 | return component!!
24 | }
25 |
26 | @Synchronized
27 | fun release() {
28 | component = null
29 | }
30 |
31 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/exceptions/HttpException.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.exceptions;
2 |
3 | public class HttpException extends RuntimeException {
4 |
5 | private int errorCode;
6 | private String message;
7 | private Throwable throwable;
8 |
9 | public int getErrorCode() {
10 | return errorCode;
11 | }
12 |
13 | public void setErrorCode(int errorCode) {
14 | this.errorCode = errorCode;
15 | }
16 |
17 | public String getMessage() {
18 | return message;
19 | }
20 |
21 | public void setMessage(String errorMessage) {
22 | this.message = errorMessage;
23 | }
24 |
25 | public Throwable getThrowable() {
26 | return throwable;
27 | }
28 |
29 | public void setThrowable(Throwable throwable) {
30 | this.throwable = throwable;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/values/badgedimageview.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/parsing/PageDownloader.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.parsing
2 |
3 | import okhttp3.HttpUrl
4 | import okhttp3.OkHttpClient
5 | import okhttp3.Request
6 | import javax.inject.Inject
7 |
8 | class PageDownloader @Inject constructor(
9 | private val okHttpClient: OkHttpClient,
10 | ) {
11 |
12 | fun download(url: HttpUrl): String {
13 | val request: Request = Request.Builder().url(url).build()
14 | val response = okHttpClient.newCall(request).execute()
15 | if (response.code == 200) {
16 | return response.body!!.string()
17 | } else {
18 | throw PageDownloadException(
19 | message = "An error caused when downloading page",
20 | httpCode = response.code,
21 | response = response.body?.string()
22 | )
23 | }
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/core/ui/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #222222
4 | #000000
5 | @color/pink_300
6 |
7 | #ffffff
8 | #88ffffff
9 | #fff5f5f5
10 | #333333
11 | #393939
12 | #454545
13 | #a6144e
14 | #C2185B
15 | #da1864
16 | #99323232
17 |
18 | @color/pink_300
19 | @color/pink_100
20 |
21 |
22 |
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/interceptors/DribbbleTokenInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.interceptors;
2 |
3 | import java.io.IOException;
4 |
5 | import okhttp3.Interceptor;
6 | import okhttp3.Request;
7 | import okhttp3.Response;
8 |
9 | public class DribbbleTokenInterceptor implements Interceptor {
10 |
11 | private String token;
12 |
13 | private static final String KEY_AUTH = "Authorization";
14 |
15 | public DribbbleTokenInterceptor(String token) {
16 | this.token = "Bearer " + token;
17 | }
18 |
19 | @Override
20 | public Response intercept(Chain chain) throws IOException {
21 | Request originalRequest = chain.request();
22 | Request tokenRequest = originalRequest.newBuilder()
23 | .addHeader(KEY_AUTH, token)
24 | .build();
25 | return chain.proceed(tokenRequest);
26 | }
27 |
28 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Follow
4 | Information
5 |
6 |
7 | - %s shot
8 | - %s shots
9 |
10 |
11 |
12 | - %s likes
13 | - %s like
14 | - %s likes
15 |
16 |
17 |
18 | - %s bucket
19 | - %s buckets
20 |
21 |
22 |
23 | - %s subscriber
24 | - %s subscribers
25 |
26 |
--------------------------------------------------------------------------------
/core/data/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("kapt")
3 | }
4 |
5 | android {
6 | defaultConfig {
7 | val dribbble_client_id: String by project
8 | val dribbble_client_secret: String by project
9 | val dribbble_client_access_token: String by project
10 | buildConfigField("String", "DRIBBBLE_CLIENT_ID", "\"${dribbble_client_id}\"")
11 | buildConfigField("String", "DRIBBBLE_CLIENT_SECRET", "\"${dribbble_client_secret}\"")
12 | buildConfigField("String", "DRIBBBLE_CLIENT_ACCESS_TOKEN", "\"${dribbble_client_access_token}\"")
13 | }
14 | }
15 |
16 | dependencies {
17 | implementation(project(Modules.Core.di))
18 | api(project(Modules.Core.models))
19 | api(project(Modules.Core.network))
20 |
21 | api(Dependencies.paging)
22 | api(Dependencies.coroutinesCore)
23 | api(Dependencies.coroutinesAndroid)
24 |
25 | implementation(Dependencies.jsoup)
26 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("kapt")
3 | }
4 |
5 | android {
6 | buildFeatures.viewBinding = true
7 | }
8 |
9 | dependencies {
10 | implementation(project(Modules.Core.di))
11 | api(project(Modules.Core.core))
12 | api(project(Modules.Core.ui))
13 |
14 | api(Dependencies.constraintLayout)
15 | api(Dependencies.androidxCardView)
16 | api(Dependencies.customTabs)
17 | api(Dependencies.viewBindingDelegate)
18 |
19 | api(Dependencies.glide)
20 | api(Dependencies.photoView)
21 |
22 | api(Dependencies.Mvp.moxy)
23 | api(Dependencies.Mvp.moxyAndroid)
24 | api(Dependencies.Mvp.moxyKtx)
25 | api(Dependencies.Mvp.cicerone)
26 |
27 | api(Dependencies.coroutinesCore)
28 | api(Dependencies.coroutinesAndroid)
29 |
30 | //permissions request
31 | api("com.afollestad.assent:core:3.0.0-RC4")
32 | api("com.afollestad.assent:rationales:3.0.0-RC4")
33 | }
--------------------------------------------------------------------------------
/tests/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("kapt")
3 | }
4 |
5 | dependencies {
6 | api(project(Modules.Core.di))
7 | api(project(Modules.Core.core))
8 | api(project(Modules.Core.network))
9 |
10 | // unit-tests
11 | implementation(Dependencies.Tests.kotlinReflect)
12 | implementation(kotlin(Dependencies.Tests.stdJdk))
13 |
14 | implementation(Dependencies.Tests.kotestRunner)
15 | implementation(Dependencies.Tests.kotestCore)
16 | implementation(Dependencies.Tests.mockk)
17 | implementation(Dependencies.Tests.strikt)
18 |
19 | implementation(Dependencies.Tests.okhttpMockServer)
20 | implementation(Dependencies.Tests.jsonObject)
21 |
22 | implementation(Dependencies.Tests.junit)
23 | implementation(Dependencies.Tests.rules)
24 | implementation(Dependencies.Tests.kakao)
25 | implementation(Dependencies.Tests.kaspresso)
26 |
27 | api("com.lectra:koson:1.1.0")
28 | }
29 |
--------------------------------------------------------------------------------
/app-mvp/app/src/unitTests/java/com/bubbble/test/TestSchedulersProvider.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.test;
2 |
3 | import io.reactivex.Scheduler;
4 | import io.reactivex.schedulers.TestScheduler;
5 |
6 | public class TestSchedulersProvider extends SchedulersProvider {
7 |
8 | private final TestScheduler testScheduler = new TestScheduler();
9 |
10 | @Override
11 | public Scheduler ui() {
12 | return testScheduler;
13 | }
14 |
15 | @Override
16 | public Scheduler computation() {
17 | return testScheduler;
18 | }
19 |
20 | @Override
21 | public Scheduler io() {
22 | return testScheduler;
23 | }
24 |
25 | @Override
26 | public Scheduler newThread() {
27 | return testScheduler;
28 | }
29 |
30 | @Override
31 | public Scheduler trampoline() {
32 | return testScheduler;
33 | }
34 |
35 | public TestScheduler testScheduler() {
36 | return testScheduler;
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/MockRequestsDispatcher.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver
2 |
3 | import okhttp3.mockwebserver.Dispatcher
4 | import okhttp3.mockwebserver.MockResponse
5 | import okhttp3.mockwebserver.RecordedRequest
6 | import java.net.URLDecoder
7 |
8 | class MockRequestsDispatcher : Dispatcher() {
9 |
10 | private val handlers = mutableMapOf()
11 |
12 | override fun dispatch(request: RecordedRequest): MockResponse {
13 | val path = request.path!!
14 | .substring(1) //remove slash from start of path
15 | .substringBefore("?")
16 | .let { URLDecoder.decode(it, "UTF-8") }
17 | val handler = handlers[path] ?: throw IllegalArgumentException("Response for \"$path\" not found")
18 | return handler.handle(request)
19 | }
20 |
21 | fun addPathHandler(path: String, handler: MockResponseHandler) {
22 | handlers[path] = handler
23 | }
24 |
25 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/debug/java/com/bubbble/DebugBubbleApplication.java:
--------------------------------------------------------------------------------
1 | package com.bubbble;
2 |
3 | import com.facebook.stetho.Stetho;
4 |
5 | public class DebugBubbleApplication extends BubbbleApplication {
6 |
7 | @Override
8 | public void onCreate() {
9 | super.onCreate();
10 |
11 | initStetho();
12 | }
13 |
14 | private void initStetho() {
15 | Stetho.InitializerBuilder initializerBuilder = Stetho.newInitializerBuilder(this);
16 | // Enable Chrome DevTools
17 | initializerBuilder.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(this));
18 | // Enable command line interface
19 | initializerBuilder.enableDumpapp(Stetho.defaultDumperPluginsProvider(this));
20 | // Use the InitializerBuilder to generate an Initializer
21 | Stetho.Initializer initializer = initializerBuilder.build();
22 | // Initialize Stetho with the Initializer
23 | Stetho.initialize(initializer);
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/app-mvp/app/src/release/java/com/bubbble/di/modules/OkHttpInterceptorsModule.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.modules;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import com.bubbble.di.global.qualifiers.OkHttpInterceptors;
6 | import com.bubbble.di.global.qualifiers.OkHttpNetworkInterceptors;
7 |
8 | import java.util.List;
9 |
10 | import javax.inject.Singleton;
11 |
12 | import dagger.Module;
13 | import dagger.Provides;
14 | import okhttp3.Interceptor;
15 |
16 | import static java.util.Collections.emptyList;
17 |
18 | @Module
19 | public class OkHttpInterceptorsModule {
20 |
21 | @Provides
22 | @OkHttpInterceptors
23 | @Singleton
24 | @NonNull
25 | public List provideOkHttpInterceptors() {
26 | return emptyList();
27 | }
28 |
29 | @Provides
30 | @OkHttpNetworkInterceptors
31 | @Singleton
32 | @NonNull
33 | public List provideOkHttpNetworkInterceptors() {
34 | return emptyList();
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/core/ui/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Поиск
5 | Открыть в браузере
6 |
7 | Пожалуйста, проверьте подключение к интернету
8 | Попробовать снова
9 | Произошла ошибка
10 |
11 |
12 | - %s подписан
13 | - %s подписан
14 | - %s подписан
15 | - %s подписан
16 |
17 |
18 |
19 | - %s проект
20 | - %s проектов
21 | - %s проектов
22 | - %s проектов
23 |
24 |
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/java/com/bubbble/shots/main/ShotsPagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shots.main
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentManager
5 | import androidx.fragment.app.FragmentPagerAdapter
6 | import java.util.*
7 |
8 | class ShotsPagerAdapter(
9 | manager: FragmentManager
10 | ) : FragmentPagerAdapter(manager) {
11 |
12 | private val fragmentList: MutableList = ArrayList()
13 | private val fragmentTitleList: MutableList = ArrayList()
14 |
15 | override fun getItem(position: Int): Fragment {
16 | return fragmentList[position]
17 | }
18 |
19 | override fun getCount(): Int = fragmentList.size
20 |
21 | fun addFragment(fragment: Fragment, title: String) {
22 | fragmentList.add(fragment)
23 | fragmentTitleList.add(title)
24 | }
25 |
26 | override fun getPageTitle(position: Int): CharSequence {
27 | return fragmentTitleList[position]
28 | }
29 |
30 | }
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/parsing/PageParserManager.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.parsing
2 |
3 | import com.bubbble.data.di.DribbbleWebSite
4 | import com.bubbble.data.global.paging.PagingParams
5 | import kotlinx.coroutines.Dispatchers
6 | import kotlinx.coroutines.withContext
7 | import javax.inject.Inject
8 | import javax.inject.Singleton
9 |
10 | @Singleton
11 | class PageParserManager @Inject constructor(
12 | private val pageDownloader: PageDownloader,
13 | @DribbbleWebSite
14 | private val dribbbleUrl: String
15 | ) {
16 |
17 | suspend fun parse(
18 | parser: PageParser,
19 | params: Params,
20 | pagingParams: PagingParams = PagingParams(1, 1)
21 | ): Data = withContext(Dispatchers.IO) {
22 | val pageUrl = parser.getUrl(dribbbleUrl, params, pagingParams)
23 | val html = pageDownloader.download(pageUrl)
24 | parser.parseHtml(html, dribbbleUrl, pageUrl.toString())
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/UserProfilePagerAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile
2 |
3 | import androidx.fragment.app.Fragment
4 | import androidx.fragment.app.FragmentManager
5 | import androidx.fragment.app.FragmentPagerAdapter
6 | import java.util.*
7 |
8 | class UserProfilePagerAdapter(
9 | manager: FragmentManager
10 | ) : FragmentPagerAdapter(manager) {
11 |
12 | private val fragmentList: MutableList = ArrayList()
13 | private val fragmentTitleList: MutableList = ArrayList()
14 |
15 | override fun getItem(position: Int): Fragment {
16 | return fragmentList[position]
17 | }
18 |
19 | override fun getCount(): Int = fragmentList.size
20 |
21 | fun addFragment(fragment: Fragment, title: String) {
22 | fragmentList.add(fragment)
23 | fragmentTitleList.add(title)
24 | }
25 |
26 | override fun getPageTitle(position: Int): CharSequence {
27 | return fragmentTitleList[position]
28 | }
29 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Подписаться
4 | Информация
5 |
6 |
7 | - %s бакет
8 | - %s бакета
9 | - %s бакетов
10 | - %s бакетов
11 |
12 |
13 |
14 | - %s подписчик
15 | - %s подписчиков
16 | - %s подписчиков
17 | - %s подписчиков
18 |
19 |
20 |
21 | - %s лайк
22 | - %s лайка
23 | - %s лайков
24 | - %s лайков
25 |
26 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/api/ShotImageZoomScreen.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails.api
2 |
3 | import android.content.Context
4 | import android.content.Intent
5 | import com.bubbble.shotdetails.shotzoom.ShotZoomActivity
6 | import com.bubbble.coreui.navigationargs.buildIntent
7 | import com.github.terrakok.cicerone.androidx.ActivityScreen
8 | import java.io.Serializable
9 |
10 | class ShotImageZoomScreen(
11 | private val title: String,
12 | private val shotUrl: String,
13 | private val imageUrl: String
14 | ) : ActivityScreen {
15 |
16 | override fun createIntent(context: Context): Intent {
17 | return buildIntent(
18 | context, Data(
19 | title = title,
20 | shotUrl = shotUrl,
21 | imageUrl = imageUrl
22 | )
23 | )
24 | }
25 |
26 | class Data(
27 | val title: String,
28 | val shotUrl: String,
29 | val imageUrl: String
30 | ) : Serializable
31 |
32 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/res/layout/fragment_shots.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
20 |
21 |
25 |
26 |
--------------------------------------------------------------------------------
/core/models/src/main/java/com/bubbble/core/models/user/User.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.models.user
2 |
3 | import java.util.*
4 |
5 | class User(
6 | val id: Long,
7 | val name: String,
8 | val userName: String,
9 | val htmlUrl: String,
10 | val avatarUrl: String,
11 | val bio: String?,
12 | val location: String?,
13 | val links: Links,
14 | val bucketsCount: Int,
15 | val commentsReceivedCount: Int,
16 | val followersCount: Int,
17 | val followingsCount: Int,
18 | val likesCount: Int,
19 | val likesReceivedCount: Int,
20 | val projectsCount: Int,
21 | val reboundsReceivedCount: Int,
22 | val shotsCount: Int,
23 | val teamsCount: Int,
24 | val canUploadShot: Boolean,
25 | val type: String,
26 | val pro: Boolean,
27 | val bucketsUrl: String,
28 | val followersUrl: String,
29 | val followingUrl: String,
30 | val likesUrl: String?,
31 | val shotsUrl: String,
32 | val teamsUrl: String,
33 | val createdAt: Date,
34 | val updatedAt: Date
35 | )
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/prefs/TempPreferences.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.prefs
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import com.bubbble.data.BuildConfig
6 | import javax.inject.Inject
7 |
8 | class TempPreferences @Inject constructor(context: Context) {
9 |
10 | private val prefs: SharedPreferences =
11 | context.getSharedPreferences(APP_PREFS_FILE_NAME, Context.MODE_PRIVATE)
12 |
13 | fun saveToken(token: String?) {
14 | prefs.edit().putString(PREF_API_TOKEN, token).apply()
15 | }
16 |
17 | val token: String
18 | get() = prefs.getString(
19 | PREF_API_TOKEN,
20 | BuildConfig.DRIBBBLE_CLIENT_ACCESS_TOKEN
21 | )!!
22 |
23 | fun clearToken() {
24 | prefs.edit().putString(PREF_API_TOKEN, null).apply()
25 | }
26 |
27 | companion object {
28 | private const val APP_PREFS_FILE_NAME = "app_preferences"
29 | private const val PREF_API_TOKEN = "api_token"
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/request/BubbbleMockWebServer.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver.request
2 |
3 | import okhttp3.mockwebserver.MockWebServer
4 | import com.bubbble.tests.mockserver.MockRequestsDispatcher
5 | import com.bubbble.tests.mockserver.MockResponseHandler
6 | import com.bubbble.tests.mockserver.ResponseBuilder
7 | import com.google.gson.Gson
8 |
9 | class BubbbleMockWebServer {
10 |
11 | private val webServer = MockWebServer()
12 | private val dispatcher = MockRequestsDispatcher()
13 | private val gson = Gson()
14 |
15 | init {
16 | webServer.dispatcher = dispatcher
17 | }
18 |
19 | fun start(port: Int? = null) = webServer.start(port ?: 9999)
20 |
21 | fun shutdown() = webServer.shutdown()
22 |
23 | fun getUrl() = webServer.url("/")
24 |
25 | fun on(path: String, builder: ResponseBuilder) {
26 | dispatcher.addPathHandler(path, MockResponseHandler(gson, builder))
27 | }
28 |
29 | fun getRequest() = MockRequest(gson, webServer.takeRequest())
30 |
31 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/layout/fragment_user_shots.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
20 |
21 |
25 |
26 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/prefs/TempDataRepository.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.prefs
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import com.bubbble.data.BuildConfig
6 | import javax.inject.Inject
7 |
8 | class TempDataRepository @Inject constructor(context: Context) {
9 |
10 | private val prefs: SharedPreferences =
11 | context.getSharedPreferences(APP_PREFS_FILE_NAME, Context.MODE_PRIVATE)
12 |
13 | fun saveToken(token: String) {
14 | prefs.edit().putString(PREF_API_TOKEN, token).apply()
15 | }
16 |
17 | val token: String
18 | get() = prefs.getString(
19 | PREF_API_TOKEN,
20 | BuildConfig.DRIBBBLE_CLIENT_ACCESS_TOKEN
21 | )!!
22 |
23 | fun clearToken() {
24 | prefs.edit().putString(PREF_API_TOKEN, null).apply()
25 | }
26 |
27 | companion object {
28 | private const val APP_PREFS_FILE_NAME = "app_preferences"
29 | private const val PREF_API_TOKEN = "api_token"
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/layout/fragment_followers.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
20 |
21 |
25 |
26 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/parsing/PageParser.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.parsing
2 |
3 | import com.bubbble.data.global.paging.PagingParams
4 | import okhttp3.HttpUrl
5 | import org.jsoup.nodes.Element
6 |
7 | abstract class PageParser {
8 |
9 | abstract fun getUrl(
10 | baseUrl: String,
11 | params: Params,
12 | pagingParams: PagingParams
13 | ): HttpUrl
14 |
15 | abstract fun parseHtml(
16 | html: String,
17 | baseUrl: String,
18 | pageUrl: String
19 | ): Data
20 |
21 | fun Element.getText(selector: String): String {
22 | return select(selector).firstOrNull()?.text() ?: ""
23 | }
24 |
25 | fun Element.getElement(selector: String): Element {
26 | return select(selector).first()
27 | }
28 |
29 | fun Element.getElementOrNull(selector: String): Element? {
30 | return select(selector).first()
31 | }
32 |
33 | fun String.remove(symbol: String): String {
34 | return replace(symbol.toRegex(), "")
35 | }
36 |
37 | }
--------------------------------------------------------------------------------
/gradle/tests/ui-tests-config.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.TestedExtension
2 | import org.gradle.kotlin.dsl.DependencyHandlerScope
3 |
4 | configure {
5 | //testBuildType = "uiTest"
6 |
7 | buildTypes {
8 | //create("uiTest") { }
9 | }
10 |
11 | sourceSets {
12 | //getByName("uiTest").resources.srcDirs("src/test/resources")
13 | getByName("androidTest").resources.srcDirs("src/test/resources")
14 | }
15 | }
16 |
17 | val androidTestImplementation by configurations
18 |
19 | dependencies {
20 | androidTestImplementation(Dependencies.Tests.okhttpMockServer)
21 | androidTestImplementation(Dependencies.Tests.jsonObject)
22 |
23 | androidTestImplementation(Dependencies.Tests.junit)
24 | androidTestImplementation(Dependencies.Tests.rules)
25 | androidTestImplementation(Dependencies.Tests.kakao)
26 | androidTestImplementation(Dependencies.Tests.kaspresso)
27 | androidTestImplementation(Dependencies.Tests.okHttpIdlingResource)
28 |
29 | androidTestImplementation(project(Modules.tests))
30 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/layout/view_empty_list.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
14 |
15 |
25 |
26 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/utils/AppUtils.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.utils;
2 |
3 | import android.app.Activity;
4 | import android.net.Uri;
5 |
6 | import androidx.browser.customtabs.CustomTabsIntent;
7 | import androidx.core.app.ShareCompat;
8 | import com.bubbble.coreui.R;
9 |
10 | public class AppUtils {
11 |
12 | public static void sharePlainText(Activity activity, String textToShare) {
13 | ShareCompat.IntentBuilder
14 | .from(activity)
15 | .setText(textToShare)
16 | .setType("text/plain")
17 | .setChooserTitle(activity.getString(R.string.share_using))
18 | .startChooser();
19 | }
20 |
21 | public static void openInChromeTab(Activity activity, String url) {
22 | CustomTabsIntent.Builder builder = new CustomTabsIntent.Builder();
23 | builder.setToolbarColor(activity.getResources().getColor(R.color.colorPrimary));
24 | CustomTabsIntent customTabsIntent = builder.build();
25 | customTabsIntent.launchUrl(activity, Uri.parse(url));
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/views/dribbbletextview/TouchableUrlSpan.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.views.dribbbletextview
2 |
3 | import android.R
4 | import android.content.res.ColorStateList
5 | import android.graphics.Color
6 | import android.text.style.URLSpan
7 | import android.text.TextPaint
8 |
9 | open class TouchableUrlSpan(
10 | url: String?,
11 | textColor: ColorStateList
12 | ) : URLSpan(url) {
13 |
14 | private var isPressed = false
15 | private val normalTextColor: Int = textColor.defaultColor
16 | private val pressedTextColor: Int = textColor.getColorForState(STATE_PRESSED, normalTextColor)
17 |
18 | fun setPressed(isPressed: Boolean) {
19 | this.isPressed = isPressed
20 | }
21 |
22 | override fun updateDrawState(drawState: TextPaint) {
23 | drawState.color = if (isPressed) pressedTextColor else normalTextColor
24 | drawState.bgColor = Color.TRANSPARENT
25 | drawState.isUnderlineText = !isPressed
26 | }
27 |
28 | companion object {
29 | private val STATE_PRESSED = intArrayOf(R.attr.state_pressed)
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/java/com/bubbble/shotdetails/ShotDetailsView.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shotdetails
2 |
3 | import com.bubbble.core.models.Comment
4 | import com.bubbble.core.models.shot.ShotDetails
5 | import com.bubbble.coreui.mvp.BaseMvpView
6 | import moxy.viewstate.strategy.alias.AddToEndSingle
7 | import moxy.viewstate.strategy.alias.OneExecution
8 |
9 | @AddToEndSingle
10 | internal interface ShotDetailsView : BaseMvpView {
11 |
12 | fun showShot(shot: ShotDetails)
13 |
14 | fun showLoadingProgress(isVisible: Boolean)
15 |
16 | fun showNoNetworkLayout(isVisible: Boolean)
17 |
18 | fun hideImageLoadingProgress()
19 |
20 | fun showNewComments(newComments: List)
21 |
22 | fun showCommentsLoadingProgress()
23 |
24 | fun hideCommentsLoadingProgress()
25 |
26 | fun showNoComments()
27 |
28 | fun showImageSavedMessage()
29 |
30 | fun showStorageAccessRationaleMessage()
31 |
32 | fun showAllowStorageAccessMessage()
33 |
34 | @OneExecution
35 | fun showShotSharing(shotTitle: String, shotUrl: String)
36 |
37 | @OneExecution
38 | fun openInBrowser(url: String)
39 |
40 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 |
3 | # IDE (e.g. Android Studio) users:
4 | # Gradle settings configured through the IDE *will override*
5 | # any settings specified in this file.
6 |
7 | # For more details on how to configure your build environment visit
8 | # http://www.gradle.org/docs/current/userguide/build_environment.html
9 |
10 | # Specifies the JVM arguments used for the daemon process.
11 | # The setting is particularly useful for tweaking memory settings.
12 | org.gradle.jvmargs=-Xmx1536m
13 |
14 | # When configured, Gradle will run in incubating parallel mode.
15 | # This option should only be used with decoupled projects. More details, visit
16 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
17 | # org.gradle.parallel=true
18 |
19 | # Dribbble API
20 | dribbble_client_id = c117ac7d45714d9692ebf30ee86508d63dce4397795cedefa7e71dd01128fda4
21 | dribbble_client_secret = 76d0e42be06af047e8d7b59c0714c203517622000530e9d12448adef8225d794
22 | dribbble_client_access_token = 22bf8ba0affd9ac65b3976930b20460f554a246d2789b3a85f04c114def47204
23 | android.useAndroidX=true
24 | android.enableJetifier=true
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/filesystem/UrlImageSaver.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.filesystem
2 |
3 | import android.app.DownloadManager
4 | import android.content.Context
5 | import android.net.Uri
6 | import android.os.Environment
7 | import java.io.File
8 | import javax.inject.Inject
9 |
10 | class UrlImageSaver @Inject constructor(private val context: Context) {
11 |
12 | private val destinationFolder: File =
13 | Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
14 |
15 | suspend fun saveImage(imageUrl: String) {
16 | val imageFileName = imageUrl.substring(imageUrl.lastIndexOf('/') + 1, imageUrl.length)
17 | val uri = Uri.parse(imageUrl)
18 | val request = DownloadManager.Request(uri)
19 | request.setTitle(imageFileName)
20 | request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
21 | request.setDestinationUri(Uri.fromFile(destinationFolder))
22 | val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
23 | downloadManager.enqueue(request)
24 | }
25 |
26 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/di/CoreUiEntrypoint.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.di
2 |
3 | import com.bubbble.core.AppContext
4 | import com.bubbble.coreui.mvp.ErrorHandler
5 | import com.bubbble.coreui.permissions.PermissionsManager
6 | import com.bubbble.coreui.resourcesmanager.ResourcesManager
7 | import com.bubbble.di.injector.ComponentApi
8 | import com.bubbble.di.injector.ComponentFactory
9 | import com.bubbble.di.injector.ComponentManager
10 | import com.github.terrakok.cicerone.NavigatorHolder
11 | import com.github.terrakok.cicerone.Router
12 | import dagger.hilt.EntryPoint
13 |
14 | import dagger.hilt.EntryPoints
15 | import dagger.hilt.InstallIn
16 | import dagger.hilt.components.SingletonComponent
17 |
18 | @EntryPoint
19 | @InstallIn(SingletonComponent::class)
20 | interface CoreUiEntrypoint {
21 |
22 | val errorHandler: ErrorHandler
23 | val resourcesManager: ResourcesManager
24 | val permissionsManager: PermissionsManager
25 | val router: Router
26 | val navigatorHolder: NavigatorHolder
27 |
28 | }
29 |
30 | val coreUiEntrypoint: CoreUiEntrypoint
31 | get() = EntryPoints.get(AppContext.instance, CoreUiEntrypoint::class.java)
--------------------------------------------------------------------------------
/app-mvp/app/src/debug/java/com/bubbble/di/modules/OkHttpInterceptorsModule.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.modules
2 |
3 | import com.bubbble.di.qualifiers.OkHttpInterceptors
4 | import okhttp3.logging.HttpLoggingInterceptor
5 | import com.bubbble.di.qualifiers.OkHttpNetworkInterceptors
6 | import com.facebook.stetho.okhttp3.StethoInterceptor
7 | import dagger.Module
8 | import dagger.Provides
9 | import dagger.hilt.InstallIn
10 | import dagger.hilt.components.SingletonComponent
11 | import okhttp3.Interceptor
12 | import javax.inject.Singleton
13 |
14 | @Module
15 | @InstallIn(SingletonComponent::class)
16 | class OkHttpInterceptorsModule {
17 |
18 | @Provides
19 | @OkHttpInterceptors
20 | @Singleton
21 | fun provideOkHttpInterceptors(): List {
22 | val httpLoggingInterceptor = HttpLoggingInterceptor()
23 | httpLoggingInterceptor.setLevel(HttpLoggingInterceptor.Level.HEADERS)
24 | return listOf(httpLoggingInterceptor)
25 | }
26 |
27 | @Provides
28 | @OkHttpNetworkInterceptors
29 | @Singleton
30 | fun provideOkHttpNetworkInterceptors(): List {
31 | return listOf(StethoInterceptor())
32 | }
33 |
34 | }
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/global/paging/CommonPagingSource.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.global.paging
2 |
3 | import androidx.paging.PagingSource
4 | import androidx.paging.PagingState
5 | import javax.inject.Inject
6 |
7 | class CommonPagingSource @Inject constructor(
8 | private val dataSource: suspend (params: PagingParams) -> List
9 | ) : PagingSource() {
10 |
11 | override suspend fun load(
12 | params: LoadParams
13 | ): LoadResult {
14 | return try {
15 | val pagingParams = params.pagingParams
16 | LoadResult.Page(
17 | data = dataSource(pagingParams),
18 | prevKey = null, // Only paging forward.
19 | nextKey = pagingParams.page + 1
20 | )
21 | } catch (e: Exception) {
22 | LoadResult.Error(e)
23 | }
24 | }
25 |
26 | override fun getRefreshKey(state: PagingState): Int? {
27 | return state.anchorPosition?.let { anchorPosition ->
28 | val anchorPage = state.closestPageToPosition(anchorPosition)
29 | anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/unitTests/java/com/bubbble/presentation/mvp/presenters/MainPresenterTest.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.presentation.mvp.presenters;
2 |
3 | import com.bubbble.shots.main.MainPresenter;
4 | import com.bubbble.shots.main.MainView;
5 | import com.bubbble.test.BubbbleTestRunner;
6 |
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.mockito.Mock;
11 | import org.mockito.MockitoAnnotations;
12 |
13 | import static org.mockito.Mockito.verify;
14 |
15 | @RunWith(BubbbleTestRunner.class)
16 | public class MainPresenterTest {
17 |
18 | @Mock
19 | private MainView view;
20 | private MainPresenter presenter;
21 |
22 | @Before
23 | public void setUp() {
24 | MockitoAnnotations.initMocks(this);
25 | presenter = new MainPresenter();
26 | }
27 |
28 | @Test
29 | public void onSearchQuery_shouldOpenSearchScreen() {
30 | //arrange
31 | String testSearchQuery = "Test search query";
32 |
33 | //act
34 | presenter.attachView(view);
35 | presenter.onSearchQuery(testSearchQuery);
36 |
37 | // assert
38 | verify(view).openSearchScreen(testSearchQuery);
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/core/data/src/test/java/com/bubbble/data/search/SearchPageParserTest.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.search
2 |
3 | import com.bubbble.core.network.Dribbble
4 | import com.bubbble.data.shots.search.SearchPageParser
5 | import com.bubbble.tests.extensions.readTextFile
6 | import com.google.gson.Gson
7 | import io.kotest.core.spec.style.DescribeSpec
8 |
9 | class SearchPageParserTest : DescribeSpec({
10 |
11 | val searchPageParser = SearchPageParser(Gson())
12 |
13 | describe("shots getting") {
14 | context("shots") {
15 | val htmlText = readTextFile("html-responses/shots-list.html")
16 | val shots = searchPageParser.parseHtml(htmlText, Dribbble.URL,)
17 | //val parseManager = ComponentHolder.component.pageParserManager()
18 | //parseManager.parse(searchPageParser, SearchParams(
19 | // page = 1,
20 | // pageSize = 10,
21 | // searchQuery = "app",
22 | // searchType = SearchType.SHOT,
23 | // sort = ShotSortType.POPULAR
24 | //))
25 | //val dow = PageDownloader(searchPageParser.getUrl())
26 | //searchPageParser.parseHtml(shotsHtml)
27 | }
28 | }
29 |
30 | })
--------------------------------------------------------------------------------
/app-mvp/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # By default, the flags in this file are appended to flags specified
3 | # in D:\AndroidDevelopment\Programms\sdk/tools/proguard/proguard-android.txt
4 | # You can edit the include path and order by changing the proguardFiles
5 | # directive in build.gradle.kts.
6 | #
7 | # For more details, see
8 | # http://developer.android.com/guide/developing/tools/proguard.html
9 |
10 | # Add any project specific keep options here:
11 |
12 | # If your project uses WebView with JS, uncomment the following
13 | # and specify the fully qualified class name to the JavaScript interface
14 | # class:
15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
16 | # public *;
17 | #}
18 |
19 | # Uncomment this to preserve the line number information for
20 | # debugging stack traces.
21 | #-keepattributes SourceFile,LineNumberTable
22 |
23 | # If you keep the line number information, uncomment this to
24 | # hide the original source file name.
25 | #-renamesourcefileattribute SourceFile
26 |
27 | -keep public class * implements com.bumptech.glide.module.GlideModule
28 | -keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
29 | **[] $VALUES;
30 | public *;
31 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/interceptors/NetworkCheckInterceptor.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network.interceptors;
2 |
3 | import com.bubbble.core.network.NetworkChecker;
4 | import com.bubbble.core.network.NoNetworkException;
5 |
6 | import java.io.IOException;
7 | import java.net.SocketTimeoutException;
8 | import java.net.UnknownHostException;
9 |
10 | import okhttp3.Interceptor;
11 | import okhttp3.Request;
12 | import okhttp3.Response;
13 |
14 | public class NetworkCheckInterceptor implements Interceptor {
15 |
16 | private final NetworkChecker networkChecker;
17 |
18 | public NetworkCheckInterceptor(NetworkChecker networkChecker) {
19 | this.networkChecker = networkChecker;
20 | }
21 |
22 | @Override
23 | public Response intercept(Chain chain) throws IOException {
24 | Request.Builder requestBuilder = chain.request().newBuilder();
25 | if (!networkChecker.isConnected()) {
26 | throw new NoNetworkException();
27 | }
28 |
29 | try {
30 | return chain.proceed(requestBuilder.build());
31 | } catch (SocketTimeoutException | UnknownHostException e) {
32 | throw new NoNetworkException();
33 | }
34 |
35 | }
36 |
37 | }
38 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/adapters/LoadMoreViewHolder.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.adapters;
2 |
3 | import android.view.LayoutInflater;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.Button;
7 | import android.widget.ProgressBar;
8 |
9 | import androidx.recyclerview.widget.RecyclerView;
10 |
11 | import com.bubbble.coreui.R;
12 |
13 | public class LoadMoreViewHolder extends RecyclerView.ViewHolder {
14 |
15 | //public final ProgressBar loadMoreProgressBar;
16 | //public final ViewGroup loadMoreErrorLayout;
17 | //public final Button retryButton;
18 |
19 | public LoadMoreViewHolder(LayoutInflater layoutInflater, ViewGroup parent) {
20 | super(parent);
21 |
22 | // loadMoreProgressBar = itemView.findViewById(R.id.load_more_comments_progress);
23 | // loadMoreErrorLayout = itemView.findViewById(R.id.load_more_error_layout);
24 | // retryButton = itemView.findViewById(R.id.retry_button);
25 | }
26 |
27 | public void showError(boolean loadingError) {
28 | // loadMoreProgressBar.setVisibility(loadingError ? View.GONE : View.VISIBLE);
29 | // loadMoreErrorLayout.setVisibility(loadingError ? View.VISIBLE : View.GONE);
30 | }
31 |
32 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_twitter.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Bubbble
3 |
4 | by
5 | Like
6 | Reply
7 | Copy
8 |
9 | Download image
10 |
11 | Load failed retry again!
12 | Has no items
13 |
14 | Open in browser
15 |
16 | No comments
17 |
18 |
19 | - %s views
20 | - %s view
21 | - %s views
22 |
23 |
24 |
25 | - %s buckets
26 | - %s bucket
27 | - %s buckets
28 |
29 |
30 |
31 | - %s followers
32 | - %s follower
33 | - %s followers
34 |
35 |
36 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/di/CoreUiModule.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.di
2 |
3 | import com.bubbble.coreui.mvp.ErrorHandler
4 | import com.bubbble.coreui.permissions.AndroidPermissionsManager
5 | import com.bubbble.coreui.permissions.PermissionsManager
6 | import com.bubbble.coreui.resourcesmanager.AndroidResourcesManager
7 | import com.bubbble.coreui.resourcesmanager.ResourcesManager
8 | import dagger.Binds
9 | import dagger.Module
10 | import dagger.Provides
11 | import dagger.hilt.InstallIn
12 | import dagger.hilt.components.SingletonComponent
13 | import javax.inject.Singleton
14 |
15 | @Module
16 | @InstallIn(SingletonComponent::class)
17 | class CoreUiModule {
18 |
19 | @Provides
20 | @Singleton
21 | fun provideErrorHandler(resourcesManager: ResourcesManager): ErrorHandler {
22 | return ErrorHandler(resourcesManager)
23 | }
24 |
25 | @Module
26 | @InstallIn(SingletonComponent::class)
27 | interface Bindings {
28 |
29 | @Binds
30 | @Singleton
31 | fun resourcesManager(manager: AndroidResourcesManager): ResourcesManager
32 |
33 | @Binds
34 | @Singleton
35 | fun permissionsManager(manager: AndroidPermissionsManager): PermissionsManager
36 |
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/navigationargs/BundleNavDataSaver.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.navigationargs
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.os.Parcelable
6 | import androidx.fragment.app.Fragment
7 | import java.io.Serializable
8 |
9 | object BundleNavDataSaver {
10 |
11 | fun setData(intent: Intent, data: T) {
12 | val bundle = intent.extras ?: Bundle()
13 | bundle.putScreenData(data)
14 | intent.putExtras(bundle)
15 | }
16 |
17 | fun setData(fragment: Fragment, data: T) {
18 | val bundle = fragment.arguments ?: Bundle()
19 | bundle.putScreenData(data)
20 | fragment.arguments = bundle
21 | }
22 |
23 | fun setData(bundle: Bundle, data: T) {
24 | bundle.putScreenData(data)
25 | }
26 |
27 | private fun Bundle.putScreenData(data: T) {
28 | if (data is Parcelable) {
29 | putParcelable(NAV_DATA_KEY, data)
30 | return
31 | }
32 |
33 | if (data is Serializable) {
34 | putSerializable(NAV_DATA_KEY, data)
35 | return
36 | }
37 |
38 | throw IllegalArgumentException("Screen data class must be Parcelable or Serializable")
39 | }
40 |
41 | }
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/shots/parser/ShotRaw.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.shots.parser
2 |
3 | import com.google.gson.annotations.SerializedName
4 |
5 | data class ShotRaw(
6 | @SerializedName("id")
7 | val id: Long,
8 | @SerializedName("title")
9 | val title: String,
10 | @SerializedName("path")
11 | val path: String,
12 | @SerializedName("published_at")
13 | val publishedAt: String,
14 | @SerializedName("is_rebound")
15 | val isRebound: Boolean,
16 | @SerializedName("rebounds_count")
17 | val reboundsCount: Int,
18 | @SerializedName("attachments_count")
19 | val attachmentsCount: Int,
20 | @SerializedName("view_count")
21 | val viewCount: String,
22 | @SerializedName("comments_count")
23 | val commentsCount: String,
24 | @SerializedName("likes_count")
25 | val likesCount: String,
26 | @SerializedName("liked")
27 | val liked: Boolean,
28 | ) {
29 |
30 | val viewCountInt: Int = viewCount.normalizeNumber()
31 | val commentsCountInt: Int = viewCount.normalizeNumber()
32 | val likesCountInt: Int = viewCount.normalizeNumber()
33 |
34 | }
35 |
36 | private fun String.normalizeNumber() : Int {
37 | return replace(".", "")
38 | .replace("k", "000")
39 | .toInt()
40 | }
--------------------------------------------------------------------------------
/gradle/base/base-config.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.android.build.gradle.BaseExtension
2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3 |
4 | apply {
5 | plugin("kotlin-android")
6 | }
7 |
8 | configure {
9 | setCompileSdkVersion(Build.Versions.compileSdk)
10 | buildToolsVersion(Build.Versions.buildTools)
11 |
12 | defaultConfig {
13 | setMinSdkVersion(Build.Versions.minSdk)
14 | setTargetSdkVersion(Build.Versions.targetSdk)
15 |
16 | vectorDrawables.useSupportLibrary = true
17 |
18 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
19 | }
20 | buildTypes {
21 | getByName("debug") {
22 | isMinifyEnabled = false
23 | isDebuggable = true
24 | }
25 | getByName("release") {
26 | isMinifyEnabled = true
27 | proguardFiles(
28 | getDefaultProguardFile("proguard-android.txt"),
29 | "proguard-rules.pro"
30 | )
31 | }
32 | }
33 | compileOptions {
34 | targetCompatibility = JavaVersion.VERSION_1_8
35 | sourceCompatibility = JavaVersion.VERSION_1_8
36 | }
37 | }
38 |
39 | tasks.withType {
40 | kotlinOptions {
41 | jvmTarget = "1.8"
42 | }
43 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/OkHttpProvider.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network
2 |
3 | import okhttp3.ConnectionSpec
4 | import okhttp3.Interceptor
5 | import okhttp3.OkHttpClient
6 | import okhttp3.TlsVersion
7 | import java.util.concurrent.TimeUnit
8 | import javax.inject.Inject
9 |
10 | class OkHttpProvider @Inject constructor(
11 |
12 | ) {
13 |
14 | fun create(
15 | interceptors: List,
16 | networkInterceptors: List
17 | ): OkHttpClient.Builder {
18 | val spec = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
19 | .tlsVersions(TlsVersion.TLS_1_0)
20 | .allEnabledCipherSuites()
21 | .build()
22 | return OkHttpClient.Builder().apply {
23 | connectionSpecs(listOf(spec, ConnectionSpec.CLEARTEXT))
24 | connectTimeout(120, TimeUnit.SECONDS)
25 | readTimeout(120, TimeUnit.SECONDS)
26 | writeTimeout(120, TimeUnit.SECONDS)
27 | retryOnConnectionFailure(true)
28 |
29 | for (interceptor in interceptors) {
30 | addInterceptor(interceptor)
31 | }
32 |
33 | for (networkInterceptor in networkInterceptors) {
34 | addNetworkInterceptor(networkInterceptor)
35 | }
36 | }
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Bubbble
3 |
4 | от
5 | Лайк
6 | Ответить
7 | Копировать
8 |
9 | Загрузить изображение
10 |
11 | Ошибка загрузки, попробуйте еще раз!
12 | Пусто
13 |
14 | Открыть в браузере
15 |
16 | Комментариев нет
17 |
18 |
19 | - нет просмотров
20 | - %s просмотр
21 | - %s просмотра
22 | - %s просмотра
23 | - %s просмотров
24 | - %s просмотров
25 |
26 |
27 |
28 | - нет бакетов
29 | - %s бакет
30 | - %s бакета
31 | - %s бакета
32 | - %s бакетов
33 | - %s бакетов
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/ApiConstants.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network
2 |
3 | object ApiConstants {
4 |
5 | const val DRIBBBLE_API_URL = "https://api.dribbble.com/v2/"
6 |
7 | object Authorize {
8 | const val path = "oauth/authorize"
9 |
10 | const val p_client_id = "client_id"
11 | const val p_redirect_uri = "redirect_uri"
12 | const val p_scope = "scope"
13 | const val p_state = "state"
14 | }
15 |
16 | object Token {
17 | const val path = "oauth/token"
18 |
19 | const val p_client_id = "client_id"
20 | const val p_client_secret = "client_secret"
21 | const val p_code = "code"
22 | const val p_redirect_uri = "redirect_uri"
23 | }
24 |
25 | object Shots {
26 | const val path = "user/shots"
27 |
28 | const val p_image = "image "
29 | const val p_title = "title "
30 | const val p_description = "description "
31 | const val p_low_profile = "low_profile "
32 | const val p_rebound_source_id = "rebound_source_id"
33 | const val p_scheduled_for = "scheduled_for"
34 | const val p_tags = "tags "
35 | const val p_team_id = "team_id"
36 | }
37 |
38 | object Projects {
39 | const val path = "user/projects"
40 |
41 | const val p_name = "name"
42 | const val p_description = "description"
43 | }
44 |
45 | }
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/shots/search/SearchPageParser.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.shots.search
2 |
3 | import com.bubbble.core.models.search.SearchParams
4 | import com.bubbble.core.models.search.SearchType
5 | import com.bubbble.core.network.Dribbble
6 | import com.bubbble.data.global.paging.PagingParams
7 | import com.bubbble.data.shots.parser.CommonShotsPageParser
8 | import com.google.gson.Gson
9 | import okhttp3.HttpUrl
10 | import okhttp3.HttpUrl.Companion.toHttpUrl
11 | import javax.inject.Inject
12 |
13 | class SearchPageParser @Inject constructor(
14 | gson: Gson
15 | ) : CommonShotsPageParser(gson) {
16 |
17 | override fun getUrl(
18 | baseUrl: String,
19 | params: SearchParams,
20 | pagingParams: PagingParams
21 | ): HttpUrl {
22 | return Dribbble.Search.search(
23 | query = params.searchQuery,
24 | type = params.searchType.code
25 | ).toHttpUrl()
26 | .newBuilder()
27 | .addQueryParameter("page", pagingParams.page.toString())
28 | .addQueryParameter("per_page", pagingParams.pageSize.toString())
29 | .build()
30 | }
31 |
32 | private val SearchType.code: String?
33 | get() = when (this) {
34 | SearchType.SHOT -> Dribbble.Search.shots
35 | SearchType.MEMBERS -> Dribbble.Search.users
36 | SearchType.TEAM -> Dribbble.Search.teams
37 | }
38 |
39 | }
--------------------------------------------------------------------------------
/core/network/src/main/java/com/bubbble/core/network/ApiBuilder.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.core.network
2 |
3 | import android.content.Context
4 | import com.bubbble.core.network.di.ApiInterceptors
5 | import com.bubbble.core.network.di.ApiNetworkInterceptors
6 | import com.bubbble.core.network.di.BaseApiUrl
7 | import okhttp3.Interceptor
8 | import retrofit2.Retrofit
9 | import retrofit2.converter.gson.GsonConverterFactory
10 | import javax.inject.Inject
11 | import kotlin.reflect.KClass
12 |
13 | class ApiBuilder @Inject constructor(
14 | private val context: Context,
15 | private val okHttpProvider: OkHttpProvider,
16 | private val gsonProvider: GsonProvider,
17 | @BaseApiUrl private val baseUrl: String,
18 | @ApiInterceptors private val interceptors: List,
19 | @ApiNetworkInterceptors private val networkInterceptors: List
20 | ) {
21 |
22 | fun createApi(apiClass: KClass): T {
23 | val client = okHttpProvider.create(getInterceptors(), networkInterceptors).build()
24 | val retrofit = Retrofit.Builder()
25 | .addConverterFactory(GsonConverterFactory.create(gsonProvider.create()))
26 | .client(client)
27 | .baseUrl(baseUrl)
28 | .build()
29 | return retrofit.create(apiClass.java)
30 | }
31 |
32 | private fun getInterceptors(): List = listOf(
33 | *interceptors.toTypedArray()
34 | )
35 |
36 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/res/drawable/ic_shot.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
25 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/drawable/dribbble_lined_logo_pink.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
25 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/shots/feed/FeedShotsParser.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.shots.feed
2 |
3 | import com.bubbble.core.models.feed.ShotsFeedParams
4 | import com.bubbble.core.network.Dribbble
5 | import com.bubbble.data.global.paging.PagingParams
6 | import com.bubbble.data.shots.parser.CommonShotsPageParser
7 | import com.google.gson.Gson
8 | import okhttp3.HttpUrl
9 | import okhttp3.HttpUrl.Companion.toHttpUrl
10 | import javax.inject.Inject
11 |
12 | class FeedShotsParser @Inject constructor(
13 | gson: Gson
14 | ) : CommonShotsPageParser(gson) {
15 |
16 | override fun getUrl(
17 | baseUrl: String,
18 | params: ShotsFeedParams,
19 | pagingParams: PagingParams
20 | ): HttpUrl {
21 | return Dribbble.URL.toHttpUrl()
22 | .newBuilder()
23 | .addQueryParameter(Dribbble.Shots.p_page, pagingParams.page.toString())
24 | .addQueryParameter(Dribbble.Shots.p_page_size, pagingParams.pageSize.toString())
25 | .apply {
26 | val sortCode = params.sort?.code
27 | if (sortCode != null) {
28 | addPathSegment(Dribbble.Shots.path)
29 | addPathSegment(sortCode)
30 | }
31 | }
32 | .build()
33 | }
34 |
35 | val ShotsFeedParams.Sorting.code: String?
36 | get() = when (this) {
37 | ShotsFeedParams.Sorting.POPULAR -> Dribbble.Shots.Sort.popular
38 | ShotsFeedParams.Sorting.RECENT -> Dribbble.Shots.Sort.recent
39 | else -> null
40 | }
41 |
42 |
43 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
10 |
11 |
18 |
19 |
26 |
27 |
28 |
33 |
34 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/mvp/ErrorHandler.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.mvp
2 |
3 | import android.util.Log
4 | import com.bubbble.core.network.NoNetworkException
5 | import com.bubbble.coreui.R
6 | import com.bubbble.coreui.resourcesmanager.ResourcesManager
7 | import java.io.IOException
8 | import java.net.UnknownHostException
9 | import javax.inject.Inject
10 | import javax.inject.Singleton
11 | import javax.net.ssl.SSLHandshakeException
12 | import kotlin.reflect.KClass
13 |
14 | @Singleton
15 | class ErrorHandler @Inject constructor(
16 | private val resourcesManager: ResourcesManager
17 | ) {
18 |
19 | fun proceed(
20 | error: Throwable,
21 | handledErrorTypes: List> = emptyList(),
22 | errorHandler: (Throwable) -> Unit = {},
23 | messageListener: (String) -> Unit = {}
24 | ) {
25 | Log.e("Bubbble", error.message, error)
26 | if (handledErrorTypes.contains(error::class)) {
27 | errorHandler(error)
28 | } else {
29 | messageListener(getMessageForError(error))
30 | }
31 | }
32 |
33 | private fun getMessageForError(error: Throwable): String {
34 | val messageResId = when (error) {
35 | is NoNetworkException -> R.string.network_error
36 | is UnknownHostException -> R.string.check_network_message
37 | is SSLHandshakeException -> R.string.ssl_error_check_device_time_message
38 | is IOException -> R.string.network_error
39 | else -> R.string.unknown_error
40 | }
41 | return resourcesManager.getString(messageResId)
42 | }
43 |
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/app-mvp/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("kapt")
3 | }
4 |
5 | android {
6 | signingConfigs {
7 | create("bubbble") {
8 | storeFile(rootProject.file("keystore.jks"))
9 | storePassword = "bubbble"
10 | keyAlias = "bubbble"
11 | keyPassword = "bubbble"
12 | }
13 | }
14 |
15 | buildTypes {
16 | getByName("debug") {
17 | signingConfig = signingConfigs.getByName("bubbble")
18 | applicationIdSuffix = ".debug"
19 | }
20 |
21 | getByName("release") {
22 | signingConfig = signingConfigs.getByName("bubbble")
23 | isMinifyEnabled = false
24 | proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
25 | }
26 | }
27 | }
28 |
29 | dependencies {
30 | implementation(project(Modules.Core.di))
31 | implementation(project(Modules.Core.core))
32 | implementation(project(Modules.Core.data))
33 | implementation(project(Modules.AppMvp.coreUi))
34 | implementation(project(Modules.AppMvp.featureShots))
35 | implementation(project(Modules.AppMvp.featureShotsSearch))
36 | implementation(project(Modules.AppMvp.featureShotDetails))
37 | implementation(project(Modules.AppMvp.featureUserProfile))
38 |
39 | kapt(Dependencies.Mvp.moxyCompiler)
40 |
41 | // Developer Tools
42 | debugImplementation(Dependencies.DevTools.stetho)
43 | debugImplementation(Dependencies.DevTools.stethoOkHttp)
44 | debugImplementation(Dependencies.DevTools.okHttpLogging)
45 | debugImplementation(Dependencies.DevTools.ok2curl)
46 | debugImplementation(Dependencies.DevTools.leakCanary)
47 | releaseImplementation(Dependencies.DevTools.leakCanaryNoOp)
48 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/navigationargs/NavData.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.navigationargs
2 |
3 | import android.app.Activity
4 | import android.content.Context
5 | import android.content.Intent
6 | import android.os.Bundle
7 | import androidx.fragment.app.Fragment
8 |
9 | inline fun Activity.screenData(): ActivityNavDataProperty {
10 | return ActivityNavDataProperty()
11 | }
12 |
13 | inline fun Fragment.screenData(): FragmentNavDataProperty {
14 | return FragmentNavDataProperty()
15 | }
16 |
17 | inline fun Intent.getScreenData(): T {
18 | val extractor = BundleNavDataExtractor()
19 | return extractor.getData(extras)
20 | }
21 |
22 | inline fun Activity.getScreenData(): T {
23 | val extractor = BundleNavDataExtractor()
24 | return extractor.getData(intent.extras)
25 | }
26 |
27 | inline fun Fragment.getScreenData(): T {
28 | val extractor = BundleNavDataExtractor()
29 | return extractor.getData(arguments)
30 | }
31 |
32 | fun Intent.setScreenData(data: T) {
33 | BundleNavDataSaver.setData(this, data)
34 | }
35 |
36 | fun Fragment.setScreenData(data: T) {
37 | BundleNavDataSaver.setData(this, data)
38 | }
39 |
40 | fun Bundle.setScreenData(data: T) {
41 | BundleNavDataSaver.setData(this, data)
42 | }
43 |
44 | inline fun buildIntent(
45 | context: Context,
46 | data: Any
47 | ): Intent {
48 | val intent = Intent(context, T::class.java)
49 | intent.setScreenData(data)
50 | return intent
51 | }
52 |
53 | inline fun createFragment(
54 | data: Any
55 | ): Fragment {
56 | val fragment = T::class.java.newInstance()
57 | fragment.setScreenData(data)
58 | return fragment
59 | }
--------------------------------------------------------------------------------
/app-mvp/app/src/unitTests/java/com/bubbble/test/BubbbleTestRunner.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.test;
2 |
3 | import android.os.Build;
4 | import androidx.annotation.NonNull;
5 |
6 | import com.bubbble.BubbbleTestApplication;
7 | import com.bubbble.BuildConfig;
8 |
9 | import org.robolectric.RobolectricTestRunner;
10 | import org.robolectric.RuntimeEnvironment;
11 | import org.robolectric.annotation.Config;
12 |
13 | import java.lang.reflect.Method;
14 |
15 | public class BubbbleTestRunner extends RobolectricTestRunner {
16 |
17 | private static final int SDK_EMULATE_LEVEL = Build.VERSION_CODES.M;
18 |
19 | public BubbbleTestRunner(@NonNull Class> clazz) throws Exception {
20 | super(clazz);
21 | }
22 |
23 | @Override
24 | public Config getConfig(@NonNull Method method) {
25 | final Config defaultConfig = super.getConfig(method);
26 | return new Config.Implementation(
27 | new int[]{SDK_EMULATE_LEVEL},
28 | defaultConfig.manifest(),
29 | defaultConfig.qualifiers(),
30 | defaultConfig.packageName(),
31 | defaultConfig.abiSplit(),
32 | defaultConfig.resourceDir(),
33 | defaultConfig.assetDir(),
34 | defaultConfig.buildDir(),
35 | defaultConfig.shadows(),
36 | defaultConfig.instrumentedPackages(),
37 | BubbbleTestApplication.class, // Notice that we override real application class for Unit tests.
38 | defaultConfig.libraries(),
39 | defaultConfig.constants() == Void.class ? BuildConfig.class : defaultConfig.constants()
40 | );
41 | }
42 |
43 | @NonNull
44 | public static BubbbleTestApplication bubbbleTestApplication() {
45 | return (BubbbleTestApplication) RuntimeEnvironment.application;
46 | }
47 |
48 | }
--------------------------------------------------------------------------------
/app-mvp/feature-user-profile/src/main/java/com/bubbble/userprofile/details/UserDetailsPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.userprofile.details
2 |
3 | import moxy.InjectViewState
4 | import com.bubbble.core.models.user.User
5 | import com.bubbble.core.network.NoNetworkException
6 | import com.bubbble.coreui.mvp.BasePresenter
7 | import com.bubbble.data.users.UsersRepository
8 | import dagger.assisted.Assisted
9 | import dagger.assisted.AssistedFactory
10 | import dagger.assisted.AssistedInject
11 |
12 | @InjectViewState
13 | class UserDetailsPresenter @AssistedInject constructor(
14 | private val usersRepository: UsersRepository,
15 | @Assisted private val userName: String
16 | ) : BasePresenter() {
17 |
18 | private lateinit var user: User
19 | private val isUserLoaded: Boolean
20 | get() = ::user.isInitialized
21 |
22 | override fun onFirstViewAttach() {
23 | loadUser()
24 | }
25 |
26 | private fun loadUser() = launchSafe {
27 | viewState.showLoadingProgress(true)
28 | try {
29 | user = usersRepository.getUser(userName)
30 | viewState.showUserInfo(user)
31 | } catch (e: NoNetworkException) {
32 | viewState.showNoNetworkLayout(true)
33 | } finally {
34 | viewState.showLoadingProgress(false)
35 | }
36 | }
37 |
38 | fun retryLoading() {
39 | viewState.showNoNetworkLayout(false)
40 | loadUser()
41 | }
42 |
43 | fun onUserTwitterButtonClicked() {
44 | if (!isUserLoaded) return
45 | viewState.openInBrowser(user.links.twitter)
46 | }
47 |
48 | fun onUserWebsiteButtonClicked() {
49 | if (!isUserLoaded) return
50 | viewState.openInBrowser(user.links.web)
51 | }
52 |
53 | @AssistedFactory
54 | interface Factory {
55 | fun create(userName: String): UserDetailsPresenter
56 | }
57 | }
--------------------------------------------------------------------------------
/commons/dribbble_lined_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
49 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/commons/glide/GlideCircleTransform.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.commons.glide;
2 |
3 | import android.content.Context;
4 | import android.graphics.Bitmap;
5 | import android.graphics.BitmapShader;
6 | import android.graphics.Canvas;
7 | import android.graphics.Paint;
8 |
9 | import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
10 | import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
11 |
12 | public class GlideCircleTransform extends BitmapTransformation {
13 |
14 | public GlideCircleTransform(Context context) {
15 | super(context);
16 | }
17 |
18 | @Override
19 | protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {
20 | return circleCrop(pool, toTransform);
21 | }
22 |
23 | private static Bitmap circleCrop(BitmapPool pool, Bitmap source) {
24 | if (source == null) return null;
25 |
26 | int size = Math.min(source.getWidth(), source.getHeight());
27 | int x = (source.getWidth() - size) / 2;
28 | int y = (source.getHeight() - size) / 2;
29 |
30 | // TODO this could be acquired from the pool too
31 | Bitmap squared = Bitmap.createBitmap(source, x, y, size, size);
32 |
33 | Bitmap result = pool.get(size, size, Bitmap.Config.ARGB_8888);
34 | if (result == null) {
35 | result = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
36 | }
37 |
38 | Canvas canvas = new Canvas(result);
39 | Paint paint = new Paint();
40 | paint.setShader(new BitmapShader(squared, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
41 | paint.setAntiAlias(true);
42 | float r = size / 2f;
43 | canvas.drawCircle(r, r, r, paint);
44 | return result;
45 | }
46 |
47 | @Override
48 | public String getId() {
49 | return getClass().getName();
50 | }
51 |
52 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/MockWebServerExtensions.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver
2 |
3 | import com.bubbble.tests.extensions.readTextFile
4 | import com.lectra.koson.ArrayType
5 | import com.lectra.koson.ObjectType
6 | import io.kotest.core.spec.style.scopes.DescribeScope
7 | import kotlinx.coroutines.runBlocking
8 | import okhttp3.mockwebserver.MockResponse
9 | import com.bubbble.tests.mockserver.request.BubbbleMockWebServer
10 |
11 | private val webServers = mutableMapOf()
12 |
13 | fun DescribeScope.initWebServer(
14 | configure: BubbbleMockWebServer.() -> Unit
15 | ): BubbbleMockWebServer {
16 | val serverKey = this
17 | beforeEach {
18 | val webServer = BubbbleMockWebServer()
19 | webServers[serverKey] = webServer
20 | webServer.configure()
21 | webServer.start()
22 | }
23 | afterEach {
24 | webServers[serverKey]!!.shutdown()
25 | }
26 | return BubbbleMockWebServer()
27 | }
28 |
29 | suspend fun DescribeScope.apiTest(
30 | description: String,
31 | body: suspend ApiTest.() -> Unit
32 | ) {
33 | val serverKey = this
34 | it(description) {
35 | val webServer = webServers[serverKey] ?: throw IllegalStateException("WebServer not initialized")
36 | val apiTest = ApiTest(webServer)
37 | runBlocking { body(apiTest) }
38 | }
39 | }
40 |
41 | fun MockResponse.setBodyFromFile(
42 | jsonFilePath: String,
43 | responseCode: Int = 200
44 | ) {
45 | setBody(readTextFile(jsonFilePath))
46 | setResponseCode(responseCode)
47 | }
48 |
49 |
50 | fun MockResponse.setJsonBody(
51 | body: ObjectType,
52 | responseCode: Int = 200
53 | ) {
54 | setBody(body.toString())
55 | setResponseCode(responseCode)
56 | }
57 |
58 | fun MockResponse.setJsonBody(
59 | body: ArrayType,
60 | responseCode: Int = 200
61 | ) {
62 | setBody(body.toString())
63 | setResponseCode(responseCode)
64 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/adapters/LoadMoreAdapter.java:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.adapters;
2 |
3 | import android.view.LayoutInflater;
4 | import android.view.ViewGroup;
5 |
6 | import androidx.recyclerview.widget.RecyclerView;
7 |
8 |
9 | public abstract class LoadMoreAdapter extends HeaderFooterAdapter {
10 |
11 | public interface OnRetryLoadMoreListener {
12 | void onRetryLoadMore();
13 | }
14 |
15 | private OnRetryLoadMoreListener onRetryLoadMoreListener;
16 | private boolean loadingError = false;
17 |
18 | public LoadMoreAdapter(boolean withHeader, boolean withFooter) {
19 | super(withHeader, withFooter);
20 | }
21 |
22 | public void setOnRetryLoadMoreListener(OnRetryLoadMoreListener onRetryLoadMoreListener) {
23 | this.onRetryLoadMoreListener = onRetryLoadMoreListener;
24 | }
25 |
26 | @Override
27 | protected RecyclerView.ViewHolder getFooterViewHolder(LayoutInflater inflater, ViewGroup parent) {
28 | return new LoadMoreViewHolder(inflater, parent);
29 | }
30 |
31 | @Override
32 | public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
33 | if (holder instanceof LoadMoreViewHolder) {
34 | LoadMoreViewHolder loadMoreViewHolder = (LoadMoreViewHolder) holder;
35 | // loadMoreViewHolder.retryButton.setOnClickListener(v -> onRetryLoadMoreClick());
36 | loadMoreViewHolder.showError(loadingError);
37 | }
38 | }
39 |
40 | private void onRetryLoadMoreClick() {
41 | if (onRetryLoadMoreListener != null) {
42 | onRetryLoadMoreListener.onRetryLoadMore();
43 | }
44 | }
45 |
46 | public void setLoadingMore(boolean isLoading) {
47 | this.loadingError = !isLoading;
48 | showFooter(isLoading);
49 | }
50 |
51 | public void setLoadingError(boolean loadingError) {
52 | this.loadingError = loadingError;
53 | showFooter(loadingError);
54 | }
55 |
56 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/request/QueryParams.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver.request
2 |
3 | import com.bubbble.tests.extensions.assertEquals
4 | import java.io.UnsupportedEncodingException
5 | import java.net.URLDecoder
6 | import kotlin.jvm.Throws
7 |
8 | class QueryParams(path: String) {
9 |
10 | private val params: Map
11 |
12 | init {
13 | params = getUrlParameters(path)
14 | }
15 |
16 | val count = params.size
17 |
18 | fun get(key: String) = params[key]
19 |
20 | fun getBool(key: String) = get(key).toBoolean()
21 |
22 | fun getInt(key: String) = get(key)?.toInt()
23 |
24 | fun getLong(key: String) = get(key)?.toLong()
25 |
26 | fun getFloat(key: String) = get(key)?.toFloat()
27 |
28 | fun assertEquals(key: String, value: String) {
29 | get(key).assertEquals(value)
30 | }
31 |
32 | fun assertEquals(key: String, value: Int) {
33 | getInt(key).assertEquals(value)
34 | }
35 |
36 | fun assertEquals(key: String, value: Long) {
37 | getLong(key).assertEquals(value)
38 | }
39 |
40 | fun assertEquals(key: String, value: Boolean) {
41 | getBool(key).assertEquals(value)
42 | }
43 |
44 | @Throws(UnsupportedEncodingException::class)
45 | private fun getUrlParameters(url: String): MutableMap {
46 | val params: MutableMap = HashMap()
47 | val urlParts = url.split("?").toTypedArray()
48 | if (urlParts.size > 1) {
49 | val query = urlParts[1]
50 | for (param in query.split("&").toTypedArray()) {
51 | val pair = param.split("=").toTypedArray()
52 | val key: String = URLDecoder.decode(pair[0], "UTF-8")
53 | var value = ""
54 | if (pair.size > 1) {
55 | value = URLDecoder.decode(pair[1], "UTF-8")
56 | }
57 | params[key] = value
58 | }
59 | }
60 | return params
61 | }
62 |
63 | }
--------------------------------------------------------------------------------
/core/ui/src/main/res/drawable/ic_bucket.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
27 |
--------------------------------------------------------------------------------
/app-mvp/feature-shots-search/src/main/res/layout/activity_shots_search.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
11 |
12 |
20 |
21 |
22 |
23 |
27 |
28 |
34 |
35 |
39 |
40 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/layout/item_network_state.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
15 |
16 |
22 |
23 |
24 |
25 |
33 |
34 |
42 |
43 |
51 |
52 |
--------------------------------------------------------------------------------
/app-mvp/feature-shots/src/main/java/com/bubbble/shots/shotslist/ShotsPresenter.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.shots.shotslist
2 |
3 | import androidx.paging.CombinedLoadStates
4 | import androidx.paging.LoadState
5 | import com.bubbble.core.models.feed.ShotsFeedParams
6 | import com.bubbble.core.models.shot.Shot
7 | import com.bubbble.coreui.mvp.BasePresenter
8 | import com.bubbble.data.shots.ShotsRepository
9 | import com.bubbble.shots.api.ShotsNavigationFactory
10 | import dagger.assisted.Assisted
11 | import dagger.assisted.AssistedFactory
12 | import dagger.assisted.AssistedInject
13 | import kotlinx.coroutines.flow.collectLatest
14 | import moxy.InjectViewState
15 |
16 | @InjectViewState
17 | class ShotsPresenter @AssistedInject constructor(
18 | private val shotsRepository: ShotsRepository,
19 | private val navigationFactory: ShotsNavigationFactory,
20 | @Assisted private val shotsSort: ShotsFeedParams.Sorting
21 | ) : BasePresenter() {
22 |
23 | override fun onFirstViewAttach() {
24 | loadShots()
25 | }
26 |
27 | private fun loadShots() = launchSafe {
28 | val params = ShotsFeedParams(shotsSort)
29 | shotsRepository.getShots(params).collectLatest { pagingData ->
30 | viewState.showPagingData(pagingData)
31 | }
32 | }
33 |
34 | fun retryLoading() {
35 | viewState.retryLoading()
36 | }
37 |
38 | fun onShotClick(shot: Shot) {
39 | router.navigateTo(navigationFactory.shotDetailsScreen(shot.shotSlug))
40 | }
41 |
42 | fun onListStateChanged(loadState: CombinedLoadStates) {
43 | val state = loadState.refresh
44 | if (state is LoadState.Error) {
45 | errorHandler.proceed(state.error)
46 | }
47 | viewState.updateListState(
48 | isProgressBarVisible = state is LoadState.Loading,
49 | isRetryVisible = state is LoadState.Error,
50 | isErrorMsgVisible = state is LoadState.Error
51 | )
52 | }
53 |
54 | @AssistedFactory
55 | interface Factory {
56 | fun create(shotsSort: ShotsFeedParams.Sorting): ShotsPresenter
57 | }
58 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/base/BaseMvpFragment.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.base
2 |
3 | import android.os.Bundle
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import android.widget.Toast
8 | import moxy.MvpAppCompatFragment
9 |
10 | abstract class BaseMvpFragment : MvpAppCompatFragment() {
11 |
12 | abstract val layoutRes: Int
13 |
14 | private var instanceStateSaved: Boolean = false
15 | protected var lastBundle: Bundle? = null
16 |
17 | // protected val navigator by lazy { getGlobal() }
18 | // protected val screenResolver by lazy { getGlobal() }
19 |
20 | override fun onCreateView(
21 | inflater: LayoutInflater,
22 | container: ViewGroup?,
23 | savedInstanceState: Bundle?
24 | ) = inflater.inflate(layoutRes, container, false)
25 |
26 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
27 | super.onViewCreated(view, savedInstanceState)
28 |
29 | if (savedInstanceState != null) {
30 | onNewBundle(savedInstanceState)
31 | }
32 | }
33 |
34 | override fun onResume() {
35 | super.onResume()
36 | instanceStateSaved = false
37 |
38 | val bundle = lastBundle
39 | if (bundle != null) {
40 | onNewBundle(bundle)
41 | }
42 | }
43 |
44 | open fun onBackPressed(): Boolean {
45 | return false
46 | }
47 |
48 | fun setNewBundle(bundle: Bundle) {
49 | lastBundle = bundle
50 | onNewBundle(bundle)
51 | }
52 |
53 | open fun onNewBundle(bundle: Bundle) {
54 | lastBundle = null
55 | }
56 |
57 | // override fun onScreenResult(screenClass: Class, result: ScreenResult?) {
58 | //
59 | // }
60 |
61 | fun showMessage(text: String) {
62 | Toast.makeText(context, text, Toast.LENGTH_SHORT).show()
63 | }
64 |
65 | fun showMessage(textResId: Int) {
66 | showMessage(getString(textResId))
67 | }
68 |
69 | //protected fun getScreen() = screenResolver.getScreen(this)
70 |
71 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/res/layout/view_network_error.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
18 |
19 |
32 |
33 |
44 |
45 |
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | by
3 | Like
4 | Reply
5 | Copy
6 |
7 | Download image
8 | Share
9 |
10 | Error
11 | Bubbble cannot work properly with no access to files on the device storage. Please, allow access to your storage.
12 | Bubbble cannot work properly with no access to files on the device storage. To enable the access please open Bubbble page in system Settings application, and click Permissions.
13 | OK
14 | Settings
15 | Image saved to downloads folder
16 |
17 |
18 | - %s likes
19 | - %s like
20 | - %s likes
21 |
22 |
23 |
24 | - %s views
25 | - %s view
26 | - %s views
27 |
28 |
29 |
30 | - %s buckets
31 | - %s bucket
32 | - %s buckets
33 |
34 |
35 |
36 | - %s shots
37 | - %s shot
38 | - %s shots
39 |
40 |
41 |
42 | - %s followers
43 | - %s follower
44 | - %s followers
45 |
46 |
47 |
48 | An error occurred while loading image
49 |
50 |
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/base/BaseMvpActivity.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.base
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import android.widget.Toast
6 | import com.bubbble.coreui.di.coreUiEntrypoint
7 | import com.bubbble.coreui.permissions.AndroidPermissionsManager
8 | import com.github.terrakok.cicerone.androidx.AppNavigator
9 | import kotlinx.coroutines.CoroutineScope
10 | import kotlinx.coroutines.Dispatchers
11 | import kotlinx.coroutines.Job
12 | import moxy.MvpAppCompatActivity
13 | import kotlin.coroutines.CoroutineContext
14 |
15 | abstract class BaseMvpActivity : MvpAppCompatActivity(), CoroutineScope {
16 |
17 | abstract val layoutRes: Int
18 |
19 | protected val parentJob = Job()
20 | override val coroutineContext: CoroutineContext
21 | get() = Dispatchers.Main + parentJob
22 |
23 | private val navigator = AppNavigator(this, -1)
24 | private val navigatorHolder = coreUiEntrypoint.navigatorHolder
25 | protected val permissionsManager by lazy {
26 | coreUiEntrypoint.permissionsManager as AndroidPermissionsManager
27 | }
28 |
29 | override fun onCreate(savedInstanceState: Bundle?) {
30 | super.onCreate(savedInstanceState)
31 | setContentView(layoutRes)
32 | }
33 |
34 | override fun onResumeFragments() {
35 | super.onResumeFragments()
36 |
37 | navigatorHolder.setNavigator(navigator)
38 | permissionsManager.attachActivity(this)
39 | }
40 |
41 | override fun onNewIntent(intent: Intent?) {
42 | super.onNewIntent(intent ?: return)
43 | val topFragment = supportFragmentManager.fragments.lastOrNull() as? BaseMvpFragment
44 | topFragment?.onNewBundle(intent.extras ?: return)
45 | }
46 |
47 | override fun onPause() {
48 | navigatorHolder.removeNavigator()
49 | permissionsManager.detachActivity()
50 | super.onPause()
51 | }
52 |
53 | override fun onDestroy() {
54 | super.onDestroy()
55 |
56 | parentJob.cancel()
57 | }
58 |
59 | fun showMessage(text: String) {
60 | Toast.makeText(this, text, Toast.LENGTH_SHORT).show()
61 | }
62 |
63 | fun showMessage(textResId: Int) {
64 | showMessage(getString(textResId))
65 | }
66 |
67 | }
--------------------------------------------------------------------------------
/core/di/src/main/java/com/bubbble/di/injector/ComponentManager.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.di.injector
2 |
3 | import kotlin.reflect.KClass
4 |
5 | object ComponentManager {
6 |
7 | private val holderStorage = mutableMapOf>()
8 | private val factoryStorage = mutableMapOf>()
9 |
10 | fun registerFactory(factory: ComponentFactory, klass: KClass) {
11 | factoryStorage[klass.qualifiedName!!] = factory
12 | }
13 |
14 | inline fun getOrCreateComponent(): T {
15 | return getOrCreateComponent(T::class)
16 | }
17 |
18 | @Suppress("UNCHECKED_CAST")
19 | fun getOrCreateComponent(klass: KClass): T {
20 | return getComponentHolder(klass).getOrCreate()
21 | }
22 |
23 | inline fun getComponent(): T {
24 | return getComponent(T::class)
25 | }
26 |
27 | @Suppress("UNCHECKED_CAST")
28 | fun getComponent(klass: KClass): T {
29 | if (!factoryStorage.contains(klass.qualifiedName!!))
30 | throw IllegalStateException("Factory for ${klass.qualifiedName!!} not registered")
31 | return getComponentHolder(klass).getWithoutRef()
32 | }
33 |
34 | fun releaseComponent(klass: KClass) {
35 | getComponentHolder(klass).release()
36 | }
37 |
38 | @Suppress("UNCHECKED_CAST")
39 | private fun getComponentHolder(klass: KClass): ComponentHolder {
40 | val key = klass.qualifiedName!!
41 | return holderStorage.getOrElse(key) {
42 | val componentHolder = ComponentHolder(getFactoryForClass(klass), this)
43 | holderStorage[key] = componentHolder
44 | return@getOrElse componentHolder
45 | } as ComponentHolder
46 | }
47 |
48 | @Suppress("UNCHECKED_CAST")
49 | private fun getFactoryForClass(klass: KClass): ComponentFactory {
50 | if (!factoryStorage.contains(klass.qualifiedName!!))
51 | throw IllegalStateException("Factory for ${klass.qualifiedName!!} not registered")
52 | return factoryStorage[klass.qualifiedName!!] as ComponentFactory
53 | }
54 |
55 | }
--------------------------------------------------------------------------------
/app-mvp/feature-shot-details/src/main/res/values-ru/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | от
3 | Лайк
4 | Ответить
5 | Копировать
6 |
7 | Загрузить изображение
8 | Поделиться
9 |
10 | Ошибка
11 | Bubbble не может работать без доступа к файлам на устройстве. Пожалуйста предоставьте доступ к файлам.
12 | Bubbble не может работать без доступа к файлам на устройстве. Чтобы включить этот доступ, откройте страницу Bubbbleа в системных настройках, и зайдите в раздел «Разрешения».
13 | OK
14 | Настройки
15 |
16 | Изображение сохранено в папку загрузок
17 |
18 |
19 | - нет лайков
20 | - %s лайк
21 | - %s лайка
22 | - %s лайка
23 | - %s лайков
24 | - %s лайков
25 |
26 |
27 |
28 | - нет просмотров
29 | - %s просмотр
30 | - %s просмотра
31 | - %s просмотра
32 | - %s просмотров
33 | - %s просмотров
34 |
35 |
36 |
37 | - нет бакетов
38 | - %s бакет
39 | - %s бакета
40 | - %s бакета
41 | - %s бакетов
42 | - %s бакетов
43 |
44 |
45 |
46 | Произошла ошибка при загрузке изображения
47 |
48 |
--------------------------------------------------------------------------------
/core/data/src/main/java/com/bubbble/data/shots/ShotsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.data.shots
2 |
3 | import androidx.paging.Pager
4 | import androidx.paging.PagingConfig
5 | import com.bubbble.core.models.shot.Shot
6 | import com.bubbble.core.models.feed.ShotsFeedParams
7 | import com.bubbble.core.models.search.SearchParams
8 | import com.bubbble.core.models.shot.ShotDetails
9 | import com.bubbble.core.models.shot.ShotDetailsParams
10 | import com.bubbble.core.models.user.UserShotsParams
11 | import com.bubbble.core.network.DribbbleApi
12 | import com.bubbble.data.global.parsing.PageParserManager
13 | import com.bubbble.data.shots.feed.FeedShotsParser
14 | import com.bubbble.data.global.paging.CommonPagingSource
15 | import com.bubbble.data.shots.details.ShotDetailsParser
16 | import com.bubbble.data.shots.search.SearchPageParser
17 | import javax.inject.Inject
18 | import javax.inject.Singleton
19 |
20 | @Singleton
21 | class ShotsRepository @Inject constructor(
22 | private val dribbbleApi: DribbbleApi,
23 | private val pageParserManager: PageParserManager,
24 | private val feedShotsParser: FeedShotsParser,
25 | private val searchPageParser: SearchPageParser,
26 | private val shotDetailsParser: ShotDetailsParser
27 | ) {
28 |
29 | fun getShots(
30 | requestParams: ShotsFeedParams
31 | ) = Pager(PagingConfig(pageSize = 20)) {
32 | CommonPagingSource { pagingParams ->
33 | pageParserManager.parse(
34 | feedShotsParser,
35 | requestParams,
36 | pagingParams
37 | )
38 | }
39 | }.flow
40 |
41 | suspend fun getShot(shotSlug: String): ShotDetails {
42 | return pageParserManager.parse(shotDetailsParser, ShotDetailsParams(shotSlug))
43 | }
44 |
45 | suspend fun getUserShots(requestParams: UserShotsParams): List {
46 | TODO()
47 | //return dribbbleApi.getUserShots(
48 | // requestParams.userName,
49 | // requestParams.page,
50 | // requestParams.pageSize
51 | //)
52 | }
53 |
54 | suspend fun search(
55 | params: SearchParams
56 | ) = Pager(PagingConfig(pageSize = 20)) {
57 | CommonPagingSource { pagingParams ->
58 | pageParserManager.parse(searchPageParser, params, pagingParams)
59 | }
60 | }.flow
61 |
62 | }
--------------------------------------------------------------------------------
/app-mvp/core-ui/src/main/java/com/bubbble/coreui/ui/adapters/LoadingStateAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.coreui.ui.adapters
2 |
3 | import android.util.Log
4 | import android.view.LayoutInflater
5 | import android.view.ViewGroup
6 | import androidx.core.view.isVisible
7 | import androidx.paging.LoadState
8 | import androidx.paging.LoadStateAdapter
9 | import androidx.recyclerview.widget.RecyclerView
10 | import com.bubbble.coreui.R
11 | import com.bubbble.coreui.databinding.ItemNetworkStateBinding
12 |
13 | class LoadingStateAdapter(
14 | private val retryListener: () -> Unit
15 | ) : LoadStateAdapter() {
16 |
17 | override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState) =
18 | LoaderViewHolder(
19 | ItemNetworkStateBinding.bind(
20 | LayoutInflater.from(parent.context)
21 | .inflate(R.layout.item_network_state, parent, false)
22 | ),
23 | retryListener
24 | )
25 |
26 | override fun onBindViewHolder(holder: LoaderViewHolder, loadState: LoadState) =
27 | holder.bind(loadState)
28 |
29 | class LoaderViewHolder(
30 | private val binding: ItemNetworkStateBinding,
31 | private val retryListener: () -> Unit
32 | ) : RecyclerView.ViewHolder(binding.root) {
33 |
34 | init {
35 | binding.retryButton.setOnClickListener { retryListener() }
36 | }
37 |
38 | fun bind(loadState: LoadState) = with(binding) {
39 | Log.d("Bubbble", """STATE: ${loadState.javaClass.simpleName}
40 | loadMoreProgress: ${loadState is LoadState.Loading}
41 | retryButton: ${loadState is LoadState.Loading}
42 | errorMsg: ${
43 | !(loadState as? LoadState.Error)?.error?.message.isNullOrBlank()
44 | }
45 | """.trimIndent())
46 | loadMoreLayout.isVisible = loadState is LoadState.Loading
47 | loadMoreProgress.isVisible = loadState is LoadState.Loading
48 | loadMoreErrorLayout.isVisible = loadState is LoadState.Error
49 | retryButton.isVisible = loadState is LoadState.Error
50 | errorMsg.isVisible =
51 | !(loadState as? LoadState.Error)?.error?.message.isNullOrBlank()
52 | errorMsg.text = (loadState as? LoadState.Error)?.error?.message
53 | }
54 | }
55 | }
--------------------------------------------------------------------------------
/tests/src/main/java/com/bubbble/tests/mockserver/request/MockBody.kt:
--------------------------------------------------------------------------------
1 | package com.bubbble.tests.mockserver.request
2 |
3 | import com.google.gson.Gson
4 | import com.google.gson.JsonElement
5 | import com.google.gson.JsonParser
6 | import okio.Buffer
7 | import com.bubbble.tests.extensions.assertEquals
8 | import com.bubbble.tests.extensions.readTextFile
9 | import java.util.*
10 |
11 | class MockBody(
12 | private val gson: Gson,
13 | private val body: Buffer
14 | ) {
15 |
16 | val asString = body.readUtf8()
17 |
18 | val asJson: JsonElement
19 | get() = JsonParser.parseString(asString)
20 |
21 | fun assertJson(jsonPath: String) {
22 | fun String.toSortedJson() : String{
23 | val map: TreeMap<*, *>? = gson.fromJson(this, TreeMap::class.java)
24 | return gson.toJson(map)
25 | }
26 |
27 | val expectedJson = readTextFile(jsonPath).toSortedJson()
28 | expectedJson.assertEquals(asString.toSortedJson())
29 | }
30 |
31 | fun assertValue(vararg path: Any, value: String) {
32 | getElement(path).asString.assertEquals(value)
33 | }
34 |
35 | fun assertValue(vararg path: Any, value: Int) {
36 | getElement(path).asInt.assertEquals(value)
37 | }
38 |
39 | fun assertValue(vararg path: Any, value: Long) {
40 | getElement(path).asLong.assertEquals(value)
41 | }
42 |
43 | fun assertValue(vararg path: Any, value: Boolean) {
44 | getElement(path).asBoolean.assertEquals(value)
45 | }
46 |
47 | private fun getElement(
48 | path: Array
49 | ): JsonElement {
50 | val rootJsonElement = asJson
51 | if (rootJsonElement.isJsonNull) {
52 | throw IllegalArgumentException("JSON is null")
53 | }
54 |
55 | var element: JsonElement = rootJsonElement
56 | (path).forEach { pathElement ->
57 | element = getElementChild(element, pathElement)
58 | }
59 | return element
60 | }
61 |
62 | private fun getElementChild(parent: JsonElement, key: Any): JsonElement {
63 | return when (key) {
64 | is String -> {
65 | parent.asJsonObject.get(key)
66 | }
67 | is Int -> {
68 | parent.asJsonArray[key]
69 | }
70 | else -> throw IllegalArgumentException("Incorrect body path item: $key")
71 | }
72 | }
73 |
74 | }
--------------------------------------------------------------------------------