mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
[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
This commit is contained in:
parent
5877d7a14a
commit
5f77f610f5
16 changed files with 535 additions and 215 deletions
|
|
@ -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"
|
||||
|
|
@ -210,8 +211,8 @@ android {
|
|||
|
||||
configurations.all {
|
||||
resolutionStrategy.force 'androidx.annotation:annotation:1.0.2'
|
||||
exclude module: 'okhttp-ws'
|
||||
}
|
||||
|
||||
flavorDimensions 'tier'
|
||||
productFlavors {
|
||||
prod {
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ package fr.free.nrw.commons.mwapi;
|
|||
import android.text.TextUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.gson.Gson;
|
||||
import fr.free.nrw.commons.profile.achievements.FeaturedImages;
|
||||
import fr.free.nrw.commons.profile.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.upload.FileUtils;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
|
@ -65,32 +65,32 @@ public class OkHttpJsonApiClient {
|
|||
}
|
||||
|
||||
@NonNull
|
||||
public Single<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
|
||||
public Observable<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
|
||||
final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
|
||||
+ "/leaderboard.py";
|
||||
return Single.fromCallable(() -> {
|
||||
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();
|
||||
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 null;
|
||||
return new LeaderboardResponse();
|
||||
}
|
||||
Timber.d("Response for leaderboard is %s", json);
|
||||
try {
|
||||
|
|
@ -99,7 +99,7 @@ public class OkHttpJsonApiClient {
|
|||
return new LeaderboardResponse();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return new LeaderboardResponse();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -188,7 +188,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();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
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 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;
|
||||
|
||||
public class DataSourceClass extends PageKeyedDataSource<Integer, LeaderboardList> {
|
||||
|
||||
private OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
private SessionManager sessionManager;
|
||||
private MutableLiveData<String> progressLiveStatus;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
public DataSourceClass(OkHttpJsonApiClient okHttpJsonApiClient,SessionManager sessionManager) {
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
this.sessionManager = sessionManager;
|
||||
progressLiveStatus = new MutableLiveData<>();
|
||||
}
|
||||
|
||||
|
||||
public MutableLiveData<String> getProgressLiveStatus() {
|
||||
return progressLiveStatus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadInitial(@NonNull LoadInitialParams<Integer> params,
|
||||
@NonNull LoadInitialCallback<Integer, LeaderboardList> callback) {
|
||||
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
|
||||
"all_time", "upload", String.valueOf(PAGE_SIZE), String.valueOf(START_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);
|
||||
}
|
||||
));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadBefore(@NonNull LoadParams<Integer> params,
|
||||
@NonNull LoadCallback<Integer, LeaderboardList> callback) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadAfter(@NonNull LoadParams<Integer> params,
|
||||
@NonNull LoadCallback<Integer, LeaderboardList> callback) {
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
|
||||
"all_time", "upload", String.valueOf(PAGE_SIZE), 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 + PAGE_SIZE);
|
||||
}
|
||||
},
|
||||
t -> {
|
||||
Timber.e(t, "Fetching leaderboard statistics failed");
|
||||
progressLiveStatus.postValue(LOADING);
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
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;
|
||||
|
||||
public class DataSourceFactory extends DataSource.Factory<Integer, LeaderboardList> {
|
||||
|
||||
private MutableLiveData<DataSourceClass> liveData;
|
||||
private OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
private CompositeDisposable compositeDisposable;
|
||||
private SessionManager sessionManager;
|
||||
|
||||
public DataSourceFactory(OkHttpJsonApiClient okHttpJsonApiClient, CompositeDisposable compositeDisposable,
|
||||
SessionManager sessionManager) {
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
this.compositeDisposable = compositeDisposable;
|
||||
this.sessionManager = sessionManager;
|
||||
liveData = new MutableLiveData<>();
|
||||
}
|
||||
|
||||
public MutableLiveData<DataSourceClass> getMutableLiveData() {
|
||||
return liveData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DataSource<Integer, LeaderboardList> create() {
|
||||
DataSourceClass dataSourceClass = new DataSourceClass(okHttpJsonApiClient, sessionManager);
|
||||
liveData.postValue(dataSourceClass);
|
||||
return dataSourceClass;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package fr.free.nrw.commons.profile.leaderboard;
|
||||
|
||||
public class LeaderboardConstants {
|
||||
|
||||
public static final int PAGE_SIZE = 10;
|
||||
|
||||
public static final int START_OFFSET = 0;
|
||||
|
||||
public static final String AVATAR_SOURCE_URL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";
|
||||
|
||||
public final static String LOADING = "Loading";
|
||||
|
||||
public final static String LOADED = "Loaded";
|
||||
|
||||
}
|
||||
|
|
@ -1,18 +1,20 @@
|
|||
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 android.accounts.Account;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
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 com.facebook.drawee.view.SimpleDraweeView;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
|
|
@ -21,25 +23,12 @@ 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.List;
|
||||
import java.util.Objects;
|
||||
import javax.inject.Inject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class LeaderboardFragment extends CommonsDaggerSupportFragment {
|
||||
|
||||
@BindView(R.id.avatar)
|
||||
SimpleDraweeView avatar;
|
||||
|
||||
@BindView(R.id.username)
|
||||
TextView username;
|
||||
|
||||
@BindView(R.id.rank)
|
||||
TextView rank;
|
||||
|
||||
@BindView(R.id.count)
|
||||
TextView count;
|
||||
|
||||
@BindView(R.id.leaderboard_list)
|
||||
RecyclerView leaderboardListRecyclerView;
|
||||
|
||||
|
|
@ -52,7 +41,10 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
|
|||
@Inject
|
||||
OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
|
||||
private String avatarSourceURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";
|
||||
@Inject
|
||||
ViewModelFactory viewModelFactory;
|
||||
|
||||
LeaderboardListViewModel viewModel;
|
||||
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
|
|
@ -67,12 +59,12 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
|
||||
/**
|
||||
* To call the API to get results in form Single<JSONObject>
|
||||
* which then calls parseJson when results are fetched
|
||||
* To call the API to get results
|
||||
* which then sets the views using setLeaderboardUser method
|
||||
*/
|
||||
private void setLeaderboard() {
|
||||
if (checkAccount()) {
|
||||
try{
|
||||
try {
|
||||
compositeDisposable.add(okHttpJsonApiClient
|
||||
.getLeaderboard(Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
|
||||
"all_time", "upload", null, null)
|
||||
|
|
@ -81,8 +73,7 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
|
|||
.subscribe(
|
||||
response -> {
|
||||
if (response != null && response.getStatus() == 200) {
|
||||
setLeaderboardUser(response);
|
||||
setLeaderboardList(response.getLeaderboardList());
|
||||
setViews(response);
|
||||
}
|
||||
},
|
||||
t -> {
|
||||
|
|
@ -101,20 +92,23 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
|
|||
* Set the views
|
||||
* @param response Leaderboard Response Object
|
||||
*/
|
||||
private void setLeaderboardUser(LeaderboardResponse response) {
|
||||
hideProgressBar();
|
||||
avatar.setImageURI(
|
||||
Uri.parse(String.format(avatarSourceURL, response.getAvatar(), response.getAvatar())));
|
||||
username.setText(response.getUsername());
|
||||
rank.setText(String.format("%s %d", getString(R.string.rank_prefix), response.getRank()));
|
||||
count.setText(String.format("%s %d", getString(R.string.count_prefix), response.getCategoryCount()));
|
||||
}
|
||||
|
||||
private void setLeaderboardList(List<LeaderboardList> leaderboardList) {
|
||||
LeaderboardListAdapter leaderboardListAdapter = new LeaderboardListAdapter(leaderboardList);
|
||||
private void setViews(LeaderboardResponse response) {
|
||||
viewModel = new ViewModelProvider(this, viewModelFactory).get(LeaderboardListViewModel.class);
|
||||
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(leaderboardListAdapter);
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,20 +117,23 @@ public class LeaderboardFragment extends CommonsDaggerSupportFragment {
|
|||
private void hideProgressBar() {
|
||||
if (progressBar != null) {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
avatar.setVisibility(View.VISIBLE);
|
||||
username.setVisibility(View.VISIBLE);
|
||||
rank.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(){
|
||||
avatar.setVisibility(View.INVISIBLE);
|
||||
username.setVisibility(View.INVISIBLE);
|
||||
rank.setVisibility(View.INVISIBLE);
|
||||
leaderboardListRecyclerView.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
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;
|
||||
|
||||
|
|
@ -8,12 +11,15 @@ public class LeaderboardList {
|
|||
@SerializedName("username")
|
||||
@Expose
|
||||
private String username;
|
||||
|
||||
@SerializedName("category_count")
|
||||
@Expose
|
||||
private Integer categoryCount;
|
||||
|
||||
@SerializedName("avatar")
|
||||
@Expose
|
||||
private String avatar;
|
||||
|
||||
@SerializedName("rank")
|
||||
@Expose
|
||||
private Integer rank;
|
||||
|
|
@ -50,4 +56,29 @@ public class LeaderboardList {
|
|||
this.rank = rank;
|
||||
}
|
||||
|
||||
|
||||
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());
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj == this) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LeaderboardList leaderboardList = (LeaderboardList) obj;
|
||||
return leaderboardList.getRank().equals(this.getRank());
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.profile.leaderboard;
|
||||
|
||||
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.AVATAR_SOURCE_URL;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.view.LayoutInflater;
|
||||
|
|
@ -7,16 +9,16 @@ 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 java.util.List;
|
||||
|
||||
public class LeaderboardListAdapter extends RecyclerView.Adapter<LeaderboardListAdapter.ListViewHolder> {
|
||||
public class LeaderboardListAdapter extends PagedListAdapter<LeaderboardList, LeaderboardListAdapter.ListViewHolder> {
|
||||
|
||||
private List<LeaderboardList> leaderboardList;
|
||||
|
||||
private String avatarSourceURL = "https://upload.wikimedia.org/wikipedia/commons/thumb/0/0a/%s/1024px-%s.png";
|
||||
protected LeaderboardListAdapter() {
|
||||
super(LeaderboardList.DIFF_CALLBACK);
|
||||
}
|
||||
|
||||
public class ListViewHolder extends RecyclerView.ViewHolder {
|
||||
TextView rank;
|
||||
|
|
@ -41,10 +43,6 @@ public class LeaderboardListAdapter extends RecyclerView.Adapter<LeaderboardList
|
|||
}
|
||||
}
|
||||
|
||||
public LeaderboardListAdapter(List<LeaderboardList> leaderboardList) {
|
||||
this.leaderboardList = leaderboardList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides the onCreateViewHolder and inflates the recyclerview list item layout
|
||||
* @param parent
|
||||
|
|
@ -72,21 +70,12 @@ public class LeaderboardListAdapter extends RecyclerView.Adapter<LeaderboardList
|
|||
TextView username = holder.username;
|
||||
TextView count = holder.count;
|
||||
|
||||
rank.setText(leaderboardList.get(position).getRank().toString());
|
||||
rank.setText(getItem(position).getRank().toString());
|
||||
|
||||
avatar.setImageURI(
|
||||
Uri.parse(String.format(avatarSourceURL, leaderboardList.get(position).getAvatar(),
|
||||
leaderboardList.get(position).getAvatar())));
|
||||
username.setText(leaderboardList.get(position).getUsername());
|
||||
count.setText(leaderboardList.get(position).getCategoryCount().toString());
|
||||
}
|
||||
|
||||
/**
|
||||
* Override the getItemCount method
|
||||
* @return the size of the recycler view list
|
||||
*/
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return leaderboardList.size();
|
||||
Uri.parse(String.format(AVATAR_SOURCE_URL, getItem(position).getAvatar(),
|
||||
getItem(position).getAvatar())));
|
||||
username.setText(getItem(position).getUsername());
|
||||
count.setText(getItem(position).getCategoryCount().toString());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
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;
|
||||
|
||||
public class LeaderboardListViewModel extends ViewModel {
|
||||
|
||||
private DataSourceFactory dataSourceFactory;
|
||||
private LiveData<PagedList<LeaderboardList>> listLiveData;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
private LiveData<String> progressLoadStatus = new MutableLiveData<>();
|
||||
|
||||
public LeaderboardListViewModel(OkHttpJsonApiClient okHttpJsonApiClient, SessionManager sessionManager) {
|
||||
dataSourceFactory = new DataSourceFactory(okHttpJsonApiClient, compositeDisposable, sessionManager);
|
||||
initializePaging();
|
||||
}
|
||||
|
||||
|
||||
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);
|
||||
|
||||
}
|
||||
|
||||
public LiveData<String> getProgressLoadStatus() {
|
||||
return progressLoadStatus;
|
||||
}
|
||||
|
||||
public LiveData<PagedList<LeaderboardList>> getListLiveData() {
|
||||
return listLiveData;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
super.onCleared();
|
||||
compositeDisposable.clear();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -9,30 +9,39 @@ public class LeaderboardResponse {
|
|||
@SerializedName("status")
|
||||
@Expose
|
||||
private Integer status;
|
||||
|
||||
@SerializedName("username")
|
||||
@Expose
|
||||
private String username;
|
||||
|
||||
@SerializedName("category_count")
|
||||
@Expose
|
||||
private Integer categoryCount;
|
||||
|
||||
@SerializedName("limit")
|
||||
@Expose
|
||||
private Object limit;
|
||||
private int limit;
|
||||
|
||||
@SerializedName("avatar")
|
||||
@Expose
|
||||
private String avatar;
|
||||
|
||||
@SerializedName("offset")
|
||||
@Expose
|
||||
private Object offset;
|
||||
private int offset;
|
||||
|
||||
@SerializedName("duration")
|
||||
@Expose
|
||||
private String duration;
|
||||
|
||||
@SerializedName("leaderboard_list")
|
||||
@Expose
|
||||
private List<LeaderboardList> leaderboardList = null;
|
||||
|
||||
@SerializedName("category")
|
||||
@Expose
|
||||
private String category;
|
||||
|
||||
@SerializedName("rank")
|
||||
@Expose
|
||||
private Integer rank;
|
||||
|
|
@ -61,11 +70,11 @@ public class LeaderboardResponse {
|
|||
this.categoryCount = categoryCount;
|
||||
}
|
||||
|
||||
public Object getLimit() {
|
||||
public int getLimit() {
|
||||
return limit;
|
||||
}
|
||||
|
||||
public void setLimit(Object limit) {
|
||||
public void setLimit(int limit) {
|
||||
this.limit = limit;
|
||||
}
|
||||
|
||||
|
|
@ -77,11 +86,11 @@ public class LeaderboardResponse {
|
|||
this.avatar = avatar;
|
||||
}
|
||||
|
||||
public Object getOffset() {
|
||||
public int getOffset() {
|
||||
return offset;
|
||||
}
|
||||
|
||||
public void setOffset(Object offset) {
|
||||
public void setOffset(int offset) {
|
||||
this.offset = offset;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
package fr.free.nrw.commons.profile.leaderboard;
|
||||
|
||||
import static fr.free.nrw.commons.profile.leaderboard.LeaderboardConstants.AVATAR_SOURCE_URL;
|
||||
|
||||
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;
|
||||
|
||||
public class UserDetailAdapter extends RecyclerView.Adapter<UserDetailAdapter.DataViewHolder> {
|
||||
|
||||
LeaderboardResponse leaderboardResponse;
|
||||
|
||||
public UserDetailAdapter(LeaderboardResponse leaderboardResponse) {
|
||||
this.leaderboardResponse = leaderboardResponse;
|
||||
}
|
||||
|
||||
public class DataViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
TextView rank;
|
||||
SimpleDraweeView avatar;
|
||||
TextView username;
|
||||
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);
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return itemView.getContext();
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@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(String.format(AVATAR_SOURCE_URL, leaderboardResponse.getAvatar(),
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@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");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,116 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView
|
||||
|
||||
<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="match_parent">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/leaderboard_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<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" />
|
||||
<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" />
|
||||
|
||||
<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.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/column_names">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/leaderboard_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<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="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</ScrollView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
@ -1,18 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView
|
||||
<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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="15dp"
|
||||
android:layout_marginEnd="15dp"
|
||||
android:weightSum="1">
|
||||
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"
|
||||
|
|
@ -51,5 +47,4 @@
|
|||
android:layout_weight="0.2"
|
||||
android:layout_height="match_parent"/>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
</LinearLayout>
|
||||
86
app/src/main/res/layout/leaderboard_user_element.xml
Normal file
86
app/src/main/res/layout/leaderboard_user_element.xml
Normal 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>
|
||||
|
|
@ -8,6 +8,7 @@ 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.utils.ViewUtilWrapper
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.Single
|
||||
import media
|
||||
import org.junit.Before
|
||||
|
|
@ -56,7 +57,7 @@ class ReasonBuilderTest {
|
|||
`when`(okHttpJsonApiClient!!.getAchievements(anyString()))
|
||||
.thenReturn(Single.just(mock(FeedbackResponse::class.java)))
|
||||
`when`(okHttpJsonApiClient!!.getLeaderboard(anyString(), anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(Single.just(mock(LeaderboardResponse::class.java)))
|
||||
.thenReturn(Observable.just(mock(LeaderboardResponse::class.java)))
|
||||
|
||||
val media = media(filename="test_file", dateUploaded = Date())
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue