mirror of
				https://github.com/commons-app/apps-android-commons.git
				synced 2025-10-31 14:53:59 +01:00 
			
		
		
		
	Revert "Converted AchievementsFragment to kotlin"
This reverts commit 4fcbb81e5d.
			
			
This commit is contained in:
		
							parent
							
								
									4fcbb81e5d
								
							
						
					
					
						commit
						9826e2bafa
					
				
					 2 changed files with 492 additions and 493 deletions
				
			
		|  | @ -0,0 +1,492 @@ | |||
| package fr.free.nrw.commons.profile.achievements; | ||||
| 
 | ||||
| import android.accounts.Account; | ||||
| import android.content.Context; | ||||
| import android.net.Uri; | ||||
| import android.os.Bundle; | ||||
| import android.util.DisplayMetrics; | ||||
| import android.view.LayoutInflater; | ||||
| import android.view.View; | ||||
| import android.view.ViewGroup; | ||||
| import android.widget.Toast; | ||||
| import androidx.annotation.Nullable; | ||||
| import androidx.appcompat.view.ContextThemeWrapper; | ||||
| import androidx.constraintlayout.widget.ConstraintLayout; | ||||
| import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; | ||||
| import fr.free.nrw.commons.R; | ||||
| import fr.free.nrw.commons.Utils; | ||||
| import fr.free.nrw.commons.auth.SessionManager; | ||||
| import fr.free.nrw.commons.databinding.FragmentAchievementsBinding; | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore; | ||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient; | ||||
| import fr.free.nrw.commons.utils.ConfigUtils; | ||||
| import fr.free.nrw.commons.utils.DialogUtil; | ||||
| import fr.free.nrw.commons.utils.ViewUtil; | ||||
| import fr.free.nrw.commons.profile.ProfileActivity; | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers; | ||||
| import io.reactivex.disposables.CompositeDisposable; | ||||
| import io.reactivex.schedulers.Schedulers; | ||||
| import java.util.Locale; | ||||
| import java.util.Objects; | ||||
| import javax.inject.Inject; | ||||
| import org.apache.commons.lang3.StringUtils; | ||||
| import timber.log.Timber; | ||||
| 
 | ||||
| /** | ||||
|  * fragment for sharing feedback on uploaded activity | ||||
|  */ | ||||
| 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; | ||||
| 
 | ||||
|     /** | ||||
|      * Help link URLs | ||||
|      */ | ||||
|     private static final String IMAGES_UPLOADED_URL = "https://commons.wikimedia.org/wiki/Commons:Project_scope"; | ||||
|     private static final String IMAGES_REVERT_URL = "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion"; | ||||
|     private static final String IMAGES_USED_URL = "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images"; | ||||
|     private static final String IMAGES_NEARBY_PLACES_URL = "https://www.wikidata.org/wiki/Property:P18"; | ||||
|     private static final String IMAGES_FEATURED_URL = "https://commons.wikimedia.org/wiki/Commons:Featured_pictures"; | ||||
|     private static final String QUALITY_IMAGE_URL = "https://commons.wikimedia.org/wiki/Commons:Quality_images"; | ||||
|     private static final String THANKS_URL = "https://www.mediawiki.org/wiki/Extension:Thanks"; | ||||
| 
 | ||||
|     private LevelController.LevelInfo levelInfo; | ||||
| 
 | ||||
|     @Inject | ||||
|     SessionManager sessionManager; | ||||
| 
 | ||||
|     @Inject | ||||
|     OkHttpJsonApiClient okHttpJsonApiClient; | ||||
| 
 | ||||
|     private FragmentAchievementsBinding binding; | ||||
| 
 | ||||
|     private CompositeDisposable compositeDisposable = new CompositeDisposable(); | ||||
| 
 | ||||
|     // To keep track of the number of wiki edits made by a user | ||||
|     private int numberOfEdits = 0; | ||||
| 
 | ||||
|     private String userName; | ||||
| 
 | ||||
|     @Override | ||||
|     public void onCreate(@Nullable final Bundle savedInstanceState) { | ||||
|         super.onCreate(savedInstanceState); | ||||
|         if (getArguments() != null) { | ||||
|             userName = getArguments().getString(ProfileActivity.KEY_USERNAME); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method helps in the creation Achievement screen and | ||||
|      * dynamically set the size of imageView | ||||
|      * | ||||
|      * @param savedInstanceState Data bundle | ||||
|      */ | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         binding = FragmentAchievementsBinding.inflate(inflater, container, false); | ||||
|         View rootView = binding.getRoot(); | ||||
| 
 | ||||
|         binding.achievementInfo.setOnClickListener(view -> showInfoDialog()); | ||||
|         binding.imagesUploadInfo.setOnClickListener(view -> showUploadInfo()); | ||||
|         binding.imagesRevertedInfo.setOnClickListener(view -> showRevertedInfo()); | ||||
|         binding.imagesUsedByWikiInfo.setOnClickListener(view -> showUsedByWikiInfo()); | ||||
|         binding.imagesNearbyInfo.setOnClickListener(view -> showImagesViaNearbyInfo()); | ||||
|         binding.imagesFeaturedInfo.setOnClickListener(view -> showFeaturedImagesInfo()); | ||||
|         binding.thanksReceivedInfo.setOnClickListener(view -> showThanksReceivedInfo()); | ||||
|         binding.qualityImagesInfo.setOnClickListener(view -> showQualityImagesInfo()); | ||||
| 
 | ||||
|         // DisplayMetrics used to fetch the size of the screen | ||||
|         DisplayMetrics displayMetrics = new DisplayMetrics(); | ||||
|         getActivity().getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); | ||||
|         int height = displayMetrics.heightPixels; | ||||
|         int width = displayMetrics.widthPixels; | ||||
| 
 | ||||
|         // Used for the setting the size of imageView at runtime | ||||
|         ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) | ||||
|             binding.achievementBadgeImage.getLayoutParams(); | ||||
|         params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); | ||||
|         params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); | ||||
|         binding.achievementBadgeImage.requestLayout(); | ||||
|         binding.progressBar.setVisibility(View.VISIBLE); | ||||
| 
 | ||||
|         setHasOptionsMenu(true); | ||||
| 
 | ||||
|         // Set the initial value of WikiData edits to 0 | ||||
|         binding.wikidataEdits.setText("0"); | ||||
|         if(sessionManager.getUserName() == null || sessionManager.getUserName().equals(userName)){ | ||||
|             binding.tvAchievementsOfUser.setVisibility(View.GONE); | ||||
|         }else{ | ||||
|             binding.tvAchievementsOfUser.setVisibility(View.VISIBLE); | ||||
|             binding.tvAchievementsOfUser.setText(getString(R.string.achievements_of_user,userName)); | ||||
|         } | ||||
| 
 | ||||
|         // Achievements currently unimplemented in Beta flavor. Skip all API calls. | ||||
|         if(ConfigUtils.isBetaFlavour()) { | ||||
|             binding.progressBar.setVisibility(View.GONE); | ||||
|             binding.imagesUsedByWikiText.setText(R.string.no_image); | ||||
|             binding.imagesRevertedText.setText(R.string.no_image_reverted); | ||||
|             binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); | ||||
|             binding.wikidataEdits.setText("0"); | ||||
|             binding.imageFeatured.setText("0"); | ||||
|             binding.qualityImages.setText("0"); | ||||
|             binding.achievementLevel.setText("0"); | ||||
|             setMenuVisibility(true); | ||||
|             return rootView; | ||||
|         } | ||||
|         setWikidataEditCount(); | ||||
|         setAchievements(); | ||||
|         return rootView; | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void onDestroyView() { | ||||
|         binding = null; | ||||
|         super.onDestroyView(); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public void setMenuVisibility(boolean visible) { | ||||
|         super.setMenuVisibility(visible); | ||||
| 
 | ||||
|         // Whenever this fragment is revealed in a menu, | ||||
|         // notify Beta users the page data is unavailable | ||||
|         if(ConfigUtils.isBetaFlavour() && visible) { | ||||
|             Context ctx = null; | ||||
|             if(getContext() != null) { | ||||
|                 ctx = getContext(); | ||||
|             } else if(getView() != null && getView().getContext() != null) { | ||||
|                 ctx = getView().getContext(); | ||||
|             } | ||||
|             if(ctx != null) { | ||||
|                 Toast.makeText(ctx, | ||||
|                     R.string.achievements_unavailable_beta, | ||||
|                     Toast.LENGTH_LONG).show(); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * To invoke the AlertDialog on clicking info button | ||||
|      */ | ||||
|     protected 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 | ||||
|      */ | ||||
|     private void setAchievements() { | ||||
|         binding.progressBar.setVisibility(View.VISIBLE); | ||||
|         if (checkAccount()) { | ||||
|             try{ | ||||
| 
 | ||||
|                 compositeDisposable.add(okHttpJsonApiClient | ||||
|                     .getAchievements(Objects.requireNonNull(userName)) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe( | ||||
|                         response -> { | ||||
|                             if (response != null) { | ||||
|                                 setUploadCount(Achievements.from(response)); | ||||
|                             } else { | ||||
|                                 Timber.d("success"); | ||||
|                                 binding.layoutImageReverts.setVisibility(View.INVISIBLE); | ||||
|                                 binding.achievementBadgeImage.setVisibility(View.INVISIBLE); | ||||
|                                 // If the number of edits made by the user are more than 150,000 | ||||
|                                 // in some cases such high number of wiki edit counts cause the | ||||
|                                 // achievements calculator to fail in some cases, for more details | ||||
|                                 // refer Issue: #3295 | ||||
|                                 if (numberOfEdits <= 150000) { | ||||
|                                     showSnackBarWithRetry(false); | ||||
|                                 } else { | ||||
|                                     showSnackBarWithRetry(true); | ||||
|                                 } | ||||
|                             } | ||||
|                         }, | ||||
|                         t -> { | ||||
|                             Timber.e(t, "Fetching achievements statistics failed"); | ||||
|                             if (numberOfEdits <= 150000) { | ||||
|                                 showSnackBarWithRetry(false); | ||||
|                             } else { | ||||
|                                 showSnackBarWithRetry(true); | ||||
|                             } | ||||
|                         } | ||||
|                     )); | ||||
|             } | ||||
|             catch (Exception e){ | ||||
|                 Timber.d(e+"success"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * To call the API to fetch the count of wiki data edits | ||||
|      *  in the form of JavaRx Single object<JSONobject> | ||||
|      */ | ||||
|     private void setWikidataEditCount() { | ||||
|         if (StringUtils.isBlank(userName)) { | ||||
|             return; | ||||
|         } | ||||
|         compositeDisposable.add(okHttpJsonApiClient | ||||
|             .getWikidataEdits(userName) | ||||
|             .subscribeOn(Schedulers.io()) | ||||
|             .observeOn(AndroidSchedulers.mainThread()) | ||||
|             .subscribe(edits -> { | ||||
|                 numberOfEdits = edits; | ||||
|                 binding.wikidataEdits.setText(String.valueOf(edits)); | ||||
|             }, e -> { | ||||
|                 Timber.e("Error:" + e); | ||||
|             })); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the | ||||
|      * listener passed | ||||
|      * @param tooManyAchievements if this value is true it means that the number of achievements of the | ||||
|      * user are so high that it wrecks havoc with the Achievements calculator due to which request may time | ||||
|      * out. Well this is the Ultimate Achievement | ||||
|      */ | ||||
|     private void showSnackBarWithRetry(boolean tooManyAchievements) { | ||||
|         if (tooManyAchievements) { | ||||
|             binding.progressBar.setVisibility(View.GONE); | ||||
|             ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), | ||||
|                 R.string.achievements_fetch_failed_ultimate_achievement, R.string.retry, view -> setAchievements()); | ||||
|         } else { | ||||
|             binding.progressBar.setVisibility(View.GONE); | ||||
|             ViewUtil.showDismissibleSnackBar(getActivity().findViewById(android.R.id.content), | ||||
|                 R.string.achievements_fetch_failed, R.string.retry, view -> setAchievements()); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a generic error toast when error occurs while loading achievements or uploads | ||||
|      */ | ||||
|     private void onError() { | ||||
|         ViewUtil.showLongToast(getActivity(), getResources().getString(R.string.error_occurred)); | ||||
|         binding.progressBar.setVisibility(View.GONE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * used to the count of images uploaded by user | ||||
|      */ | ||||
|     private void setUploadCount(Achievements achievements) { | ||||
|         if (checkAccount()) { | ||||
|             compositeDisposable.add(okHttpJsonApiClient | ||||
|                 .getUploadCount(Objects.requireNonNull(userName)) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe( | ||||
|                     uploadCount -> setAchievementsUploadCount(achievements, uploadCount), | ||||
|                     t -> { | ||||
|                         Timber.e(t, "Fetching upload count failed"); | ||||
|                         onError(); | ||||
|                     } | ||||
|                 )); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * used to set achievements upload count and call hideProgressbar | ||||
|      * @param uploadCount | ||||
|      */ | ||||
|     private void setAchievementsUploadCount(Achievements achievements, int uploadCount) { | ||||
|         // Create a new instance of Achievements with updated imagesUploaded | ||||
|         Achievements updatedAchievements = new Achievements( | ||||
|             achievements.getUniqueUsedImages(), | ||||
|             achievements.getArticlesUsingImages(), | ||||
|             achievements.getThanksReceived(), | ||||
|             achievements.getFeaturedImages(), | ||||
|             achievements.getQualityImages(), | ||||
|             uploadCount,  // Update imagesUploaded with new value | ||||
|             achievements.getRevertCount() | ||||
|         ); | ||||
| 
 | ||||
|         hideProgressBar(updatedAchievements); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * used to the uploaded images progressbar | ||||
|      * @param uploadCount | ||||
|      */ | ||||
|     private void setUploadProgress(int uploadCount){ | ||||
|         if (uploadCount==0){ | ||||
|             setZeroAchievements(); | ||||
|         }else { | ||||
|             binding.imagesUploadedProgressbar.setVisibility(View.VISIBLE); | ||||
|             binding.imagesUploadedProgressbar.setProgress | ||||
|                 (100*uploadCount/levelInfo.getMaxUploadCount()); | ||||
|             binding.tvUploadedImages.setText | ||||
|                 (uploadCount + "/" + levelInfo.getMaxUploadCount()); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     private void setZeroAchievements() { | ||||
|         String message = !Objects.equals(sessionManager.getUserName(), userName) ? | ||||
|             getString(R.string.no_achievements_yet, userName) : | ||||
|             getString(R.string.you_have_no_achievements_yet); | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             null, | ||||
|             message, | ||||
|             getString(R.string.ok), | ||||
|             () -> {}, | ||||
|             true); | ||||
| //        binding.imagesUploadedProgressbar.setVisibility(View.INVISIBLE); | ||||
| //        binding.imageRevertsProgressbar.setVisibility(View.INVISIBLE); | ||||
| //        binding.imagesUsedByWikiProgressBar.setVisibility(View.INVISIBLE); | ||||
|         binding.achievementBadgeImage.setVisibility(View.INVISIBLE); | ||||
|         binding.imagesUsedByWikiText.setText(R.string.no_image); | ||||
|         binding.imagesRevertedText.setText(R.string.no_image_reverted); | ||||
|         binding.imagesUploadTextParam.setText(R.string.no_image_uploaded); | ||||
|         binding.achievementBadgeImage.setVisibility(View.INVISIBLE); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * used to set the non revert image percentage | ||||
|      * @param notRevertPercentage | ||||
|      */ | ||||
|     private void setImageRevertPercentage(int notRevertPercentage){ | ||||
|         binding.imageRevertsProgressbar.setVisibility(View.VISIBLE); | ||||
|         binding.imageRevertsProgressbar.setProgress(notRevertPercentage); | ||||
|         final String revertPercentage = Integer.toString(notRevertPercentage); | ||||
|         binding.tvRevertedImages.setText(revertPercentage + "%"); | ||||
|         binding.imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used the inflate the fetched statistics of the images uploaded by user | ||||
|      * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu | ||||
|      * @param achievements | ||||
|      */ | ||||
|     private void inflateAchievements(Achievements achievements) { | ||||
| //        binding.imagesUsedByWikiProgressBar.setVisibility(View.VISIBLE); | ||||
|         binding.thanksReceived.setText(String.valueOf(achievements.getThanksReceived())); | ||||
|         binding.imagesUsedByWikiProgressBar.setProgress | ||||
|             (100 * achievements.getUniqueUsedImages() / levelInfo.getMaxUniqueImages()); | ||||
|         binding.tvWikiPb.setText(achievements.getUniqueUsedImages() + "/" | ||||
|             + levelInfo.getMaxUniqueImages()); | ||||
|         binding.imageFeatured.setText(String.valueOf(achievements.getFeaturedImages())); | ||||
|         binding.qualityImages.setText(String.valueOf(achievements.getQualityImages())); | ||||
|         String levelUpInfoString = getString(R.string.level).toUpperCase(Locale.ROOT); | ||||
|         levelUpInfoString += " " + levelInfo.getLevelNumber(); | ||||
|         binding.achievementLevel.setText(levelUpInfoString); | ||||
|         binding.achievementBadgeImage.setImageDrawable(VectorDrawableCompat.create(getResources(), R.drawable.badge, | ||||
|             new ContextThemeWrapper(getActivity(), levelInfo.getLevelStyle()).getTheme())); | ||||
|         binding.achievementBadgeText.setText(Integer.toString(levelInfo.getLevelNumber())); | ||||
|         BasicKvStore store = new BasicKvStore(this.getContext(), userName); | ||||
|         store.putString("userAchievementsLevel", Integer.toString(levelInfo.getLevelNumber())); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * to hide progressbar | ||||
|      */ | ||||
|     private void hideProgressBar(Achievements achievements) { | ||||
|         if (binding.progressBar != null) { | ||||
|             levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), | ||||
|                 achievements.getUniqueUsedImages(), | ||||
|                 achievements.getNotRevertPercentage()); | ||||
|             inflateAchievements(achievements); | ||||
|             setUploadProgress(achievements.getImagesUploaded()); | ||||
|             setImageRevertPercentage(achievements.getNotRevertPercentage()); | ||||
|             binding.progressBar.setVisibility(View.GONE); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     protected void showUploadInfo(){ | ||||
|         launchAlertWithHelpLink( | ||||
|             getResources().getString(R.string.images_uploaded), | ||||
|             getResources().getString(R.string.images_uploaded_explanation), | ||||
|             IMAGES_UPLOADED_URL); | ||||
|     } | ||||
| 
 | ||||
|     protected void showRevertedInfo(){ | ||||
|         launchAlertWithHelpLink( | ||||
|             getResources().getString(R.string.image_reverts), | ||||
|             getResources().getString(R.string.images_reverted_explanation), | ||||
|             IMAGES_REVERT_URL); | ||||
|     } | ||||
| 
 | ||||
|     protected void showUsedByWikiInfo(){ | ||||
|         launchAlertWithHelpLink( | ||||
|             getResources().getString(R.string.images_used_by_wiki), | ||||
|             getResources().getString(R.string.images_used_explanation), | ||||
|             IMAGES_USED_URL); | ||||
|     } | ||||
| 
 | ||||
|     protected void showImagesViaNearbyInfo(){ | ||||
|         launchAlertWithHelpLink( | ||||
|             getResources().getString(R.string.statistics_wikidata_edits), | ||||
|             getResources().getString(R.string.images_via_nearby_explanation), | ||||
|             IMAGES_NEARBY_PLACES_URL); | ||||
|     } | ||||
| 
 | ||||
|     protected void showFeaturedImagesInfo(){ | ||||
|         launchAlertWithHelpLink( | ||||
|             getResources().getString(R.string.statistics_featured), | ||||
|             getResources().getString(R.string.images_featured_explanation), | ||||
|             IMAGES_FEATURED_URL); | ||||
|     } | ||||
| 
 | ||||
|     protected void showThanksReceivedInfo(){ | ||||
|         launchAlertWithHelpLink( | ||||
|             getResources().getString(R.string.statistics_thanks), | ||||
|             getResources().getString(R.string.thanks_received_explanation), | ||||
|             THANKS_URL); | ||||
|     } | ||||
| 
 | ||||
|     public void showQualityImagesInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             getResources().getString(R.string.statistics_quality), | ||||
|             getResources().getString(R.string.quality_images_info), | ||||
|             QUALITY_IMAGE_URL); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * takes title and message as input to display alerts | ||||
|      * @param title | ||||
|      * @param message | ||||
|      */ | ||||
|     private void launchAlert(String title, String message){ | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             title, | ||||
|             message, | ||||
|             getString(R.string.ok), | ||||
|             () -> {}, | ||||
|             true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      *  Launch Alert with a READ MORE button and clicking it open a custom webpage | ||||
|      */ | ||||
|     private void launchAlertWithHelpLink(String title, String message, String helpLinkUrl) { | ||||
|         DialogUtil.showAlertDialog(getActivity(), | ||||
|             title, | ||||
|             message, | ||||
|             getString(R.string.ok), | ||||
|             getString(R.string.read_help_link), | ||||
|             () -> {}, | ||||
|             () -> Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)), | ||||
|             null, | ||||
|             true); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * 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; | ||||
|     } | ||||
| } | ||||
|  | @ -1,493 +0,0 @@ | |||
| package fr.free.nrw.commons.profile.achievements | ||||
| 
 | ||||
| import android.content.Context | ||||
| import android.net.Uri | ||||
| import android.os.Bundle | ||||
| import android.util.DisplayMetrics | ||||
| import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import android.view.ViewGroup | ||||
| import android.widget.Toast | ||||
| import androidx.annotation.VisibleForTesting | ||||
| import androidx.appcompat.view.ContextThemeWrapper | ||||
| import androidx.constraintlayout.widget.ConstraintLayout | ||||
| import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat | ||||
| import fr.free.nrw.commons.R | ||||
| import fr.free.nrw.commons.Utils | ||||
| import fr.free.nrw.commons.auth.SessionManager | ||||
| import fr.free.nrw.commons.databinding.FragmentAchievementsBinding | ||||
| import fr.free.nrw.commons.di.CommonsDaggerSupportFragment | ||||
| import fr.free.nrw.commons.kvstore.BasicKvStore | ||||
| import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient | ||||
| import fr.free.nrw.commons.profile.ProfileActivity | ||||
| import fr.free.nrw.commons.profile.achievements.Achievements.Companion.from | ||||
| import fr.free.nrw.commons.profile.achievements.LevelController.LevelInfo | ||||
| import fr.free.nrw.commons.profile.achievements.LevelController.LevelInfo.Companion.from | ||||
| import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour | ||||
| import fr.free.nrw.commons.utils.DialogUtil.showAlertDialog | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showDismissibleSnackBar | ||||
| import fr.free.nrw.commons.utils.ViewUtil.showLongToast | ||||
| import io.reactivex.android.schedulers.AndroidSchedulers | ||||
| import io.reactivex.disposables.CompositeDisposable | ||||
| import io.reactivex.schedulers.Schedulers | ||||
| import org.apache.commons.lang3.StringUtils | ||||
| import timber.log.Timber | ||||
| import javax.inject.Inject | ||||
| 
 | ||||
| /** | ||||
|  * fragment for sharing feedback on uploaded activity | ||||
|  */ | ||||
| class AchievementsFragment : CommonsDaggerSupportFragment() { | ||||
|     @Inject | ||||
|     lateinit var sessionManager: SessionManager | ||||
| 
 | ||||
|     @Inject | ||||
|     lateinit var okHttpJsonApiClient: OkHttpJsonApiClient | ||||
| 
 | ||||
|     private var levelInfo: LevelInfo? = null | ||||
|     private var binding: FragmentAchievementsBinding? = null | ||||
|     private val compositeDisposable = CompositeDisposable() | ||||
| 
 | ||||
|     // To keep track of the number of wiki edits made by a user | ||||
|     private var numberOfEdits = 0 | ||||
|     private var userName: String? = null | ||||
| 
 | ||||
|     override fun onCreate(savedInstanceState: Bundle?) { | ||||
|         super.onCreate(savedInstanceState) | ||||
|         if (arguments != null) { | ||||
|             userName = arguments!!.getString(ProfileActivity.KEY_USERNAME) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method helps in the creation Achievement screen and | ||||
|      * dynamically set the size of imageView | ||||
|      * | ||||
|      * @param savedInstanceState Data bundle | ||||
|      */ | ||||
|     override fun onCreateView( | ||||
|         inflater: LayoutInflater, | ||||
|         container: ViewGroup?, | ||||
|         savedInstanceState: Bundle? | ||||
|     ): View { | ||||
|         binding = FragmentAchievementsBinding.inflate(inflater, container, false) | ||||
|         val rootView: View = binding!!.root | ||||
| 
 | ||||
|         binding!!.achievementInfo.setOnClickListener { showInfoDialog() } | ||||
|         binding!!.imagesUploadInfo.setOnClickListener { showUploadInfo() } | ||||
|         binding!!.imagesRevertedInfo.setOnClickListener { showRevertedInfo() } | ||||
|         binding!!.imagesUsedByWikiInfo.setOnClickListener { showUsedByWikiInfo() } | ||||
|         binding!!.imagesNearbyInfo.setOnClickListener { showImagesViaNearbyInfo() } | ||||
|         binding!!.imagesFeaturedInfo.setOnClickListener { showFeaturedImagesInfo() } | ||||
|         binding!!.thanksReceivedInfo.setOnClickListener { showThanksReceivedInfo() } | ||||
|         binding!!.qualityImagesInfo.setOnClickListener { showQualityImagesInfo() } | ||||
| 
 | ||||
|         // DisplayMetrics used to fetch the size of the screen | ||||
|         val displayMetrics = DisplayMetrics() | ||||
|         requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) | ||||
|         val height = displayMetrics.heightPixels | ||||
|         val width = displayMetrics.widthPixels | ||||
| 
 | ||||
|         // Used for the setting the size of imageView at runtime | ||||
|         val params = binding!!.achievementBadgeImage.layoutParams as ConstraintLayout.LayoutParams | ||||
|         params.height = (height * BADGE_IMAGE_HEIGHT_RATIO).toInt() | ||||
|         params.width = (width * BADGE_IMAGE_WIDTH_RATIO).toInt() | ||||
|         binding!!.achievementBadgeImage.requestLayout() | ||||
|         binding!!.progressBar.visibility = View.VISIBLE | ||||
| 
 | ||||
|         setHasOptionsMenu(true) | ||||
| 
 | ||||
|         // Set the initial value of WikiData edits to 0 | ||||
|         binding!!.wikidataEdits.text = "0" | ||||
|         if (sessionManager.userName == null || sessionManager.userName == userName) { | ||||
|             binding!!.tvAchievementsOfUser.visibility = View.GONE | ||||
|         } else { | ||||
|             binding!!.tvAchievementsOfUser.visibility = View.VISIBLE | ||||
|             binding!!.tvAchievementsOfUser.text = | ||||
|                 getString(R.string.achievements_of_user, userName) | ||||
|         } | ||||
| 
 | ||||
|         // Achievements currently unimplemented in Beta flavor. Skip all API calls. | ||||
|         if (isBetaFlavour) { | ||||
|             binding!!.progressBar.visibility = View.GONE | ||||
|             binding!!.imagesUsedByWikiText.setText(R.string.no_image) | ||||
|             binding!!.imagesRevertedText.setText(R.string.no_image_reverted) | ||||
|             binding!!.imagesUploadTextParam.setText(R.string.no_image_uploaded) | ||||
|             binding!!.wikidataEdits.text = "0" | ||||
|             binding!!.imageFeatured.text = "0" | ||||
|             binding!!.qualityImages.text = "0" | ||||
|             binding!!.achievementLevel.text = "0" | ||||
|             setMenuVisibility(true) | ||||
|             return rootView | ||||
|         } | ||||
|         setWikidataEditCount() | ||||
|         setAchievements() | ||||
|         return rootView | ||||
|     } | ||||
| 
 | ||||
|     override fun onDestroyView() { | ||||
|         binding = null | ||||
|         super.onDestroyView() | ||||
|     } | ||||
| 
 | ||||
|     override fun setMenuVisibility(visible: Boolean) { | ||||
|         super.setMenuVisibility(visible) | ||||
| 
 | ||||
|         // Whenever this fragment is revealed in a menu, | ||||
|         // notify Beta users the page data is unavailable | ||||
|         if (isBetaFlavour && visible) { | ||||
|             val ctx: Context? = if (context != null) { | ||||
|                 context | ||||
|             } else if (view != null && requireView().context != null) { | ||||
|                 requireView().context | ||||
|             } else { | ||||
|                 null | ||||
|             } | ||||
| 
 | ||||
|             ctx?.let { | ||||
|                 Toast.makeText(it, R.string.achievements_unavailable_beta, Toast.LENGTH_LONG).show() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * To invoke the AlertDialog on clicking info button | ||||
|      */ | ||||
|     @VisibleForTesting | ||||
|     fun showInfoDialog() = launchAlert( | ||||
|         resources.getString(R.string.Achievements), | ||||
|         resources.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 | ||||
|     </JSONObject> */ | ||||
|     private fun setAchievements() { | ||||
|         binding!!.progressBar.visibility = View.VISIBLE | ||||
|         if (checkAccount()) { | ||||
|             try { | ||||
|                 compositeDisposable.add( | ||||
|                     okHttpJsonApiClient.getAchievements(userName) | ||||
|                         .subscribeOn(Schedulers.io()) | ||||
|                         .observeOn(AndroidSchedulers.mainThread()) | ||||
|                         .subscribe({ response: FeedbackResponse? -> | ||||
|                             if (response != null) { | ||||
|                                 setUploadCount(from(response)) | ||||
|                             } else { | ||||
|                                 Timber.d("success") | ||||
|                                 binding!!.layoutImageReverts.visibility = View.INVISIBLE | ||||
|                                 binding!!.achievementBadgeImage.visibility = View.INVISIBLE | ||||
| 
 | ||||
|                                 // If the number of edits made by the user are more than 150,000 | ||||
|                                 // in some cases such high number of wiki edit counts cause the | ||||
|                                 // achievements calculator to fail in some cases, for more details | ||||
|                                 // refer Issue: #3295 | ||||
|                                 if (numberOfEdits <= 150000) { | ||||
|                                     showSnackBarWithRetry(false) | ||||
|                                 } else { | ||||
|                                     showSnackBarWithRetry(true) | ||||
|                                 } | ||||
|                             } | ||||
|                         }, { t: Throwable? -> | ||||
|                             Timber.e(t, "Fetching achievements statistics failed") | ||||
|                             if (numberOfEdits <= 150000) { | ||||
|                                 showSnackBarWithRetry(false) | ||||
|                             } else { | ||||
|                                 showSnackBarWithRetry(true) | ||||
|                             } | ||||
|                         })) | ||||
|             } catch (e: Exception) { | ||||
|                 Timber.d(e, "success") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * To call the API to fetch the count of wiki data edits | ||||
|      * in the form of JavaRx Single object<JSONobject> | ||||
|     </JSONobject> */ | ||||
|     private fun setWikidataEditCount() { | ||||
|         if (StringUtils.isBlank(userName)) { | ||||
|             return | ||||
|         } | ||||
|         compositeDisposable.add( | ||||
|             okHttpJsonApiClient.getWikidataEdits(userName) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe({ edits: Int -> | ||||
|                     numberOfEdits = edits | ||||
|                     binding!!.wikidataEdits.text = edits.toString() | ||||
|                 }, { e: Throwable -> | ||||
|                     Timber.e(e,"Error") | ||||
|                 }) | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a snack bar which has an action button which on click dismisses the snackbar and invokes the | ||||
|      * listener passed | ||||
|      * @param tooManyAchievements if this value is true it means that the number of achievements of the | ||||
|      * user are so high that it wrecks havoc with the Achievements calculator due to which request may time | ||||
|      * out. Well this is the Ultimate Achievement | ||||
|      */ | ||||
|     private fun showSnackBarWithRetry(tooManyAchievements: Boolean) { | ||||
|         binding!!.progressBar.visibility = View.GONE | ||||
|         showDismissibleSnackBar( | ||||
|             view = requireActivity().findViewById(android.R.id.content), | ||||
|             messageResourceId = if (tooManyAchievements) { | ||||
|                 R.string.achievements_fetch_failed_ultimate_achievement | ||||
|             } else { | ||||
|                 R.string.achievements_fetch_failed | ||||
|             }, | ||||
|             actionButtonResourceId = R.string.retry | ||||
|         ) { setAchievements() } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Shows a generic error toast when error occurs while loading achievements or uploads | ||||
|      */ | ||||
|     private fun onError() { | ||||
|         showLongToast(requireActivity(), resources.getString(R.string.error_occurred)) | ||||
|         binding!!.progressBar.visibility = View.GONE | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * used to the count of images uploaded by user | ||||
|      */ | ||||
|     private fun setUploadCount(achievements: Achievements) { | ||||
|         if (checkAccount()) { | ||||
|             compositeDisposable.add( | ||||
|                 okHttpJsonApiClient.getUploadCount(userName) | ||||
|                     .subscribeOn(Schedulers.io()) | ||||
|                     .observeOn(AndroidSchedulers.mainThread()) | ||||
|                     .subscribe( | ||||
|                         { uploadCount: Int -> | ||||
|                             setAchievementsUploadCount(achievements, uploadCount) | ||||
|                         }, | ||||
|                         { t: Throwable? -> | ||||
|                             Timber.e(t, "Fetching upload count failed") | ||||
|                             onError() | ||||
|                         } | ||||
|                     )) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * used to set achievements upload count and call hideProgressbar | ||||
|      * @param uploadCount | ||||
|      */ | ||||
|     private fun setAchievementsUploadCount(achievements: Achievements, uploadCount: Int) = | ||||
|         hideProgressBar(achievements.copy(imagesUploaded = uploadCount)) | ||||
| 
 | ||||
|     /** | ||||
|      * used to the uploaded images progressbar | ||||
|      * @param uploadCount | ||||
|      */ | ||||
|     private fun setUploadProgress(uploadCount: Int) { | ||||
|         if (uploadCount == 0) { | ||||
|             setZeroAchievements() | ||||
|         } else { | ||||
|             binding!!.imagesUploadedProgressbar.visibility = View.VISIBLE | ||||
|             binding!!.imagesUploadedProgressbar.progress = | ||||
|                 100 * uploadCount / levelInfo!!.maxUploadCount | ||||
|             binding!!.tvUploadedImages.text = | ||||
|                 uploadCount.toString() + "/" + levelInfo!!.maxUploadCount | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun setZeroAchievements() { | ||||
|         val message = if (sessionManager.userName != userName) getString( | ||||
|             R.string.no_achievements_yet, | ||||
|             userName | ||||
|         ) else getString( | ||||
|             R.string.you_have_no_achievements_yet | ||||
|         ) | ||||
|         showAlertDialog(requireActivity(), null, message, getString(R.string.ok), {}, true) | ||||
|         binding!!.achievementBadgeImage.visibility = View.INVISIBLE | ||||
|         binding!!.imagesUsedByWikiText.setText(R.string.no_image) | ||||
|         binding!!.imagesRevertedText.setText(R.string.no_image_reverted) | ||||
|         binding!!.imagesUploadTextParam.setText(R.string.no_image_uploaded) | ||||
|         binding!!.achievementBadgeImage.visibility = View.INVISIBLE | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * used to set the non revert image percentage | ||||
|      * @param notRevertPercentage | ||||
|      */ | ||||
|     private fun setImageRevertPercentage(notRevertPercentage: Int) { | ||||
|         binding!!.imageRevertsProgressbar.visibility = View.VISIBLE | ||||
|         binding!!.imageRevertsProgressbar.progress = notRevertPercentage | ||||
|         val revertPercentage = notRevertPercentage.toString() | ||||
|         binding!!.tvRevertedImages.text = "$revertPercentage%" | ||||
|         binding!!.imagesRevertLimitText.text = | ||||
|             resources.getString(R.string.achievements_revert_limit_message) + levelInfo!!.minNonRevertPercentage + "%" | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Used the inflate the fetched statistics of the images uploaded by user | ||||
|      * and assign badge and level. Also stores the achievements level of the user in BasicKvStore to display in menu | ||||
|      * @param achievements | ||||
|      */ | ||||
|     private fun inflateAchievements(achievements: Achievements) = with(binding!!) { | ||||
|         thanksReceived.text = achievements.thanksReceived.toString() | ||||
|         imagesUsedByWikiProgressBar.progress = | ||||
|             100 * achievements.uniqueUsedImages / levelInfo!!.maxUniqueImages | ||||
|         tvWikiPb.text = (achievements.uniqueUsedImages.toString() + "/" | ||||
|                 + levelInfo!!.maxUniqueImages) | ||||
|         imageFeatured.text = achievements.featuredImages.toString() | ||||
|         qualityImages.text = achievements.qualityImages.toString() | ||||
|         var levelUpInfoString = getString(R.string.level).uppercase() | ||||
|         levelUpInfoString += " " + levelInfo!!.levelNumber | ||||
|         achievementLevel.text = levelUpInfoString | ||||
|         achievementBadgeImage.setImageDrawable( | ||||
|             VectorDrawableCompat.create( | ||||
|                 resources, R.drawable.badge, | ||||
|                 ContextThemeWrapper(activity, levelInfo!!.levelStyle).theme | ||||
|             ) | ||||
|         ) | ||||
|         achievementBadgeText.text = levelInfo!!.levelNumber.toString() | ||||
|         val store = BasicKvStore(requireContext(), userName) | ||||
|         store.putString("userAchievementsLevel", levelInfo!!.levelNumber.toString()) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * to hide progressbar | ||||
|      */ | ||||
|     private fun hideProgressBar(achievements: Achievements) { | ||||
|         if (binding?.progressBar != null) { | ||||
|             levelInfo = from( | ||||
|                 achievements.imagesUploaded, | ||||
|                 achievements.uniqueUsedImages, | ||||
|                 achievements.notRevertPercentage | ||||
|             ) | ||||
|             inflateAchievements(achievements) | ||||
|             setUploadProgress(achievements.imagesUploaded) | ||||
|             setImageRevertPercentage(achievements.notRevertPercentage) | ||||
|             binding!!.progressBar.visibility = View.GONE | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun showUploadInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             resources.getString(R.string.images_uploaded), | ||||
|             resources.getString(R.string.images_uploaded_explanation), | ||||
|             IMAGES_UPLOADED_URL | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun showRevertedInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             resources.getString(R.string.image_reverts), | ||||
|             resources.getString(R.string.images_reverted_explanation), | ||||
|             IMAGES_REVERT_URL | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun showUsedByWikiInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             resources.getString(R.string.images_used_by_wiki), | ||||
|             resources.getString(R.string.images_used_explanation), | ||||
|             IMAGES_USED_URL | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun showImagesViaNearbyInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             resources.getString(R.string.statistics_wikidata_edits), | ||||
|             resources.getString(R.string.images_via_nearby_explanation), | ||||
|             IMAGES_NEARBY_PLACES_URL | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun showFeaturedImagesInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             resources.getString(R.string.statistics_featured), | ||||
|             resources.getString(R.string.images_featured_explanation), | ||||
|             IMAGES_FEATURED_URL | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun showThanksReceivedInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             resources.getString(R.string.statistics_thanks), | ||||
|             resources.getString(R.string.thanks_received_explanation), | ||||
|             THANKS_URL | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     @VisibleForTesting | ||||
|     fun showQualityImagesInfo() { | ||||
|         launchAlertWithHelpLink( | ||||
|             resources.getString(R.string.statistics_quality), | ||||
|             resources.getString(R.string.quality_images_info), | ||||
|             QUALITY_IMAGE_URL | ||||
|         ) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * takes title and message as input to display alerts | ||||
|      * @param title | ||||
|      * @param message | ||||
|      */ | ||||
|     private fun launchAlert(title: String, message: String) = | ||||
|         showAlertDialog(requireActivity(), title, message, getString(R.string.ok), {}, true) | ||||
| 
 | ||||
|     /** | ||||
|      * Launch Alert with a READ MORE button and clicking it open a custom webpage | ||||
|      */ | ||||
|     private fun launchAlertWithHelpLink(title: String, message: String, helpLinkUrl: String) = | ||||
|         showAlertDialog( | ||||
|             requireActivity(), title, message, | ||||
|             getString(R.string.ok), | ||||
|             getString(R.string.read_help_link), | ||||
|             {}, | ||||
|             { Utils.handleWebUrl(requireContext(), Uri.parse(helpLinkUrl)) }, | ||||
|             null, | ||||
|             true | ||||
|         ) | ||||
| 
 | ||||
|     /** | ||||
|      * check to ensure that user is logged in | ||||
|      * @return | ||||
|      */ | ||||
|     private fun checkAccount(): Boolean { | ||||
|         val currentAccount = sessionManager.currentAccount | ||||
|         if (currentAccount == null) { | ||||
|             Timber.d("Current account is null") | ||||
|             showLongToast(requireActivity(), resources.getString(R.string.user_not_logged_in)) | ||||
|             sessionManager.forceLogin(activity) | ||||
|             return false | ||||
|         } | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val BADGE_IMAGE_WIDTH_RATIO = 0.4 | ||||
|         private const val BADGE_IMAGE_HEIGHT_RATIO = 0.3 | ||||
| 
 | ||||
|         /** | ||||
|          * Help link URLs | ||||
|          */ | ||||
|         private const val IMAGES_UPLOADED_URL = | ||||
|             "https://commons.wikimedia.org/wiki/Commons:Project_scope" | ||||
|         private const val IMAGES_REVERT_URL = | ||||
|             "https://commons.wikimedia.org/wiki/Commons:Deletion_policy#Reasons_for_deletion" | ||||
|         private const val IMAGES_USED_URL = | ||||
|             "https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images" | ||||
|         private const val IMAGES_NEARBY_PLACES_URL = | ||||
|             "https://www.wikidata.org/wiki/Property:P18" | ||||
|         private const val IMAGES_FEATURED_URL = | ||||
|             "https://commons.wikimedia.org/wiki/Commons:Featured_pictures" | ||||
|         private const val QUALITY_IMAGE_URL = | ||||
|             "https://commons.wikimedia.org/wiki/Commons:Quality_images" | ||||
|         private const val THANKS_URL = | ||||
|             "https://www.mediawiki.org/wiki/Extension:Thanks" | ||||
|     } | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Paul Hawke
						Paul Hawke