├── 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 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 11 | 12 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 9 | 10 | 14 | 15 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 10 | 11 | 16 | 17 | -------------------------------------------------------------------------------- /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 | 4 | 5 | 10 | 11 | 16 | 17 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /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 | 5 | 6 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 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 |