[GSoC] Merge Leaderboard branch with master (#3905)

* [GSoC] Fixes #3789 Updated UI of achievements activity to display level in first tab & Leaderboard in the second tab (#3794)

* Updated UI of achievements activity to display level in first tab and Leaderboard in the second tab

* Removed hardcoded string

* Fixes #3861 Use the APIs to fetch leaderboard’s based on uploads via mobile app (all time) and display it in the Leaderboard screen. (#3865)

* [GSoC] Added Unit Tests and Fixed Landscape Mode Bug (#3872)

* Fixes #3861 Use the APIs to fetch leaderboard’s based on uploads via mobile app (all time) and display it in the Leaderboard screen.

* Fixed Bug - missing data in landscape mode

* Added Unit Tests for Leaderboard

* Added JavaDocs

* Updated JavaDocs

* [GSoC] Added Pagination to Leaderboard (#3881)

* Fixes #3861 Use the APIs to fetch leaderboard’s based on uploads via mobile app (all time) and display it in the Leaderboard screen.

* Fixed Bug - missing data in landscape mode

* Added Unit Tests for Leaderboard

* Added JavaDocs

* Updated JavaDocs

* Added Pagination

* Added Merge Adapter

* Fixed Test Case

* Added Smooth Scroll

* Added Progress Bar for Paging

* Fixed Gradle

* [GSoC] Added option to set a new avatar (#3892)

* Fixes #3861 Use the APIs to fetch leaderboard’s based on uploads via mobile app (all time) and display it in the Leaderboard screen.

* Added option to set a new avatar

* [GSoC] Added Click to open user profile for leaderboard (#3887)

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* #3749 Improve MediaClient UnitTests  (#3846)

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadCategoryDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - update BaseAdapter to be easier to use

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchImagesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchCategoriesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace NotificationRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadDepictsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace PlaceRenderer

* #3756 Convert SearchDepictionsFragment to use Pagination - convert SearchDepictionsFragment

* #3756 Convert SearchDepictionsFragment to use Pagination - fix presenter unit tests now that view is not nullable - fix Category prefix imports

* #3756 Convert SearchDepictionsFragment to use Pagination - test DataSource related classes

* #3756 Convert SearchDepictionsFragment to use Pagination - reset rx scheduler - ignore failing test

* #3760 Convert SearchCategoriesFragment to use Pagination - extract functionality of pagination to base classes - add category pagination

* #3772 Convert SearchImagesFragment to use Pagination  - convert SearchImagesFragment - tidy up showing the empty view - make search fragments show snackbar with appropriate text

* #3772 Convert SearchImagesFragment to use Pagination  - allow viewpager to load more data

* #3760 remove test that got re-added by merge

* #3760 remove duplicate dependency

* #3772 fix compilation

* #3780 Create media using a combination of Entities & MwQueryResult - construct media with an entity - move fields from media down to contribution - move dynamic fields outside of media - remove unused constructors - remove all unnecessary fetching of captions/descriptions - bump database version

* #3808 Construct media objects that depict an item id correctly - use generator to construct media for DepictedImages

* #3810 Convert DepictedImagesFragment to use Pagination - extract common media paging methods - convert to DepictedImages to use pagination

* #3810 Convert DepictedImagesFragment to use Pagination - rename base classes to better reflect usage

* #3810 Convert DepictedImagesFragment to use Pagination - map to empty result with no pages

* #3810 Convert DepictedImagesFragment to use Pagination - align test with returned values

* #3780 Create media using a combination of Entities & MwQueryResult - update wikicode to align with expected behaviour

* #3780 Create media using a combination of Entities & MwQueryResult - replace old site of thumbnail title with most relevant caption

* #3818 Convert SubDepictionListFragment to use Pagination - replace SubDepictionList with Child and Parent Fragments - replace contracts with simple presenter declarations - move classes to appropriate packages - delete unused network models - delete duplicated paging classes

* #3820 Convert CategoryImagesListFragment to use Pagination - replace CategoryImagesListFragment with CategoriesMediaFragment - disallow the construction of media objects without imageinfo

* #3822 Convert SubCategoryImagesListFragment to use Pagination - convert subcategories - add continuation support in category client - rely on interfaces for callbacks of PageableMediaFragments

* #3822 Convert SubCategoryImagesListFragment to use Pagination - convert parent categories - delete list fragment - creat base class to support continuation requests in clients

* #3822 Convert SubCategoryImagesListFragment to use Pagination - add tests for ParentCategoriesDataSource

* #3822 Convert SubCategoryImagesListFragment to use Pagination - remove no longer applicable test

* #3749 Improve MediaClient UnitTests - test rewrite

* #3749 Improve MediaClient UnitTests - align with buildConfig property

* With pause and resume for uploads (#3858)

* With pause and resume for uploads

* Dispose current upload

* Make pause and resume work

* Check stash validity

* With java docs

* minor

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Add nearby presenter unit tests  (#3615)

* init the test file

* Add tests to check if searchthisarea button action and checkbox actions are added after initialize test

* Add tests to locked unlocked nearby cases

* Add tests for null cases in updateMapAndList method

* Add test to check which locations are used to populate places, depending to LocationChangeType

* Add tests to test users position is not followed if blue dot (current location marker) is not visible

* Add tests to decide search this area method visibility on camera move

* Add tests for multi filteirng of placetypes

* add tests for single place type selection too

* Add tests to tests search view focus gain and bottom sheet visibilities

* Add tests for SearchCloseToCurrentLocation

* Remove two unneeded getter and setter for isNearbyLocked, use @VisibleForTesting annotation instead

* Add VisibleForTesting annotation to initializeNearbyOperations method so that it will be private by default

* Add missing tests

* Add tests for map updated case and add missing lines those are being tested

* Add some missind method verifications

* Create real latlang objects isntead of spying them

* Use a real presenter object instead of a spy

* Revert nonneeded @VisibleForTest annotations, instead reach via lockUnlock method

* Reduce code repetitions

* Do not call a test from another test method

* Add some more tests

* Fix minor issues

* Hidden categories are not showed in suggested categories (#3853)

* Localisation updates from https://translatewiki.net.

* Clarify that the caption would also be used as the image title (#3876)

The app uses the fist caption as the file title. This should also
be communicated to the user via the info box as they would not be
aware of it otherwise.

* Localisation updates from https://translatewiki.net.

* Added Click event for leaderboard, Fixed #3886

* Fixed Travis

Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net>
Co-authored-by: Seán Mac Gillicuddy <seantheappdev@gmail.com>
Co-authored-by: Vivek Maskara <maskaravivek@gmail.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
Co-authored-by: Paulina <63326136+PaulinaQuintero@users.noreply.github.com>
Co-authored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>

* [GSoC] Added Leaderboard Filters (#3902)

* Attempt to add filters

* Basic Filter Working

* Filter Improved

* Filter Completed

* Add JavaDocs

* Added Test for Update Avatar

* Decreased Margin of Filter

* [GSoC] Updated leaderboard string (#3897)

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* #3749 Improve MediaClient UnitTests  (#3846)

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadCategoryDepictionsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - update BaseAdapter to be easier to use

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchImagesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace SearchCategoriesRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace NotificationRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace UploadDepictsRenderer

* #3468 Switch from RvRenderer to AdapterDelegates - replace PlaceRenderer

* #3756 Convert SearchDepictionsFragment to use Pagination - convert SearchDepictionsFragment

* #3756 Convert SearchDepictionsFragment to use Pagination - fix presenter unit tests now that view is not nullable - fix Category prefix imports

* #3756 Convert SearchDepictionsFragment to use Pagination - test DataSource related classes

* #3756 Convert SearchDepictionsFragment to use Pagination - reset rx scheduler - ignore failing test

* #3760 Convert SearchCategoriesFragment to use Pagination - extract functionality of pagination to base classes - add category pagination

* #3772 Convert SearchImagesFragment to use Pagination  - convert SearchImagesFragment - tidy up showing the empty view - make search fragments show snackbar with appropriate text

* #3772 Convert SearchImagesFragment to use Pagination  - allow viewpager to load more data

* #3760 remove test that got re-added by merge

* #3760 remove duplicate dependency

* #3772 fix compilation

* #3780 Create media using a combination of Entities & MwQueryResult - construct media with an entity - move fields from media down to contribution - move dynamic fields outside of media - remove unused constructors - remove all unnecessary fetching of captions/descriptions - bump database version

* #3808 Construct media objects that depict an item id correctly - use generator to construct media for DepictedImages

* #3810 Convert DepictedImagesFragment to use Pagination - extract common media paging methods - convert to DepictedImages to use pagination

* #3810 Convert DepictedImagesFragment to use Pagination - rename base classes to better reflect usage

* #3810 Convert DepictedImagesFragment to use Pagination - map to empty result with no pages

* #3810 Convert DepictedImagesFragment to use Pagination - align test with returned values

* #3780 Create media using a combination of Entities & MwQueryResult - update wikicode to align with expected behaviour

* #3780 Create media using a combination of Entities & MwQueryResult - replace old site of thumbnail title with most relevant caption

* #3818 Convert SubDepictionListFragment to use Pagination - replace SubDepictionList with Child and Parent Fragments - replace contracts with simple presenter declarations - move classes to appropriate packages - delete unused network models - delete duplicated paging classes

* #3820 Convert CategoryImagesListFragment to use Pagination - replace CategoryImagesListFragment with CategoriesMediaFragment - disallow the construction of media objects without imageinfo

* #3822 Convert SubCategoryImagesListFragment to use Pagination - convert subcategories - add continuation support in category client - rely on interfaces for callbacks of PageableMediaFragments

* #3822 Convert SubCategoryImagesListFragment to use Pagination - convert parent categories - delete list fragment - creat base class to support continuation requests in clients

* #3822 Convert SubCategoryImagesListFragment to use Pagination - add tests for ParentCategoriesDataSource

* #3822 Convert SubCategoryImagesListFragment to use Pagination - remove no longer applicable test

* #3749 Improve MediaClient UnitTests - test rewrite

* #3749 Improve MediaClient UnitTests - align with buildConfig property

* With pause and resume for uploads (#3858)

* With pause and resume for uploads

* Dispose current upload

* Make pause and resume work

* Check stash validity

* With java docs

* minor

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Add nearby presenter unit tests  (#3615)

* init the test file

* Add tests to check if searchthisarea button action and checkbox actions are added after initialize test

* Add tests to locked unlocked nearby cases

* Add tests for null cases in updateMapAndList method

* Add test to check which locations are used to populate places, depending to LocationChangeType

* Add tests to test users position is not followed if blue dot (current location marker) is not visible

* Add tests to decide search this area method visibility on camera move

* Add tests for multi filteirng of placetypes

* add tests for single place type selection too

* Add tests to tests search view focus gain and bottom sheet visibilities

* Add tests for SearchCloseToCurrentLocation

* Remove two unneeded getter and setter for isNearbyLocked, use @VisibleForTesting annotation instead

* Add VisibleForTesting annotation to initializeNearbyOperations method so that it will be private by default

* Add missing tests

* Add tests for map updated case and add missing lines those are being tested

* Add some missind method verifications

* Create real latlang objects isntead of spying them

* Use a real presenter object instead of a spy

* Revert nonneeded @VisibleForTest annotations, instead reach via lockUnlock method

* Reduce code repetitions

* Do not call a test from another test method

* Add some more tests

* Fix minor issues

* Hidden categories are not showed in suggested categories (#3853)

* Localisation updates from https://translatewiki.net.

* Clarify that the caption would also be used as the image title (#3876)

The app uses the fist caption as the file title. This should also
be communicated to the user via the info box as they would not be
aware of it otherwise.

* Localisation updates from https://translatewiki.net.

* Delete NearbyMapFragment & NearbyListFragment (#3885)

* Fixes #3884
* Delete NearbyMapFragment & NearbyListFragment

* Fixed NearbyParentFragmentPresenterTest

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Add more nearby tests (#3877)

* more nearby tests added

* Add tests for Label class

* Add checkbox test javadocs

* Add javadocs for label

* Localisation updates from https://translatewiki.net.

* Updated strings.xml

Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net>
Co-authored-by: Seán Mac Gillicuddy <seantheappdev@gmail.com>
Co-authored-by: Vivek Maskara <maskaravivek@gmail.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
Co-authored-by: Paulina <63326136+PaulinaQuintero@users.noreply.github.com>
Co-authored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
Co-authored-by: Ashish Kumar <ashishkumar468@gmail.com>

* Updated Strings

* Added JavaDocs for all methods and classes

* Added JavaDocs for all methods and classes

* Added More JavaDocs

* Revert string.xml

* Revert "Revert string.xml"

This reverts commit 00019b598a.

* Added protected

* Fixed strings.xml extra changes

* Revert codeStyle change

* Fixed extra string change

Co-authored-by: Vivek Maskara <maskaravivek@gmail.com>
Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net>
Co-authored-by: Seán Mac Gillicuddy <seantheappdev@gmail.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
Co-authored-by: Paulina <63326136+PaulinaQuintero@users.noreply.github.com>
Co-authored-by: Kaartic Sivaraam <kaartic.sivaraam@gmail.com>
Co-authored-by: Ashish Kumar <ashishkumar468@gmail.com>
This commit is contained in:
Madhur Gupta 2020-08-27 19:20:58 +05:30 committed by GitHub
parent 50bcaab15d
commit eb816b4536
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 2839 additions and 641 deletions

View file

@ -21,7 +21,7 @@ dependencies {
// Utils
implementation 'in.yuvi:http.fluent:1.3'
implementation 'com.google.code.gson:gson:2.8.5'
implementation 'com.squareup.okhttp3:okhttp:4.5.0'
implementation 'com.squareup.okhttp3:okhttp:4.8.0'
implementation 'com.squareup.okio:okio:2.2.2'
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
implementation 'io.reactivex.rxjava2:rxjava:2.2.3'
@ -42,6 +42,7 @@ dependencies {
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
implementation 'com.karumi:dexter:5.0.0'
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
implementation "com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:$ADAPTER_DELEGATES_VERSION"
@ -50,6 +51,7 @@ dependencies {
testImplementation "androidx.paging:paging-common-ktx:$PAGING_VERSION"
implementation "androidx.paging:paging-rxjava2-ktx:$PAGING_VERSION"
implementation "androidx.recyclerview:recyclerview:1.2.0-alpha02"
implementation 'com.squareup.okhttp3:okhttp-ws:3.4.1'
// Logging
implementation 'ch.acra:acra-dialog:5.3.0'
@ -79,7 +81,7 @@ dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3'
testImplementation 'androidx.test:core:1.2.0'
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
testImplementation "com.squareup.okhttp3:mockwebserver:4.8.0"
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
testImplementation 'org.mockito:mockito-core:2.23.0'
@ -94,7 +96,7 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.2.0'
androidTestImplementation 'androidx.test:rules:1.2.0'
androidTestImplementation 'androidx.annotation:annotation:1.1.0'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.8.0'
androidTestUtil 'androidx.test:orchestrator:1.2.0'
// Debugging
@ -209,8 +211,8 @@ android {
configurations.all {
resolutionStrategy.force 'androidx.annotation:annotation:1.0.2'
exclude module: 'okhttp-ws'
}
flavorDimensions 'tier'
productFlavors {
prod {

View file

@ -7,10 +7,9 @@ import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
import androidx.test.espresso.intent.rule.IntentsTestRule
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.filters.MediumTest
import androidx.test.runner.AndroidJUnit4
import fr.free.nrw.commons.achievements.AchievementsActivity
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.profile.ProfileActivity
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -32,6 +31,6 @@ class AchievementsActivityTest {
onView(withId(R.id.drawer_layout)).perform(DrawerActions.open())
onView(withId(R.id.user_icon)).perform(click())
Intents.intended(hasComponent(AchievementsActivity::class.java.name))
Intents.intended(hasComponent(ProfileActivity::class.java.name))
}
}

View file

@ -137,8 +137,8 @@
/>
<activity
android:name=".achievements.AchievementsActivity"
android:label="@string/Achievements" />
android:name=".profile.ProfileActivity"
android:label="@string/Profile" />
<activity
android:name=".bookmarks.BookmarksActivity"

View file

@ -12,7 +12,7 @@ import javax.inject.Singleton;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.ViewUtilWrapper;

View file

@ -4,7 +4,6 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
@ -15,6 +14,7 @@ import fr.free.nrw.commons.explore.depictions.WikidataItemDetailsActivity;
import fr.free.nrw.commons.explore.SearchActivity;
import fr.free.nrw.commons.explore.ExploreActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.profile.ProfileActivity;
import fr.free.nrw.commons.review.ReviewActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
import fr.free.nrw.commons.upload.UploadActivity;
@ -68,7 +68,7 @@ public abstract class ActivityBuilderModule {
abstract ExploreActivity bindExploreActivity();
@ContributesAndroidInjector
abstract AchievementsActivity bindAchievementsActivity();
abstract ProfileActivity bindAchievementsActivity();
@ContributesAndroidInjector
abstract BookmarksActivity bindBookmarksActivity();

View file

@ -19,6 +19,8 @@ import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
import fr.free.nrw.commons.profile.achievements.AchievementsFragment;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
import fr.free.nrw.commons.review.ReviewImageFragment;
import fr.free.nrw.commons.settings.SettingsFragment;
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
@ -103,4 +105,10 @@ public abstract class FragmentBuilderModule {
@ContributesAndroidInjector
abstract ParentCategoriesFragment bindParentCategoriesFragment();
@ContributesAndroidInjector
abstract AchievementsFragment bindAchievementsFragment();
@ContributesAndroidInjector
abstract LeaderboardFragment bindLeaderboardFragment();
}

View file

@ -85,11 +85,13 @@ public class NetworkingModule {
public OkHttpJsonApiClient provideOkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
@Named("tools_forge") HttpUrl toolsForgeUrl,
@Named("test_tools_forge") HttpUrl testToolsForgeUrl,
@Named("default_preferences") JsonKvStore defaultKvStore,
Gson gson) {
return new OkHttpJsonApiClient(okHttpClient,
depictsClient,
toolsForgeUrl,
testToolsForgeUrl,
WIKIDATA_SPARQL_QUERY_URL,
BuildConfig.WIKIMEDIA_CAMPAIGNS_URL,
gson);
@ -124,6 +126,14 @@ public class NetworkingModule {
return HttpUrl.parse(TOOLS_FORGE_URL);
}
@Provides
@Named("test_tools_forge")
@NonNull
@SuppressWarnings("ConstantConditions")
public HttpUrl provideTestToolsForgeUrl() {
return HttpUrl.parse(TEST_TOOLS_FORGE_URL);
}
@Provides
@Singleton
@Named(NAMED_COMMONS_WIKI_SITE)

View file

@ -22,15 +22,19 @@ import butterknife.ButterKnife;
import com.google.android.material.snackbar.Snackbar;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.Bookmark;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.DownloadUtils;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.disposables.CompositeDisposable;
import java.util.Objects;
import javax.inject.Inject;
import timber.log.Timber;
@ -38,6 +42,14 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Inject BookmarkPicturesDao bookmarkDao;
@Inject
protected OkHttpJsonApiClient okHttpJsonApiClient;
@Inject
protected SessionManager sessionManager;
private static CompositeDisposable compositeDisposable = new CompositeDisposable();
@BindView(R.id.mediaDetailsPager) ViewPager pager;
private Boolean editable;
private boolean isFeaturedImage;
@ -160,6 +172,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
// Set wallpaper
setWallpaper(m);
return true;
case R.id.menu_set_as_avatar:
// Set avatar
setAvatar(m);
return true;
default:
return super.onOptionsItemSelected(item);
}
@ -178,6 +194,20 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
}
/**
* Set the media as user's leaderboard avatar
* @param media
*/
private void setAvatar(Media media) {
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
Timber.d("Media URL not present");
return;
}
ImageUtils.setAvatarFromImageUrl(getActivity(), media.getImageUrl(),
Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
okHttpJsonApiClient, compositeDisposable);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
if (!editable) { // Disable menu options for editable views

View file

@ -1,16 +1,21 @@
package fr.free.nrw.commons.mwapi;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LEADERBOARD_END_POINT;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.UPDATE_AVATAR_END_POINT;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
import fr.free.nrw.commons.achievements.FeaturedImages;
import fr.free.nrw.commons.achievements.FeedbackResponse;
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
import fr.free.nrw.commons.explore.depictions.DepictsClient;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.nearby.model.NearbyResponse;
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
import fr.free.nrw.commons.profile.achievements.FeaturedImages;
import fr.free.nrw.commons.profile.achievements.FeedbackResponse;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse;
import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse;
import fr.free.nrw.commons.upload.FileUtils;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.utils.ConfigUtils;
@ -40,6 +45,7 @@ public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient;
private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final HttpUrl wikiMediaTestToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final Gson gson;
@ -49,17 +55,105 @@ public class OkHttpJsonApiClient {
public OkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl,
HttpUrl wikiMediaTestToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.gson = gson;
}
/**
* The method will gradually calls the leaderboard API and fetches the leaderboard
* @param userName username of leaderboard user
* @param duration duration for leaderboard
* @param category category for leaderboard
* @param limit page size limit for list
* @param offset offset for the list
* @return LeaderboardResponse object
*/
@NonNull
public Observable<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ LEADERBOARD_END_POINT;
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new LeaderboardResponse();
}
Timber.d("Response for leaderboard is %s", json);
try {
return gson.fromJson(json, LeaderboardResponse.class);
} catch (Exception e) {
return new LeaderboardResponse();
}
}
return new LeaderboardResponse();
});
}
/**
* This method will update the leaderboard user avatar
* @param username username to update
* @param avatar url of the new avatar
* @return UpdateAvatarResponse object
*/
@NonNull
public Single<UpdateAvatarResponse> setAvatar(String username, String avatar) {
final String urlTemplate = wikiMediaTestToolforgeUrl
+ UPDATE_AVATAR_END_POINT;
return Single.fromCallable(() -> {
String url = String.format(Locale.ENGLISH,
urlTemplate,
username,
avatar);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", username);
urlBuilder.addQueryParameter("avatar", avatar);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
try {
return gson.fromJson(json, UpdateAvatarResponse.class);
} catch (Exception e) {
return new UpdateAvatarResponse();
}
}
return null;
});
}
@NonNull
public Single<Integer> getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
@ -145,7 +239,6 @@ public class OkHttpJsonApiClient {
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();

View file

@ -0,0 +1,84 @@
package fr.free.nrw.commons.profile;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.viewpager.widget.ViewPager;
import butterknife.BindView;
import butterknife.ButterKnife;
import com.google.android.material.tabs.TabLayout;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.profile.achievements.AchievementsFragment;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardFragment;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import java.util.ArrayList;
import java.util.List;
/**
* This activity will set two tabs, achievements and
* each tab will have their own fragments
*/
public class ProfileActivity extends NavigationBaseActivity {
private FragmentManager supportFragmentManager;
@BindView(R.id.viewPager)
ViewPager viewPager;
@BindView(R.id.tab_layout)
TabLayout tabLayout;
private ViewPagerAdapter viewPagerAdapter;
private AchievementsFragment achievementsFragment;
private LeaderboardFragment leaderboardFragment;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_profile);
ButterKnife.bind(this);
initDrawer();
setTitle(R.string.Profile);
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
viewPager.setAdapter(viewPagerAdapter);
tabLayout.setupWithViewPager(viewPager);
setTabs();
}
/**
* Creates a way to change current activity to AchievementActivity
* @param context
*/
public static void startYourself(Context context) {
Intent intent = new Intent(context, ProfileActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(intent);
}
/**
* Set the tabs for the fragments
*/
private void setTabs() {
List<Fragment> fragmentList = new ArrayList<>();
List<String> titleList = new ArrayList<>();
achievementsFragment = new AchievementsFragment();
fragmentList.add(achievementsFragment);
titleList.add(getResources().getString(R.string.achievements_tab_title).toUpperCase());
leaderboardFragment = new LeaderboardFragment();
fragmentList.add(leaderboardFragment);
titleList.add(getResources().getString(R.string.leaderboard_tab_title).toUpperCase());
viewPagerAdapter.setTabData(fragmentList, titleList);
viewPagerAdapter.notifyDataSetChanged();
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
}
}

View file

@ -0,0 +1,57 @@
package fr.free.nrw.commons.profile;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* This View Pager Adapter will set the fragments for profile activity
*/
public class ViewPagerAdapter extends FragmentPagerAdapter {
private List<Fragment> fragmentList = new ArrayList<>();
private List<String> fragmentTitleList = new ArrayList<>();
public ViewPagerAdapter(FragmentManager manager) {
super(manager);
}
/**
* This method returns the fragment of the viewpager at a particular position
* @param position
*/
@Override
public Fragment getItem(int position) {
return fragmentList.get(position);
}
/**
* This method returns the total number of fragments in the viewpager.
* @return size
*/
@Override
public int getCount() {
return fragmentList.size();
}
/**
* This method sets the fragment and title list in the viewpager
* @param fragmentList List of all fragments to be displayed in the viewpager
* @param fragmentTitleList List of all titles of the fragments
*/
public void setTabData(List<Fragment> fragmentList, List<String> fragmentTitleList) {
this.fragmentList = fragmentList;
this.fragmentTitleList = fragmentTitleList;
}
/**
* This method returns the title of the page at a particular position
* @param position
*/
@Override
public CharSequence getPageTitle(int position) {
return fragmentTitleList.get(position);
}
}

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.achievements
package fr.free.nrw.commons.profile.achievements
/**
* Represents Achievements class and stores all the parameters
@ -87,12 +87,14 @@ class Achievements {
*/
@JvmStatic
fun from(response: FeedbackResponse): Achievements {
return Achievements(response.uniqueUsedImages,
return Achievements(
response.uniqueUsedImages,
response.articlesUsingImages,
response.thanksReceived,
response.featuredImages.qualityImages
+ response.featuredImages.featuredPicturesOnWikimediaCommons, 0,
response.deletedUploads)
response.deletedUploads
)
}
}
}

View file

@ -1,61 +1,52 @@
package fr.free.nrw.commons.achievements;
package fr.free.nrw.commons.profile.achievements;
import android.accounts.Account;
import android.annotation.SuppressLint;
import android.content.Context;
import android.app.AlertDialog;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.util.DisplayMetrics;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.view.ContextThemeWrapper;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.core.content.FileProvider;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.dinuscxj.progressbar.CircleProgressBar;
import org.apache.commons.lang3.StringUtils;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;
import javax.inject.Inject;
import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.dinuscxj.progressbar.CircleProgressBar;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Objects;
import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils;
import timber.log.Timber;
/**
* activity for sharing feedback on uploaded activity
* fragment for sharing feedback on uploaded activity
*/
public class AchievementsActivity extends NavigationBaseActivity {
public class AchievementsFragment extends CommonsDaggerSupportFragment {
private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4;
private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3;
@ -64,55 +55,72 @@ public class AchievementsActivity extends NavigationBaseActivity {
@BindView(R.id.achievement_badge_image)
ImageView imageView;
@BindView(R.id.achievement_badge_text)
TextView badgeText;
@BindView(R.id.achievement_level)
TextView levelNumber;
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.thanks_received)
TextView thanksReceived;
@BindView(R.id.images_uploaded_progressbar)
CircleProgressBar imagesUploadedProgressbar;
@BindView(R.id.images_used_by_wiki_progress_bar)
CircleProgressBar imagesUsedByWikiProgressBar;
@BindView(R.id.image_reverts_progressbar)
CircleProgressBar imageRevertsProgressbar;
@BindView(R.id.image_featured)
TextView imagesFeatured;
@BindView(R.id.images_revert_limit_text)
TextView imagesRevertLimitText;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.layout_image_uploaded)
RelativeLayout layoutImageUploaded;
@BindView(R.id.layout_image_reverts)
RelativeLayout layoutImageReverts;
@BindView(R.id.layout_image_used_by_wiki)
RelativeLayout layoutImageUsedByWiki;
@BindView(R.id.layout_statistics)
LinearLayout layoutStatistics;
@BindView(R.id.images_used_by_wiki_text)
TextView imageByWikiText;
@BindView(R.id.images_reverted_text)
TextView imageRevertedText;
@BindView(R.id.images_upload_text_param)
TextView imageUploadedText;
@BindView(R.id.wikidata_edits)
TextView wikidataEditsText;
@Inject
SessionManager sessionManager;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
MenuItem item;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
// To keep track of the number of wiki edits made by a user
private int numberOfEdits = 0;
// menu item for action bar
private MenuItem item;
/**
* This method helps in the creation Achievement screen and
* dynamically set the size of imageView
@ -120,15 +128,13 @@ public class AchievementsActivity extends NavigationBaseActivity {
* @param savedInstanceState Data bundle
*/
@Override
@SuppressLint("StringFormatInvalid")
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_achievements);
ButterKnife.bind(this);
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_achievements, container, false);
ButterKnife.bind(this, rootView);
// DisplayMetrics used to fetch the size of the screen
DisplayMetrics displayMetrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics);
int height = displayMetrics.heightPixels;
int width = displayMetrics.widthPixels;
@ -139,37 +145,23 @@ public class AchievementsActivity extends NavigationBaseActivity {
params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO);
imageView.requestLayout();
setSupportActionBar(toolbar);
progressBar.setVisibility(View.VISIBLE);
setHasOptionsMenu(true);
hideLayouts();
setWikidataEditCount();
setAchievements();
initDrawer();
return rootView;
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
}
/**
* To invoke the AlertDialog on clicking info button
*/
@OnClick(R.id.achievement_info)
public void showInfoDialog(){
launchAlert(getResources().getString(R.string.Achievements)
,getResources().getString(R.string.achievements_info_message));
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
public void onCreateOptionsMenu(Menu menu, MenuInflater menuInflater) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_about, menu);
super.onCreateOptionsMenu(menu, menuInflater);
menuInflater.inflate(R.menu.menu_about, menu);
item = menu.getItem(0);
item.setVisible(false);
return true;
}
/**
@ -180,7 +172,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
int id = item.getItemId();
// take screenshot in form of bitmap and show it in Alert Dialog
if (id == R.id.share_app_icon) {
View rootView = getWindow().getDecorView().findViewById(android.R.id.content);
View rootView = getActivity().getWindow().getDecorView().findViewById(android.R.id.content);
Bitmap screenShot = Utils.getScreenShot(rootView);
showAlert(screenShot);
}
@ -188,20 +180,39 @@ public class AchievementsActivity extends NavigationBaseActivity {
return super.onOptionsItemSelected(item);
}
/**
* It displays the alertDialog with Image of screenshot
* @param screenshot
*/
public void showAlert(Bitmap screenshot){
AlertDialog.Builder alertadd = new AlertDialog.Builder(getActivity());
LayoutInflater factory = LayoutInflater.from(getActivity());
final View view = factory.inflate(R.layout.image_alert_layout, null);
ImageView screenShotImage = view.findViewById(R.id.alert_image);
screenShotImage.setImageBitmap(screenshot);
TextView shareMessage = view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.achievements_share_message);
alertadd.setView(view);
alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
alertadd.show();
}
/**
* To take bitmap and store it temporary storage and share it
* @param bitmap
*/
void shareScreen(Bitmap bitmap) {
try {
File file = new File(this.getExternalCacheDir(), "screen.png");
File file = new File(getActivity().getExternalCacheDir(), "screen.png");
FileOutputStream fOut = new FileOutputStream(file);
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut);
fOut.flush();
fOut.close();
file.setReadable(true, false);
Uri fileUri = FileProvider.getUriForFile(getApplicationContext(), getPackageName()+".provider", file);
grantUriPermission(getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
Uri fileUri = FileProvider
.getUriForFile(getActivity().getApplicationContext(), getActivity().getPackageName()+".provider", file);
getActivity().grantUriPermission(getActivity().getPackageName(), fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION);
final Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.putExtra(Intent.EXTRA_STREAM, fileUri);
@ -212,6 +223,15 @@ public class AchievementsActivity extends NavigationBaseActivity {
}
}
/**
* To invoke the AlertDialog on clicking info button
*/
@OnClick(R.id.achievement_info)
public void showInfoDialog(){
launchAlert(getResources().getString(R.string.Achievements)
,getResources().getString(R.string.achievements_info_message));
}
/**
* To call the API to get results in form Single<JSONObject>
* which then calls parseJson when results are fetched
@ -264,7 +284,6 @@ public class AchievementsActivity extends NavigationBaseActivity {
* To call the API to fetch the count of wiki data edits
* in the form of JavaRx Single object<JSONobject>
*/
@SuppressLint("CheckResult")
private void setWikidataEditCount() {
String userName = sessionManager.getUserName();
if (StringUtils.isBlank(userName)) {
@ -292,11 +311,11 @@ public class AchievementsActivity extends NavigationBaseActivity {
private void showSnackBarWithRetry(boolean tooManyAchievements) {
if (tooManyAchievements) {
progressBar.setVisibility(View.GONE);
ViewUtil.showDismissibleSnackBar(findViewById(android.R.id.content),
ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements());
} else {
progressBar.setVisibility(View.GONE);
ViewUtil.showDismissibleSnackBar(findViewById(android.R.id.content),
ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content),
R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements());
}
}
@ -305,7 +324,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* Shows a generic error toast when error occurs while loading achievements or uploads
*/
private void onError() {
ViewUtil.showLongToast(this, getResources().getString(R.string.error_occurred));
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred));
progressBar.setVisibility(View.GONE);
}
@ -355,7 +374,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
}
private void setZeroAchievements() {
AlertDialog.Builder builder=new AlertDialog.Builder(this)
AlertDialog.Builder builder=new AlertDialog.Builder(getActivity())
.setMessage(getString(R.string.no_achievements_yet))
.setPositiveButton(getString(R.string.ok), (dialog, which) -> {
});
@ -399,20 +418,10 @@ public class AchievementsActivity extends NavigationBaseActivity {
levelUpInfoString += " " + levelInfo.getLevelNumber();
levelNumber.setText(levelUpInfoString);
imageView.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge,
new ContextThemeWrapper(this, levelInfo.getLevelStyle()).getTheme()));
new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme()));
badgeText.setText(Integer.toString(levelInfo.getLevelNumber()));
}
/**
* Creates a way to change current activity to AchievementActivity
* @param context
*/
public static void startYourself(Context context) {
Intent intent = new Intent(context, AchievementsActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
context.startActivity(intent);
}
/**
* to hide progressbar
*/
@ -447,24 +456,6 @@ public class AchievementsActivity extends NavigationBaseActivity {
levelNumber.setVisibility(View.INVISIBLE);
}
/**
* It display the alertDialog with Image of screenshot
* @param screenshot
*/
public void showAlert(Bitmap screenshot){
AlertDialog.Builder alertadd = new AlertDialog.Builder(AchievementsActivity.this);
LayoutInflater factory = LayoutInflater.from(AchievementsActivity.this);
final View view = factory.inflate(R.layout.image_alert_layout, null);
ImageView screenShotImage = view.findViewById(R.id.alert_image);
screenShotImage.setImageBitmap(screenshot);
TextView shareMessage = view.findViewById(R.id.alert_text);
shareMessage.setText(R.string.achievements_share_message);
alertadd.setView(view);
alertadd.setPositiveButton(R.string.about_translate_proceed, (dialog, which) -> shareScreen(screenshot));
alertadd.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.cancel());
alertadd.show();
}
@OnClick(R.id.images_upload_info)
public void showUploadInfo(){
launchAlert(getResources().getString(R.string.images_uploaded)
@ -507,7 +498,7 @@ public class AchievementsActivity extends NavigationBaseActivity {
* @param message
*/
private void launchAlert(String title, String message){
new AlertDialog.Builder(AchievementsActivity.this)
new AlertDialog.Builder(getActivity())
.setTitle(title)
.setMessage(message)
.setCancelable(true)
@ -524,8 +515,8 @@ public class AchievementsActivity extends NavigationBaseActivity {
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(this);
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(getActivity());
return false;
}
return true;

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.achievements
package fr.free.nrw.commons.profile.achievements
import com.google.gson.annotations.SerializedName

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.achievements
package fr.free.nrw.commons.profile.achievements
/**
* Represent the Feedback Response of the user

View file

@ -1,4 +1,4 @@
package fr.free.nrw.commons.achievements
package fr.free.nrw.commons.profile.achievements
import fr.free.nrw.commons.R

View file

@ -0,0 +1,125 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import androidx.paging.PageKeyedDataSource;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
import java.util.Objects;
import timber.log.Timber;
/**
* This class will call the leaderboard API to get new list when the pagination is performed
*/
public class DataSourceClass extends PageKeyedDataSource<Integer, LeaderboardList> {
private OkHttpJsonApiClient okHttpJsonApiClient;
private SessionManager sessionManager;
private MutableLiveData<String> progressLiveStatus;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private String duration;
private String category;
private int limit;
private int offset;
/**
* Initialise the Data Source Class with API params
* @param okHttpJsonApiClient
* @param sessionManager
* @param duration
* @param category
* @param limit
* @param offset
*/
public DataSourceClass(OkHttpJsonApiClient okHttpJsonApiClient,SessionManager sessionManager,
String duration, String category, int limit, int offset) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.sessionManager = sessionManager;
this.duration = duration;
this.category = category;
this.limit = limit;
this.offset = offset;
progressLiveStatus = new MutableLiveData<>();
}
/**
* @return the status of the list
*/
public MutableLiveData<String> getProgressLiveStatus() {
return progressLiveStatus;
}
/**
* Loads the initial set of data from API
* @param params
* @param callback
*/
@Override
public void loadInitial(@NonNull LoadInitialParams<Integer> params,
@NonNull LoadInitialCallback<Integer, LeaderboardList> callback) {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
duration, category, String.valueOf(limit), String.valueOf(offset))
.doOnSubscribe(disposable -> {
compositeDisposable.add(disposable);
progressLiveStatus.postValue(LOADING);
}).subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
progressLiveStatus.postValue(LOADED);
callback.onResult(response.getLeaderboardList(), null, response.getLimit());
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
progressLiveStatus.postValue(LOADING);
}
));
}
/**
* Loads any data before the inital page is loaded
* @param params
* @param callback
*/
@Override
public void loadBefore(@NonNull LoadParams<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> callback) {
}
/**
* Loads the next set of data on scrolling with offset as the limit of the last set of data
* @param params
* @param callback
*/
@Override
public void loadAfter(@NonNull LoadParams<Integer> params,
@NonNull LoadCallback<Integer, LeaderboardList> callback) {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
duration, category, String.valueOf(limit), String.valueOf(params.key))
.doOnSubscribe(disposable -> {
compositeDisposable.add(disposable);
progressLiveStatus.postValue(LOADING);
}).subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
progressLiveStatus.postValue(LOADED);
callback.onResult(response.getLeaderboardList(), params.key + limit);
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
progressLiveStatus.postValue(LOADING);
}
));
}
}

View file

@ -0,0 +1,110 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.lifecycle.MutableLiveData;
import androidx.paging.DataSource;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
/**
* This class will create a new instance of the data source class on pagination
*/
public class DataSourceFactory extends DataSource.Factory<Integer, LeaderboardList> {
private MutableLiveData<DataSourceClass> liveData;
private OkHttpJsonApiClient okHttpJsonApiClient;
private CompositeDisposable compositeDisposable;
private SessionManager sessionManager;
private String duration;
private String category;
private int limit;
private int offset;
/**
* Gets the current set leaderboard list duration
*/
public String getDuration() {
return duration;
}
/**
* Sets the current set leaderboard duration with the new duration
*/
public void setDuration(final String duration) {
this.duration = duration;
}
/**
* Gets the current set leaderboard list category
*/
public String getCategory() {
return category;
}
/**
* Sets the current set leaderboard category with the new category
*/
public void setCategory(final String category) {
this.category = category;
}
/**
* Gets the current set leaderboard list limit
*/
public int getLimit() {
return limit;
}
/**
* Sets the current set leaderboard limit with the new limit
*/
public void setLimit(final int limit) {
this.limit = limit;
}
/**
* Gets the current set leaderboard list offset
*/
public int getOffset() {
return offset;
}
/**
* Sets the current set leaderboard offset with the new offset
*/
public void setOffset(final int offset) {
this.offset = offset;
}
/**
* Constructor for DataSourceFactory class
* @param okHttpJsonApiClient client for OKhttp
* @param compositeDisposable composite disposable
* @param sessionManager sessionManager
*/
public DataSourceFactory(OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable,
SessionManager sessionManager) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.compositeDisposable = compositeDisposable;
this.sessionManager = sessionManager;
liveData = new MutableLiveData<>();
}
/**
* @return the live data
*/
public MutableLiveData<DataSourceClass> getMutableLiveData() {
return liveData;
}
/**
* Creates the new instance of data source class
* @return
*/
@Override
public DataSource<Integer, LeaderboardList> create() {
DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager, duration, category, limit, offset);
liveData.postValue(dataSourceClass);
return dataSourceClass;
}
}

View file

@ -0,0 +1,45 @@
package fr.free.nrw.commons.profile.leaderboard;
/**
* This class contains the constant variables for leaderboard
*/
public class LeaderboardConstants {
/**
* This is the size of the page i.e. number items to load in a batch when pagination is performed
*/
public static final int PAGE_SIZE = 10;
/**
* This is the starting offset, we set it to 0 to start loading from rank 1
*/
public static final int START_OFFSET = 0;
/**
* This is the prefix of the user's homepage url, appending the username will give us complete url
*/
public static final String USER_LINK_PREFIX = "https://commons.wikimedia.org/wiki/User:";
/**
* This is the a constant string for the state loading, when the pages are getting loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
public final static String LOADING = "Loading";
/**
* This is the a constant string for the state loaded, when the pages are loaded we can
* use this constant to identify if we need to show the progress bar or not
*/
public final static String LOADED = "Loaded";
/**
* This API endpoint is to update the leaderboard avatar
*/
public final static String UPDATE_AVATAR_END_POINT = "/update_avatar.py";
/**
* This API endpoint is to get leaderboard data
*/
public final static String LEADERBOARD_END_POINT = "/leaderboard.py";
}

View file

@ -0,0 +1,277 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADED;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.LOADING;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.START_OFFSET;
import android.accounts.Account;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar;
import android.widget.Spinner;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.MergeAdapter;
import androidx.recyclerview.widget.RecyclerView;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.Objects;
import javax.inject.Inject;
import timber.log.Timber;
/**
* This class extends the CommonsDaggerSupportFragment and creates leaderboard fragment
*/
public class LeaderboardFragment extends CommonsDaggerSupportFragment {
@BindView(R.id.leaderboard_list)
RecyclerView leaderboardListRecyclerView;
@BindView(R.id.progressBar)
ProgressBar progressBar;
@BindView(R.id.category_spinner)
Spinner categorySpinner;
@BindView(R.id.duration_spinner)
Spinner durationSpinner;
@Inject
SessionManager sessionManager;
@Inject
OkHttpJsonApiClient okHttpJsonApiClient;
@Inject
ViewModelFactory viewModelFactory;
/**
* View model for the paged leaderboard list
*/
private LeaderboardListViewModel viewModel;
/**
* Composite disposable for API call
*/
private CompositeDisposable compositeDisposable = new CompositeDisposable();
/**
* Duration of the leaderboard API
*/
private String duration;
/**
* Category of the Leaderboard API
*/
private String category;
/**
* Page size of the leaderboard API
*/
private int limit = PAGE_SIZE;
/**
* offset for the leaderboard API
*/
private int offset = START_OFFSET;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_leaderboard, container, false);
ButterKnife.bind(this, rootView);
progressBar.setVisibility(View.VISIBLE);
hideLayouts();
setSpinners();
/**
* This array is for the duration filter, we have three filters weekly, yearly and all-time
* each filter have a key and value pair, the value represents the param of the API
*/
String[] durationValues = getContext().getResources().getStringArray(R.array.leaderboard_duration_values);
/**
* This array is for the category filter, we have three filters upload, used and nearby
* each filter have a key and value pair, the value represents the param of the API
*/
String[] categoryValues = getContext().getResources().getStringArray(R.array.leaderboard_category_values);
duration = durationValues[0];
category = categoryValues[0];
setLeaderboard(duration, category, limit, offset);
durationSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
duration = durationValues[durationSpinner.getSelectedItemPosition()];
refreshLeaderboard();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
categorySpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
category = categoryValues[categorySpinner.getSelectedItemPosition()];
refreshLeaderboard();
}
@Override
public void onNothingSelected(AdapterView<?> adapterView) {
}
});
return rootView;
}
/**
* Refreshes the leaderboard list
*/
private void refreshLeaderboard() {
if (viewModel != null) {
viewModel.refresh(duration, category, limit, offset);
setLeaderboard(duration, category, limit, offset);
}
}
/**
* Set the spinners for the leaderboard filters
*/
private void setSpinners() {
ArrayAdapter<CharSequence> categoryAdapter = ArrayAdapter.createFromResource(getContext(),
R.array.leaderboard_categories, android.R.layout.simple_spinner_item);
categoryAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
categorySpinner.setAdapter(categoryAdapter);
ArrayAdapter<CharSequence> durationAdapter = ArrayAdapter.createFromResource(getContext(),
R.array.leaderboard_durations, android.R.layout.simple_spinner_item);
durationAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
durationSpinner.setAdapter(durationAdapter);
}
/**
* To call the API to get results
* which then sets the views using setLeaderboardUser method
*/
private void setLeaderboard(String duration, String category, int limit, int offset) {
if (checkAccount()) {
try {
compositeDisposable.add(okHttpJsonApiClient
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
duration, category, null, null)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
if (response != null && response.getStatus() == 200) {
setViews(response, duration, category, limit, offset);
}
},
t -> {
Timber.e(t, "Fetching leaderboard statistics failed");
onError();
}
));
}
catch (Exception e){
Timber.d(e+"success");
}
}
}
/**
* Set the views
* @param response Leaderboard Response Object
*/
private void setViews(LeaderboardResponse response, String duration, String category, int limit, int offset) {
viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class);
viewModel.setParams(duration, category, limit, offset);
LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter();
UserDetailAdapter userDetailAdapter= new UserDetailAdapter(response);
MergeAdapter mergeAdapter = new MergeAdapter(userDetailAdapter, leaderboardListAdapter);
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(getContext());
leaderboardListRecyclerView.setLayoutManager(linearLayoutManager);
leaderboardListRecyclerView.setAdapter(mergeAdapter);
viewModel.getListLiveData().observe(getViewLifecycleOwner(), leaderboardListAdapter::submitList);
viewModel.getProgressLoadStatus().observe(getViewLifecycleOwner(), status -> {
if (Objects.requireNonNull(status).equalsIgnoreCase(LOADING)) {
showProgressBar();
} else if (status.equalsIgnoreCase(LOADED)) {
hideProgressBar();
}
});
}
/**
* to hide progressbar
*/
private void hideProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
categorySpinner.setVisibility(View.VISIBLE);
durationSpinner.setVisibility(View.VISIBLE);
leaderboardListRecyclerView.setVisibility(View.VISIBLE);
}
}
/**
* to show progressbar
*/
private void showProgressBar() {
if (progressBar != null) {
progressBar.setVisibility(View.VISIBLE);
}
}
/**
* used to hide the layouts while fetching results from api
*/
private void hideLayouts(){
categorySpinner.setVisibility(View.INVISIBLE);
durationSpinner.setVisibility(View.INVISIBLE);
leaderboardListRecyclerView.setVisibility(View.INVISIBLE);
}
/**
* check to ensure that user is logged in
* @return
*/
private boolean checkAccount(){
Account currentAccount = sessionManager.getCurrentAccount();
if (currentAccount == null) {
Timber.d("Current account is null");
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.user_not_logged_in));
sessionManager.forceLogin(getActivity());
return false;
}
return true;
}
/**
* Shows a generic error toast when error occurs while loading leaderboard
*/
private void onError() {
ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred));
progressBar.setVisibility(View.GONE);
}
}

View file

@ -0,0 +1,137 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.DiffUtil;
import androidx.recyclerview.widget.DiffUtil.ItemCallback;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* This class represents the leaderboard API response sub part of i.e. leaderboard list
* The leaderboard list will contain the ranking of the users from 1 to n,
* avatars, username and count in the selected category.
*/
public class LeaderboardList {
/**
* Username of the user
* Example value - Syced
*/
@SerializedName("username")
@Expose
private String username;
/**
* Count in the category
* Example value - 10
*/
@SerializedName("category_count")
@Expose
private Integer categoryCount;
/**
* URL of the avatar of user
* Example value = https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png
*/
@SerializedName("avatar")
@Expose
private String avatar;
/**
* Rank of the user
* Example value - 1
*/
@SerializedName("rank")
@Expose
private Integer rank;
/**
* @return the username of the user in the leaderboard list
*/
public String getUsername() {
return username;
}
/**
* Sets the username of the user in the leaderboard list
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the category count of the user in the leaderboard list
*/
public Integer getCategoryCount() {
return categoryCount;
}
/**
* Sets the category count of the user in the leaderboard list
*/
public void setCategoryCount(Integer categoryCount) {
this.categoryCount = categoryCount;
}
/**
* @return the avatar of the user in the leaderboard list
*/
public String getAvatar() {
return avatar;
}
/**
* Sets the avatar of the user in the leaderboard list
*/
public void setAvatar(String avatar) {
this.avatar = avatar;
}
/**
* @return the rank of the user in the leaderboard list
*/
public Integer getRank() {
return rank;
}
/**
* Sets the rank of the user in the leaderboard list
*/
public void setRank(Integer rank) {
this.rank = rank;
}
/**
* This method checks for the diff in the callbacks for paged lists
*/
public static DiffUtil.ItemCallback<LeaderboardList> DIFF_CALLBACK =
new ItemCallback<LeaderboardList>() {
@Override
public boolean areItemsTheSame(@NonNull LeaderboardList oldItem,
@NonNull LeaderboardList newItem) {
return newItem == oldItem;
}
@Override
public boolean areContentsTheSame(@NonNull LeaderboardList oldItem,
@NonNull LeaderboardList newItem) {
return newItem.getRank().equals(oldItem.getRank());
}
};
/**
* Returns true if two objects are equal, false otherwise
* @param obj
* @return
*/
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
LeaderboardList leaderboardList = (LeaderboardList) obj;
return leaderboardList.getRank().equals(this.getRank());
}
}

View file

@ -0,0 +1,89 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.USER_LINK_PREFIX;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.paging.PagedListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
/**
* This class extends RecyclerView.Adapter and creates the List section of the leaderboard
*/
public class LeaderboardListAdapter extends PagedListAdapter<LeaderboardList, LeaderboardListAdapter.ListViewHolder> {
protected LeaderboardListAdapter() {
super(LeaderboardList.DIFF_CALLBACK);
}
public class ListViewHolder extends RecyclerView.ViewHolder {
TextView rank;
SimpleDraweeView avatar;
TextView username;
TextView count;
public ListViewHolder(View itemView) {
super(itemView);
this.rank = itemView.findViewById(R.id.user_rank);
this.avatar = itemView.findViewById(R.id.user_avatar);
this.username = itemView.findViewById(R.id.user_name);
this.count = itemView.findViewById(R.id.user_count);
}
/**
* This method will return the Context
* @return Context
*/
public Context getContext() {
return itemView.getContext();
}
}
/**
* Overrides the onCreateViewHolder and inflates the recyclerview list item layout
* @param parent
* @param viewType
* @return
*/
@NonNull
@Override
public LeaderboardListAdapter.ListViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.leaderboard_list_element, parent, false);
return new ListViewHolder(view);
}
/**
* Overrides the onBindViewHolder Set the view at the specific position with the specific value
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull LeaderboardListAdapter.ListViewHolder holder, int position) {
TextView rank = holder.rank;
SimpleDraweeView avatar = holder.avatar;
TextView username = holder.username;
TextView count = holder.count;
rank.setText(getItem(position).getRank().toString());
avatar.setImageURI(Uri.parse(getItem(position).getAvatar()));
username.setText(getItem(position).getUsername());
count.setText(getItem(position).getCategoryCount().toString());
/*
Open the user profile in a webview when a username is clicked on leaderboard
*/
holder.itemView.setOnClickListener(view -> Utils.handleWebUrl(holder.getContext(), Uri.parse(
String.format("%s%s", USER_LINK_PREFIX, getItem(position).getUsername()))));
}
}

View file

@ -0,0 +1,107 @@
package fr.free.nrw.commons.profile.leaderboard;
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.PAGE_SIZE;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.paging.LivePagedListBuilder;
import androidx.paging.PagedList;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.disposables.CompositeDisposable;
/**
* Extends the ViewModel class and creates the LeaderboardList View Model
*/
public class LeaderboardListViewModel extends ViewModel {
private DataSourceFactory dataSourceFactory;
private LiveData<PagedList<LeaderboardList>> listLiveData;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private LiveData<String> progressLoadStatus = new MutableLiveData<>();
/**
* Constructor for a new LeaderboardListViewModel
* @param okHttpJsonApiClient
* @param sessionManager
*/
public LeaderboardListViewModel(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager
sessionManager) {
dataSourceFactory = new DataSourceFactory(okHttpJsonApiClient,
compositeDisposable, sessionManager);
initializePaging();
}
/**
* Initialises the paging
*/
private void initializePaging() {
PagedList.Config pagedListConfig =
new PagedList.Config.Builder()
.setEnablePlaceholders(false)
.setInitialLoadSizeHint(PAGE_SIZE)
.setPageSize(PAGE_SIZE).build();
listLiveData = new LivePagedListBuilder<>(dataSourceFactory, pagedListConfig)
.build();
progressLoadStatus = Transformations
.switchMap(dataSourceFactory.getMutableLiveData(), DataSourceClass::getProgressLiveStatus);
}
/**
* Refreshes the paged list with the new params and starts the loading of new data
* @param duration
* @param category
* @param limit
* @param offset
*/
public void refresh(String duration, String category, int limit, int offset) {
dataSourceFactory.setDuration(duration);
dataSourceFactory.setCategory(category);
dataSourceFactory.setLimit(limit);
dataSourceFactory.setOffset(offset);
dataSourceFactory.getMutableLiveData().getValue().invalidate();
}
/**
* Sets the new params for the paged list API calls
* @param duration
* @param category
* @param limit
* @param offset
*/
public void setParams(String duration, String category, int limit, int offset) {
dataSourceFactory.setDuration(duration);
dataSourceFactory.setCategory(category);
dataSourceFactory.setLimit(limit);
dataSourceFactory.setOffset(offset);
}
/**
* @return the loading status of paged list
*/
public LiveData<String> getProgressLoadStatus() {
return progressLoadStatus;
}
/**
* @return the paged list with live data
*/
public LiveData<PagedList<LeaderboardList>> getListLiveData() {
return listLiveData;
}
@Override
protected void onCleared() {
super.onCleared();
compositeDisposable.clear();
}
}

View file

@ -0,0 +1,237 @@
package fr.free.nrw.commons.profile.leaderboard;
import java.util.List;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* GSON Response Class for Leaderboard API response
*/
public class LeaderboardResponse {
/**
* Status Code returned from the API
* Example value - 200
*/
@SerializedName("status")
@Expose
private Integer status;
/**
* Username returned from the API
* Example value - Syced
*/
@SerializedName("username")
@Expose
private String username;
/**
* Category count returned from the API
* Example value - 10
*/
@SerializedName("category_count")
@Expose
private Integer categoryCount;
/**
* Limit returned from the API
* Example value - 10
*/
@SerializedName("limit")
@Expose
private int limit;
/**
* Avatar returned from the API
* Example value - https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png
*/
@SerializedName("avatar")
@Expose
private String avatar;
/**
* Offset returned from the API
* Example value - 0
*/
@SerializedName("offset")
@Expose
private int offset;
/**
* Duration returned from the API
* Example value - yearly
*/
@SerializedName("duration")
@Expose
private String duration;
/**
* Leaderboard list returned from the API
* Example value - [{
* "username": "",
* "category_count": 107147,
* "avatar": "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/Gnome-stock_person.svg/200px-Gnome-stock_person.svg.png",
* "rank": 1
* }]
*/
@SerializedName("leaderboard_list")
@Expose
private List<LeaderboardList> leaderboardList = null;
/**
* Category returned from the API
* Example value - upload
*/
@SerializedName("category")
@Expose
private String category;
/**
* Rank returned from the API
* Example value - 1
*/
@SerializedName("rank")
@Expose
private Integer rank;
/**
* @return the status code
*/
public Integer getStatus() {
return status;
}
/**
* Sets the status code
*/
public void setStatus(Integer status) {
this.status = status;
}
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* Sets the username
*/
public void setUsername(String username) {
this.username = username;
}
/**
* @return the category count
*/
public Integer getCategoryCount() {
return categoryCount;
}
/**
* Sets the category count
*/
public void setCategoryCount(Integer categoryCount) {
this.categoryCount = categoryCount;
}
/**
* @return the limit
*/
public int getLimit() {
return limit;
}
/**
* Sets the limit
*/
public void setLimit(int limit) {
this.limit = limit;
}
/**
* @return the avatar
*/
public String getAvatar() {
return avatar;
}
/**
* Sets the avatar
*/
public void setAvatar(String avatar) {
this.avatar = avatar;
}
/**
* @return the offset
*/
public int getOffset() {
return offset;
}
/**
* Sets the offset
*/
public void setOffset(int offset) {
this.offset = offset;
}
/**
* @return the duration
*/
public String getDuration() {
return duration;
}
/**
* Sets the duration
*/
public void setDuration(String duration) {
this.duration = duration;
}
/**
* @return the leaderboard list
*/
public List<LeaderboardList> getLeaderboardList() {
return leaderboardList;
}
/**
* Sets the leaderboard list
*/
public void setLeaderboardList(List<LeaderboardList> leaderboardList) {
this.leaderboardList = leaderboardList;
}
/**
* @return the category
*/
public String getCategory() {
return category;
}
/**
* Sets the category
*/
public void setCategory(String category) {
this.category = category;
}
/**
* @return the rank
*/
public Integer getRank() {
return rank;
}
/**
* Sets the rank
*/
public void setRank(Integer rank) {
this.rank = rank;
}
}

View file

@ -0,0 +1,77 @@
package fr.free.nrw.commons.profile.leaderboard;
import com.google.gson.annotations.Expose;
import com.google.gson.annotations.SerializedName;
/**
* GSON Response Class for Update Avatar API response
*/
public class UpdateAvatarResponse {
/**
* Status Code returned from the API
* Example value - 200
*/
@SerializedName("status")
@Expose
private String status;
/**
* Message returned from the API
* Example value - Avatar Updated
*/
@SerializedName("message")
@Expose
private String message;
/**
* Username returned from the API
* Example value - Syced
*/
@SerializedName("user")
@Expose
private String user;
/**
* @return the status code
*/
public String getStatus() {
return status;
}
/**
* Sets the status code
*/
public void setStatus(String status) {
this.status = status;
}
/**
* @return the message
*/
public String getMessage() {
return message;
}
/**
* Sets the message
*/
public void setMessage(String message) {
this.message = message;
}
/**
* @return the username
*/
public String getUser() {
return user;
}
/**
* Sets the username
*/
public void setUser(String user) {
this.user = user;
}
}

View file

@ -0,0 +1,93 @@
package fr.free.nrw.commons.profile.leaderboard;
import android.content.Context;
import android.net.Uri;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import com.facebook.drawee.view.SimpleDraweeView;
import fr.free.nrw.commons.R;
/**
* This class extends RecyclerView.Adapter and creates the UserDetail section of the leaderboard
*/
public class UserDetailAdapter extends RecyclerView.Adapter<UserDetailAdapter.DataViewHolder> {
private LeaderboardResponse leaderboardResponse;
public UserDetailAdapter(LeaderboardResponse leaderboardResponse) {
this.leaderboardResponse = leaderboardResponse;
}
public class DataViewHolder extends RecyclerView.ViewHolder {
private TextView rank;
private SimpleDraweeView avatar;
private TextView username;
private TextView count;
public DataViewHolder(@NonNull View itemView) {
super(itemView);
this.rank = itemView.findViewById(R.id.rank);
this.avatar = itemView.findViewById(R.id.avatar);
this.username = itemView.findViewById(R.id.username);
this.count = itemView.findViewById(R.id.count);
}
/**
* This method will return the Context
* @return Context
*/
public Context getContext() {
return itemView.getContext();
}
}
/**
* Overrides the onCreateViewHolder and sets the view with leaderboard user element layout
* @param parent
* @param viewType
* @return
*/
@NonNull
@Override
public UserDetailAdapter.DataViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
int viewType) {
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.leaderboard_user_element, parent, false);
return new DataViewHolder(view);
}
/**
* Overrides the onBindViewHolder Set the view at the specific position with the specific value
* @param holder
* @param position
*/
@Override
public void onBindViewHolder(@NonNull UserDetailAdapter.DataViewHolder holder, int position) {
TextView rank = holder.rank;
SimpleDraweeView avatar = holder.avatar;
TextView username = holder.username;
TextView count = holder.count;
rank.setText(String.format("%s %d",
holder.getContext().getResources().getString(R.string.rank_prefix),
leaderboardResponse.getRank()));
avatar.setImageURI(
Uri.parse(leaderboardResponse.getAvatar()));
username.setText(leaderboardResponse.getUsername());
count.setText(String.format("%s %d",
holder.getContext().getResources().getString(R.string.count_prefix),
leaderboardResponse.getCategoryCount()));
}
@Override
public int getItemCount() {
return 1;
}
}

View file

@ -0,0 +1,41 @@
package fr.free.nrw.commons.profile.leaderboard;
import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import javax.inject.Inject;
/**
* This class extends the ViewModelProvider.Factory and creates a ViewModelFactory class
* for leaderboardListViewModel
*/
public class ViewModelFactory implements ViewModelProvider.Factory {
private OkHttpJsonApiClient okHttpJsonApiClient;
private SessionManager sessionManager;
@Inject
public ViewModelFactory(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) {
this.okHttpJsonApiClient = okHttpJsonApiClient;
this.sessionManager = sessionManager;
}
/**
* Creats a new LeaderboardListViewModel
* @param modelClass
* @param <T>
* @return
*/
@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(LeaderboardListViewModel.class)) {
return (T) new LeaderboardListViewModel(okHttpJsonApiClient, sessionManager);
}
throw new IllegalArgumentException("Unknown class name");
}
}

View file

@ -26,6 +26,7 @@ import androidx.drawerlayout.widget.DrawerLayout;
import com.google.android.material.navigation.NavigationView;
import fr.free.nrw.commons.profile.ProfileActivity;
import org.wikipedia.dataclient.Service;
import javax.inject.Inject;
@ -37,7 +38,6 @@ import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.achievements.AchievementsActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.LogoutClient;
import fr.free.nrw.commons.bookmarks.BookmarksActivity;
@ -140,7 +140,7 @@ public abstract class NavigationBaseActivity extends BaseActivity
LinearLayout userIcon = navHeaderView.findViewById(R.id.user_details);
userIcon.setOnClickListener(v -> {
drawerLayout.closeDrawer(navigationView);
AchievementsActivity.startYourself(NavigationBaseActivity.this);
ProfileActivity.startYourself(NavigationBaseActivity.this);
});
}

View file

@ -7,11 +7,9 @@ import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.net.Uri;
import androidx.annotation.IntDef;
import androidx.annotation.Nullable;
import androidx.exifinterface.media.ExifInterface;
import com.facebook.common.executors.CallerThreadExecutor;
import com.facebook.common.references.CloseableReference;
import com.facebook.datasource.DataSource;
@ -21,13 +19,15 @@ import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
import com.facebook.imagepipeline.image.CloseableImage;
import com.facebook.imagepipeline.request.ImageRequest;
import com.facebook.imagepipeline.request.ImageRequestBuilder;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import timber.log.Timber;
/**
@ -69,7 +69,9 @@ public class ImageUtils {
public static final int FILE_NAME_EXISTS = -4;
static final int NO_CATEGORY_SELECTED = -5;
private static ProgressDialog progressDialog;
private static ProgressDialog progressDialogWallpaper;
private static ProgressDialog progressDialogAvatar;
@IntDef(
flag = true,
@ -223,28 +225,78 @@ public class ImageUtils {
}, CallerThreadExecutor.getInstance());
}
/**
* Calls the set avatar api to set the image url as user's avatar
* @param context
* @param url
* @param username
* @param okHttpJsonApiClient
* @param compositeDisposable
*/
public static void setAvatarFromImageUrl(Context context, String url, String username,
OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable) {
showSettingAvatarProgressBar(context);
try {
compositeDisposable.add(okHttpJsonApiClient
.setAvatar(username, url)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
response -> {
if (response != null && response.getStatus().equals("200")) {
ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_successfully));
if (progressDialogAvatar != null && progressDialogAvatar.isShowing()) {
progressDialogAvatar.dismiss();
}
}
},
t -> {
Timber.e(t, "Setting Avatar Failed");
ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully));
if (progressDialogAvatar != null) {
progressDialogAvatar.cancel();
}
}
));
}
catch (Exception e){
Timber.d(e+"success");
ViewUtil.showLongToast(context, context.getString(R.string.avatar_set_unsuccessfully));
if (progressDialogAvatar != null) {
progressDialogAvatar.cancel();
}
}
}
private static void setWallpaper(Context context, Bitmap bitmap) {
WallpaperManager wallpaperManager = WallpaperManager.getInstance(context);
try {
wallpaperManager.setBitmap(bitmap);
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
if (progressDialog != null && progressDialog.isShowing()) {
progressDialog.dismiss();
if (progressDialogWallpaper != null && progressDialogWallpaper.isShowing()) {
progressDialogWallpaper.dismiss();
}
} catch (IOException e) {
Timber.e(e, "Error setting wallpaper");
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_unsuccessfully));
if (progressDialog != null) {
progressDialog.cancel();
if (progressDialogWallpaper != null) {
progressDialogWallpaper.cancel();
}
}
}
private static void showSettingWallpaperProgressBar(Context context) {
progressDialog = ProgressDialog.show(context, context.getString(R.string.setting_wallpaper_dialog_title),
progressDialogWallpaper = ProgressDialog.show(context, context.getString(R.string.setting_wallpaper_dialog_title),
context.getString(R.string.setting_wallpaper_dialog_message), true);
}
private static void showSettingAvatarProgressBar(Context context) {
progressDialogAvatar = ProgressDialog.show(context, context.getString(R.string.setting_avatar_dialog_title),
context.getString(R.string.setting_avatar_dialog_message), true);
}
/**
* Result variable is a result of an or operation of all possible problems. Ie. if result
* is 0001 means IMAGE_DARK

View file

@ -1,495 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/toolbarLayout"
android:layout_width="wrap_content"
android:layout_height="?attr/actionBarSize">
<include layout="@layout/toolbar"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="?attr/actionBarSize"
android:layout_below="@id/toolbarLayout">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/toolbar"
android:background="?attr/achievementBackground"
android:orientation="vertical">
<TextView
style="?android:textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:text="@string/level"
android:id="@+id/achievement_level"
android:textAllCaps="true"/>
<ImageView
android:id="@+id/achievement_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin_vertical"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/black"
android:layout_marginVertical="@dimen/activity_margin_vertical" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/badge_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/achievement_info"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/achievement_badge_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/badge" />
<TextView
android:id="@+id/achievement_badge_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="@color/achievement_badge_text"
android:textSize="75sp"
app:layout_constraintBottom_toBottomOf="@+id/achievement_badge_image"
app:layout_constraintEnd_toEndOf="@+id/achievement_badge_image"
app:layout_constraintStart_toStartOf="@+id/achievement_badge_image"
app:layout_constraintTop_toTopOf="@+id/achievement_badge_image"
app:layout_constraintVertical_bias="0.58" />
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout
android:id="@+id/layout_image_uploaded"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/badge_layout"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal">
<TextView
style="?android:textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:id="@+id/images_upload_text_param"
android:layout_marginTop="@dimen/achievements_activity_margin_vertical"
android:text="@string/images_uploaded" />
<ImageView
android:layout_width="@dimen/quarter_standard_height"
android:layout_height="@dimen/quarter_standard_height"
android:id="@+id/images_upload_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_toRightOf="@+id/images_upload_text_param"
android:layout_toEndOf="@+id/images_upload_text_param"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"/>
<com.dinuscxj.progressbar.CircleProgressBar
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="@dimen/large_gap"
android:layout_marginRight="@dimen/large_gap"
android:id="@+id/images_uploaded_progressbar"
android:progress="50"
app:progress_text_size="@dimen/progressbar_text"
app:progress_end_color="#8C8B98"
app:progress_start_color="#3A3381"
app:progress_stroke_width="@dimen/progressbar_stroke"
app:progress_text_format_pattern="573/110"
app:progress_text_color="@color/secondaryColor"
app:style="solid_line" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_image_reverts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tiny_margin"
android:layout_below="@+id/layout_image_uploaded"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal">
<TextView
style="?android:textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:id="@+id/images_reverted_text"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:text="@string/image_reverts" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:id="@+id/images_reverted_info"
android:layout_toRightOf="@+id/images_reverted_text"
android:layout_toEndOf="@+id/images_reverted_text"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/achievements_revert_limit_message"
android:textSize="@dimen/small_text"
android:id="@+id/images_revert_limit_text"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_below="@+id/images_reverted_text"/>
<com.dinuscxj.progressbar.CircleProgressBar
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="@dimen/large_gap"
android:layout_marginEnd="@dimen/large_gap"
android:progress="50"
android:id="@+id/image_reverts_progressbar"
app:progress_end_color="#8C8B98"
app:progress_start_color="#3A3381"
app:progress_text_size="@dimen/progressbar_text"
app:progress_stroke_width="@dimen/progressbar_stroke"
app:progress_text_format_pattern="92%%"
app:progress_text_color="@color/secondaryColor"
app:style="solid_line" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_image_used_by_wiki"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tiny_margin"
android:layout_below="@+id/layout_image_reverts"
android:layout_marginBottom="@dimen/activity_margin_vertical"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal">
<TextView
style="?android:textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/images_used_by_wiki_text"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/achievements_activity_margin_vertical"
android:text="@string/images_used_by_wiki" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/images_used_by_wiki_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_toRightOf="@+id/images_used_by_wiki_text"
android:layout_toEndOf="@+id/images_used_by_wiki_text"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"/>
<com.dinuscxj.progressbar.CircleProgressBar
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="@dimen/large_gap"
android:layout_marginEnd="@dimen/large_gap"
android:progress="50"
app:progress_text_size="@dimen/progressbar_text"
android:id="@+id/images_used_by_wiki_progress_bar"
app:progress_end_color="#8C8B98"
app:progress_start_color="#3A3381"
app:progress_stroke_width="2.5dp"
app:progress_text_color="@color/secondaryColor"
app:progress_text_format_pattern="12/24"
app:style="solid_line" />
</RelativeLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_centerVertical="true"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/layout_statistics"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/statistics"
style="?android:textAppearanceLarge"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_vertical"
android:textAllCaps="true"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="@+id/wikidata_edits"
android:layout_toLeftOf="@+id/wikidata_edits"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="@dimen/overflow_icon_dimen"
android:layout_height="@dimen/overflow_icon_dimen"
android:id="@+id/wikidata_edits_icon"
app:srcCompat="@drawable/ic_custom_map_marker" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="@string/statistics_wikidata_edits" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/images_nearby_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/half_standard_height"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_centerVertical="true"
tools:text="2"
android:id="@+id/wikidata_edits"
android:layout_marginRight="@dimen/half_standard_height" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="@+id/image_featured"
android:layout_toLeftOf="@+id/image_featured"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="@dimen/overflow_icon_dimen"
android:layout_height="@dimen/overflow_icon_dimen"
android:id="@+id/featured_image_icon"
app:srcCompat="@drawable/featured"
android:scaleType="centerCrop" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="@string/statistics_featured" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/images_featured_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_centerVertical="true"
tools:text="2"
android:id="@+id/image_featured"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/half_standard_height"
android:layout_marginRight="@dimen/half_standard_height" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="@+id/thanks_received"
android:layout_toLeftOf="@+id/thanks_received"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="@dimen/overflow_icon_dimen"
android:layout_height="@dimen/overflow_icon_dimen"
android:id="@+id/thanks_image_icon"
app:srcCompat="@drawable/ic_thanks"
android:scaleType="centerCrop" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="@string/statistics_thanks" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/thanks_received_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_centerVertical="true"
tools:text="2"
android:id="@+id/thanks_received"
android:layout_marginEnd="@dimen/half_standard_height"
android:layout_marginRight="@dimen/half_standard_height" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
<include layout="@layout/drawer_view" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/toolbar_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/primaryDarkColor">
<include layout="@layout/toolbar"/>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:background="?attr/tabBackground"
app:tabIndicatorColor="?attr/tabIndicatorColor"
app:tabMode="fixed"
app:tabSelectedTextColor="?attr/tabSelectedTextColor"
app:tabTextColor="?attr/tabTextColor" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager.widget.ViewPager
android:id="@+id/viewPager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar_layout" />
</RelativeLayout>
<include layout="@layout/drawer_view" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,482 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/drawer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ScrollView
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/toolbar"
android:background="?attr/achievementBackground"
android:orientation="vertical">
<TextView
style="?android:textAppearanceLarge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:text="@string/level"
android:id="@+id/achievement_level"
android:textAllCaps="true"/>
<ImageView
android:id="@+id/achievement_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin_vertical"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/black"
android:layout_marginVertical="@dimen/activity_margin_vertical" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/badge_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/achievement_info"
android:layout_centerHorizontal="true">
<ImageView
android:id="@+id/achievement_badge_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/badge" />
<TextView
android:id="@+id/achievement_badge_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textAlignment="center"
android:textColor="@color/achievement_badge_text"
android:textSize="75sp"
app:layout_constraintBottom_toBottomOf="@+id/achievement_badge_image"
app:layout_constraintEnd_toEndOf="@+id/achievement_badge_image"
app:layout_constraintStart_toStartOf="@+id/achievement_badge_image"
app:layout_constraintTop_toTopOf="@+id/achievement_badge_image"
app:layout_constraintVertical_bias="0.58" />
</androidx.constraintlayout.widget.ConstraintLayout>
<RelativeLayout
android:id="@+id/layout_image_uploaded"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/badge_layout"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal">
<TextView
style="?android:textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:id="@+id/images_upload_text_param"
android:layout_marginTop="@dimen/achievements_activity_margin_vertical"
android:text="@string/images_uploaded" />
<ImageView
android:layout_width="@dimen/quarter_standard_height"
android:layout_height="@dimen/quarter_standard_height"
android:id="@+id/images_upload_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_toRightOf="@+id/images_upload_text_param"
android:layout_toEndOf="@+id/images_upload_text_param"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"/>
<com.dinuscxj.progressbar.CircleProgressBar
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="@dimen/large_gap"
android:layout_marginRight="@dimen/large_gap"
android:id="@+id/images_uploaded_progressbar"
android:progress="50"
app:progress_text_size="@dimen/progressbar_text"
app:progress_end_color="#8C8B98"
app:progress_start_color="#3A3381"
app:progress_stroke_width="@dimen/progressbar_stroke"
app:progress_text_format_pattern="573/110"
app:progress_text_color="@color/secondaryColor"
app:style="solid_line" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_image_reverts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tiny_margin"
android:layout_below="@+id/layout_image_uploaded"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal">
<TextView
style="?android:textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:id="@+id/images_reverted_text"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:text="@string/image_reverts" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:id="@+id/images_reverted_info"
android:layout_toRightOf="@+id/images_reverted_text"
android:layout_toEndOf="@+id/images_reverted_text"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/achievements_revert_limit_message"
android:textSize="@dimen/small_text"
android:id="@+id/images_revert_limit_text"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_below="@+id/images_reverted_text"/>
<com.dinuscxj.progressbar.CircleProgressBar
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="@dimen/large_gap"
android:layout_marginEnd="@dimen/large_gap"
android:progress="50"
android:id="@+id/image_reverts_progressbar"
app:progress_end_color="#8C8B98"
app:progress_start_color="#3A3381"
app:progress_text_size="@dimen/progressbar_text"
app:progress_stroke_width="@dimen/progressbar_stroke"
app:progress_text_format_pattern="92%%"
app:progress_text_color="@color/secondaryColor"
app:style="solid_line" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/layout_image_used_by_wiki"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/tiny_margin"
android:layout_below="@+id/layout_image_reverts"
android:layout_marginBottom="@dimen/activity_margin_vertical"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal">
<TextView
style="?android:textAppearanceMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/images_used_by_wiki_text"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/achievements_activity_margin_vertical"
android:text="@string/images_used_by_wiki" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/images_used_by_wiki_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_toRightOf="@+id/images_used_by_wiki_text"
android:layout_toEndOf="@+id/images_used_by_wiki_text"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"/>
<com.dinuscxj.progressbar.CircleProgressBar
android:layout_width="@dimen/dimen_40"
android:layout_height="@dimen/dimen_40"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginRight="@dimen/large_gap"
android:layout_marginEnd="@dimen/large_gap"
android:progress="50"
app:progress_text_size="@dimen/progressbar_text"
android:id="@+id/images_used_by_wiki_progress_bar"
app:progress_end_color="#8C8B98"
app:progress_start_color="#3A3381"
app:progress_stroke_width="2.5dp"
app:progress_text_color="@color/secondaryColor"
app:progress_text_format_pattern="12/24"
app:style="solid_line" />
</RelativeLayout>
<ProgressBar
android:id="@+id/progressBar"
android:layout_centerVertical="true"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</RelativeLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/layout_statistics"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/statistics"
style="?android:textAppearanceLarge"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_vertical"
android:textAllCaps="true"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="@+id/wikidata_edits"
android:layout_toLeftOf="@+id/wikidata_edits"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="@dimen/overflow_icon_dimen"
android:layout_height="@dimen/overflow_icon_dimen"
android:id="@+id/wikidata_edits_icon"
app:srcCompat="@drawable/ic_custom_map_marker" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="@string/statistics_wikidata_edits" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/images_nearby_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/half_standard_height"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_centerVertical="true"
tools:text="2"
android:id="@+id/wikidata_edits"
android:layout_marginRight="@dimen/half_standard_height" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="@+id/image_featured"
android:layout_toLeftOf="@+id/image_featured"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="@dimen/overflow_icon_dimen"
android:layout_height="@dimen/overflow_icon_dimen"
android:id="@+id/featured_image_icon"
app:srcCompat="@drawable/featured"
android:scaleType="centerCrop" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="@string/statistics_featured" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/images_featured_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_centerVertical="true"
tools:text="2"
android:id="@+id/image_featured"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/half_standard_height"
android:layout_marginRight="@dimen/half_standard_height" />
</RelativeLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_toStartOf="@+id/thanks_received"
android:layout_toLeftOf="@+id/thanks_received"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageView
android:layout_width="@dimen/overflow_icon_dimen"
android:layout_height="@dimen/overflow_icon_dimen"
android:id="@+id/thanks_image_icon"
app:srcCompat="@drawable/ic_thanks"
android:scaleType="centerCrop" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="@string/statistics_thanks" />
<ImageView
android:layout_width="@dimen/medium_width"
android:layout_height="@dimen/medium_height"
android:id="@+id/thanks_received_info"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:layout_marginEnd="@dimen/activity_margin_horizontal"
android:layout_gravity="top"
app:srcCompat="@drawable/ic_info_outline_24dp"
android:tint="@color/primaryDarkColor" />
</LinearLayout>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="?android:textAppearanceMedium"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true"
android:layout_marginTop="@dimen/activity_margin_horizontal"
android:layout_marginStart="@dimen/activity_margin_horizontal"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_centerVertical="true"
tools:text="2"
android:id="@+id/thanks_received"
android:layout_marginEnd="@dimen/half_standard_height"
android:layout_marginRight="@dimen/half_standard_height" />
</RelativeLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>
<include layout="@layout/drawer_view" />
</androidx.drawerlayout.widget.DrawerLayout>

View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:id="@+id/filters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:weightSum="1"
android:layout_margin="20dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<Spinner
android:layout_marginStart="50dp"
android:id="@+id/duration_spinner"
android:layout_width="match_parent"
android:layout_weight="0.5"
android:layout_height="match_parent" />
<Spinner
android:layout_marginEnd="50dp"
android:id="@+id/category_spinner"
android:layout_width="match_parent"
android:layout_weight="0.5"
android:layout_height="match_parent" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/leaderboard_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/filters" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/leaderboard_list" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"
android:weightSum="1">
<TextView
android:id="@+id/user_rank"
style="?android:textAppearanceMedium"
android:gravity="center_vertical|center_horizontal"
android:layout_width="0dp"
android:layout_weight="0.1"
android:layout_height="match_parent"/>
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/user_avatar"
android:layout_weight="0.1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="20dp"
fresco:roundAsCircle="true"
fresco:viewAspectRatio="1"
android:gravity="center_vertical|start"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/user_name"
style="?android:textAppearanceMedium"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.6"
android:layout_height="match_parent"/>
<TextView
android:id="@+id/user_count"
style="?android:textAppearanceSmall"
android:gravity="center_vertical|end"
android:layout_width="0dp"
android:layout_weight="0.2"
android:layout_height="match_parent"/>
</LinearLayout>

View file

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:fresco="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.facebook.drawee.view.SimpleDraweeView
android:id="@+id/avatar"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_margin="20dp"
fresco:roundAsCircle="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/avatar" />
<TextView
android:id="@+id/rank"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/username" />
<TextView
android:id="@+id/count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="?android:textAppearanceMedium"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/rank" />
<LinearLayout
android:id="@+id/column_names"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_margin="15dp"
android:weightSum="1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/count">
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_rank"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|start"
android:layout_width="0dp"
android:layout_weight="0.6"
android:text="@string/leaderboard_column_user"
android:layout_height="match_parent"/>
<TextView
style="?android:textAppearanceButton"
android:gravity="center_vertical|end"
android:layout_width="0dp"
android:layout_weight="0.2"
android:text="@string/leaderboard_column_count"
android:layout_height="match_parent"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -25,5 +25,9 @@
android:id="@+id/menu_set_as_wallpaper"
android:title="@string/menu_set_wallpaper"
app:showAsAction="never" />
<item
android:id="@+id/menu_set_as_avatar"
android:title="@string/menu_set_avatar"
app:showAsAction="never" />
</menu>

View file

@ -46,4 +46,29 @@
<item>1</item>
<item>0</item>
</string-array>
<string-array name="leaderboard_categories">
<item>@string/leaderboard_upload</item>
<item>@string/leaderboard_used</item>
<item>@string/leaderboard_nearby</item>
</string-array>
<string-array name="leaderboard_category_values">
<item>upload</item>
<item>used</item>
<item>nearby</item>
</string-array>
<string-array name="leaderboard_durations">
<item>@string/leaderboard_weekly</item>
<item>@string/leaderboard_yearly</item>
<item>@string/leaderboard_all_time</item>
</string-array>
<string-array name="leaderboard_duration_values">
<item>weekly</item>
<item>yearly</item>
<item>all_time</item>
</string-array>
</resources>

View file

@ -399,6 +399,7 @@
<string name="nominate_delete">Nominate For Deletion</string>
<string name="delete">Delete</string>
<string name="Achievements">Achievements</string>
<string name="Profile">Profile</string>
<string name="statistics">Statistics</string>
<string name="statistics_thanks">Thanks Received</string>
<string name="statistics_featured">Featured Images</string>
@ -669,4 +670,22 @@ Upload your first media by tapping on the add button.</string>
<string name="pause">pause</string>
<string name="resume">resume</string>
<string name="paused">Paused</string>
<string name="achievements_tab_title">Achievements</string>
<string name="leaderboard_tab_title">Leaderboard</string>
<string name="rank_prefix">Rank:</string>
<string name="count_prefix">Count:</string>
<string name="leaderboard_column_rank">Rank</string>
<string name="leaderboard_column_user">User</string>
<string name="leaderboard_column_count">Count</string>
<string name="setting_avatar_dialog_title">Set as leaderboard avatar</string>
<string name="setting_avatar_dialog_message">Setting as avatar, please wait</string>
<string name="avatar_set_successfully">Avatar set</string>
<string name="avatar_set_unsuccessfully">Error setting new avatar, please try again</string>
<string name="menu_set_avatar">Set as avatar</string>
<string name="leaderboard_yearly">Yearly</string>
<string name="leaderboard_weekly">Weekly</string>
<string name="leaderboard_all_time">All time</string>
<string name="leaderboard_upload">Upload</string>
<string name="leaderboard_nearby">Nearby</string>
<string name="leaderboard_used">Used</string>
</resources>

View file

@ -3,10 +3,13 @@ package fr.free.nrw.commons.delete
import android.content.Context
import android.content.res.Resources
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.achievements.FeedbackResponse
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
import fr.free.nrw.commons.profile.achievements.FeedbackResponse
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse
import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse
import fr.free.nrw.commons.utils.ViewUtilWrapper
import io.reactivex.Observable
import io.reactivex.Single
import media
import org.junit.Before
@ -54,6 +57,10 @@ class ReasonBuilderTest {
`when`(sessionManager?.doesAccountExist()).thenReturn(true)
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
`when`(okHttpJsonApiClient!!.getLeaderboard(anyString(), anyString(), anyString(), anyString(), anyString()))
.thenReturn(Observable.just(mock(LeaderboardResponse::class.java)))
`when`(okHttpJsonApiClient!!.setAvatar(anyString(), anyString()))
.thenReturn(Single.just(mock(UpdateAvatarResponse::class.java)))
val media = media(filename="test_file", dateUploaded = Date())

View file

@ -0,0 +1,116 @@
package fr.free.nrw.commons.leaderboard;
import com.google.gson.Gson;
import fr.free.nrw.commons.profile.leaderboard.LeaderboardResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
/**
* This class tests the Leaderboard API calls
*/
public class LeaderboardApiTest {
MockWebServer server;
private static final String TEST_USERNAME = "user";
private static final String TEST_AVATAR = "avatar";
private static final int TEST_USER_RANK = 1;
private static final int TEST_USER_COUNT = 0;
private static final String FILE_NAME = "leaderboard_sample_response.json";
private static final String ENDPOINT = "/leaderboard.py";
/**
* This method initialises a Mock Server
*/
@Before
public void initTest() {
server = new MockWebServer();
}
/**
* This method will setup a Mock Server and load Test JSON Response File
* @throws Exception
*/
@Before
public void setUp() throws Exception {
String testResponseBody = convertStreamToString(getClass().getClassLoader().getResourceAsStream(FILE_NAME));
server.enqueue(new MockResponse().setBody(testResponseBody));
server.start();
}
/**
* This method converts a Input Stream to String
* @param is takes Input Stream of JSON File as Parameter
* @return a String with JSON data
* @throws Exception
*/
private static String convertStreamToString(InputStream is) throws Exception {
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
}
/**
* This method will call the Mock Server and Test it with sample values.
* It will test the Leaderboard API call functionality and check if the object is
* being created with the correct values
* @throws IOException
*/
@Test
public void apiTest() throws IOException {
HttpUrl httpUrl = server.url(ENDPOINT);
LeaderboardResponse response = sendRequest(new OkHttpClient(), httpUrl);
Assert.assertEquals(TEST_AVATAR, response.getAvatar());
Assert.assertEquals(TEST_USERNAME, response.getUsername());
Assert.assertEquals(Integer.valueOf(TEST_USER_RANK), response.getRank());
Assert.assertEquals(Integer.valueOf(TEST_USER_COUNT), response.getCategoryCount());
}
/**
* This method will call the Mock API and returns the Leaderboard Response Object
* @param okHttpClient
* @param httpUrl
* @return Leaderboard Response Object
* @throws IOException
*/
private LeaderboardResponse sendRequest(OkHttpClient okHttpClient, HttpUrl httpUrl)
throws IOException {
Request request = new Builder().url(httpUrl).build();
Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
Gson gson = new Gson();
return gson.fromJson(response.body().string(), LeaderboardResponse.class);
}
return null;
}
/**
* This method shuts down the Mock Server
* @throws IOException
*/
@After
public void shutdown() throws IOException {
server.shutdown();
}
}

View file

@ -0,0 +1,117 @@
package fr.free.nrw.commons.leaderboard;
import com.google.gson.Gson;
import fr.free.nrw.commons.profile.leaderboard.UpdateAvatarResponse;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Request.Builder;
import okhttp3.Response;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
public class UpdateAvatarApiTest {
private static final String TEST_USERNAME = "user";
private static final String TEST_STATUS = "200";
private static final String TEST_MESSAGE = "Avatar Updated";
private static final String FILE_NAME = "update_leaderboard_avatar_sample_response.json";
private static final String ENDPOINT = "/update_avatar.py";
MockWebServer server;
/**
* This method converts a Input Stream to String
*
* @param is takes Input Stream of JSON File as Parameter
* @return a String with JSON data
* @throws Exception
*/
private static String convertStreamToString(final InputStream is) throws Exception {
final BufferedReader reader = new BufferedReader(new InputStreamReader(is));
final StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line).append("\n");
}
reader.close();
return sb.toString();
}
/**
* This method initialises a Mock Server
*/
@Before
public void initTest() {
server = new MockWebServer();
}
/**
* This method will setup a Mock Server and load Test JSON Response File
*
* @throws Exception
*/
@Before
public void setUp() throws Exception {
final String testResponseBody = convertStreamToString(
getClass().getClassLoader().getResourceAsStream(FILE_NAME));
server.enqueue(new MockResponse().setBody(testResponseBody));
server.start();
}
/**
* This method will call the Mock Server and Test it with sample values. It will test the Update
* Avatar API call functionality and check if the object is being created with the correct
* values
*
* @throws IOException
*/
@Test
public void apiTest() throws IOException {
final HttpUrl httpUrl = server.url(ENDPOINT);
final UpdateAvatarResponse response = sendRequest(new OkHttpClient(), httpUrl);
Assert.assertEquals(TEST_USERNAME, response.getUser());
Assert.assertEquals(TEST_STATUS, response.getStatus());
Assert.assertEquals(TEST_MESSAGE, response.getMessage());
}
/**
* This method will call the Mock API and returns the Update Avatar Response Object
*
* @param okHttpClient
* @param httpUrl
* @return Update Avatar Response Object
* @throws IOException
*/
private UpdateAvatarResponse sendRequest(final OkHttpClient okHttpClient, final HttpUrl httpUrl)
throws IOException {
final Request request = new Builder().url(httpUrl).build();
final Response response = okHttpClient.newCall(request).execute();
if (response.isSuccessful()) {
final Gson gson = new Gson();
return gson.fromJson(response.body().string(), UpdateAvatarResponse.class);
}
return null;
}
/**
* This method shuts down the Mock Server
*
* @throws IOException
*/
@After
public void shutdown() throws IOException {
server.shutdown();
}
}

View file

@ -0,0 +1,19 @@
{
"status": 200,
"username": "user",
"category_count": 0,
"limit": null,
"avatar": "avatar",
"offset": null,
"duration": "all_time",
"leaderboard_list": [
{
"username": "user",
"category_count": 0,
"avatar": "avatar",
"rank": 1
}
],
"category": "used",
"rank": 1
}

View file

@ -0,0 +1,5 @@
{
"status": "200",
"message": "Avatar Updated",
"user": "user"
}