diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 0f1feeac7..caa02a103 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,15 +5,30 @@ If you're not sure where to start head on to [this wiki page](https://github.com
Here's a gist of the guidelines,
-# Make separate commits for logically separate changes
+1. Make separate commits for logically separate changes
-# Describe your changes well in the commit message
+1. Describe your changes well in the commit message
-The first line of the commit message should be a short description of what has
+ The first line of the commit message should be a short description of what has
changed. It is also good to prefix the first line with "area: " where the "area"
is a filename or identifier for the general area of the code being modified.
The body should provide a meaningful commit message.
-# Write tests for your code (if possible)
+1. Write Javadocs
-# Make sure the Wiki pages don't become stale by updating them (if needed)
+ We require contributors to include Javadocs for all new methods and classes
+ submitted via PRs (after 1 May 2018). This is aimed at making it easier for
+ new contributors to dive into our codebase, especially those who are new to
+ Android development. A few things to note:
+
+ - This should not replace the need for code that is easily-readable in
+ and of itself
+ - Please make sure that your Javadocs are reasonably descriptive, not just
+ a copy of the method name
+ - Please do not use `@author` tags - we aim for collective code ownership,
+ and if needed, Git allows us to see who wrote something without needing
+ to add these tags (`git blame`)
+
+1. Write tests for your code (if possible)
+
+1. Make sure the Wiki pages don't become stale by updating them (if needed)
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
index 34078f07e..9d7150008 100644
--- a/PULL_REQUEST_TEMPLATE.md
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -1,15 +1,15 @@
-## Description
+## Description (required)
-Fixes #{GitHub issue number}
+Fixes #{GitHub issue number and title}
{Describe the changes made and why they were made.}
-## Tests performed
+## Tests performed (required)
Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdDebug}.
-{Please test your PR at least once before submitting.}
-
-## Screenshots showing what changed
+## Screenshots showing what changed (optional)
{Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)}
+
+_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._
diff --git a/app/build.gradle b/app/build.gradle
index 535f58143..5d37f8f54 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -25,6 +25,8 @@ dependencies {
transitive=true
}
+ implementation "com.github.deano2390:MaterialShowcaseView:1.2.0"
+
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
implementation "com.android.support:design:$SUPPORT_LIB_VERSION"
@@ -49,6 +51,8 @@ dependencies {
implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
+ implementation 'org.jsoup:jsoup:1.11.3'
+
implementation 'com.facebook.fresco:fresco:1.5.0'
implementation 'com.facebook.stetho:stetho:1.5.0'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6aab09b55..17f6770d2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -92,8 +92,9 @@
android:label="@string/navigation_item_notification" />
+ android:name=".category.CategoryImagesActivity"
+ android:label="@string/title_activity_featured_images"
+ android:parentActivityName=".contributions.ContributionsActivity" />
diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
index e6bf34736..65cba7531 100644
--- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java
@@ -135,9 +135,10 @@ public class AboutActivity extends NavigationBaseActivity {
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.share_app_icon:
+ String shareText = "Upload photos to Wikimedia Commons on your phone\nDownload the Commons app: http://play.google.com/store/apps/details?id=fr.free.nrw.commons";
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
- sendIntent.putExtra(Intent.EXTRA_TEXT, "http://play.google.com/store/apps/details?id=fr.free.nrw.commons");
+ sendIntent.putExtra(Intent.EXTRA_TEXT, shareText);
sendIntent.setType("text/plain");
startActivity(Intent.createChooser(sendIntent, "Share app via..."));
return true;
diff --git a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
index 2d79a6c4f..affb57528 100644
--- a/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
+++ b/app/src/main/java/fr/free/nrw/commons/MediaDataExtractor.java
@@ -61,8 +61,8 @@ public class MediaDataExtractor {
}
try{
- Timber.d("Nominated for deletion: " + mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename));
- deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/"+filename);
+ deletionStatus = mediaWikiApi.pageExists("Commons:Deletion_requests/" + filename);
+ Timber.d("Nominated for deletion: " + deletionStatus);
}
catch (Exception e){
Timber.d(e.getMessage());
diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
index 7b2b0a97f..d0fb628e3 100644
--- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java
@@ -47,6 +47,7 @@ import fr.free.nrw.commons.di.ApplicationlessInjection;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.theme.NavigationBaseActivity;
import fr.free.nrw.commons.ui.widget.HtmlTextView;
+import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@@ -109,14 +110,14 @@ public class LoginActivity extends AccountAuthenticatorActivity {
usernameEdit.addTextChangedListener(textWatcher);
usernameEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
passwordEdit.addTextChangedListener(textWatcher);
passwordEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
@@ -144,16 +145,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
Utils.handleWebUrl(this,Uri.parse("https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\\"));
}
- public void hideKeyboard(View view) {
- if (view != null) {
- InputMethodManager inputMethodManager = (InputMethodManager) this.getSystemService(Activity.INPUT_METHOD_SERVICE);
- if (inputMethodManager != null) {
- inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
- }
- }
- }
-
-
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java
index 101d4b21e..e804189ab 100644
--- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java
@@ -1,7 +1,6 @@
package fr.free.nrw.commons.category;
-import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.support.v7.app.AlertDialog;
@@ -16,7 +15,6 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.ProgressBar;
import android.widget.TextView;
@@ -43,6 +41,7 @@ import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.upload.MwVolleyApi;
import fr.free.nrw.commons.utils.StringSortingUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.schedulers.Schedulers;
@@ -116,7 +115,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
@@ -128,16 +127,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment {
return rootView;
}
- public void hideKeyboard(View view) {
-
- if (view != null) {
- InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
- if (inputMethodManager != null) {
- inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
- }
- }
- }
-
@Override
public void onDestroyView() {
categoriesFilter.removeTextChangedListener(textWatcher);
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
new file mode 100644
index 000000000..3495d710c
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageController.java
@@ -0,0 +1,29 @@
+package fr.free.nrw.commons.category;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.mwapi.MediaWikiApi;
+
+@Singleton
+public class CategoryImageController {
+
+ private MediaWikiApi mediaWikiApi;
+
+ @Inject
+ public CategoryImageController(MediaWikiApi mediaWikiApi) {
+ this.mediaWikiApi = mediaWikiApi;
+ }
+
+ /**
+ * Takes a category name as input and calls the API to get a list of images for that category
+ * @param categoryName
+ * @return
+ */
+ public List getCategoryImages(String categoryName) {
+ return mediaWikiApi.getCategoryImages(categoryName);
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
new file mode 100644
index 000000000..18749847e
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java
@@ -0,0 +1,225 @@
+package fr.free.nrw.commons.category;
+
+import org.jsoup.Jsoup;
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+import fr.free.nrw.commons.Media;
+import timber.log.Timber;
+
+public class CategoryImageUtils {
+
+ /**
+ * The method iterates over the child nodes to return a list of Media objects
+ * @param childNodes
+ * @return
+ */
+ public static List getMediaList(NodeList childNodes) {
+ List categoryImages = new ArrayList<>();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node node = childNodes.item(i);
+ categoryImages.add(getMediaFromPage(node));
+ }
+
+ return categoryImages;
+ }
+
+ /**
+ * Creates a new Media object from the XML response as received by the API
+ * @param node
+ * @return
+ */
+ private static Media getMediaFromPage(Node node) {
+ Media media = new Media(null,
+ getImageUrl(node),
+ getFileName(node),
+ getDescription(node),
+ getDataLength(node),
+ getDateCreated(node),
+ getDateCreated(node),
+ getCreator(node)
+ );
+
+ media.setLicense(getLicense(node));
+
+ return media;
+ }
+
+ /**
+ * Extracts the filename of the uploaded image
+ * @param document
+ * @return
+ */
+ private static String getFileName(Node document) {
+ Element element = (Element) document;
+ return element.getAttribute("title");
+ }
+
+ /**
+ * Extracts the image description for that particular upload
+ * @param document
+ * @return
+ */
+ private static String getDescription(Node document) {
+ return getMetaDataValue(document, "ImageDescription");
+ }
+
+ /**
+ * Extracts license information from the image meta data
+ * @param document
+ * @return
+ */
+ private static String getLicense(Node document) {
+ return getMetaDataValue(document, "License");
+ }
+
+ /**
+ * Returns the parsed value of artist from the response
+ * The artist information is returned as a HTML string from the API. Jsoup library parses the HTML string
+ * to extract just the text value
+ * @param document
+ * @return
+ */
+ private static String getCreator(Node document) {
+ String artist = getMetaDataValue(document, "Artist");
+ if (artist != null) {
+ return Jsoup.parse(artist).text();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the parsed date of creation of the image
+ * @param document
+ * @return
+ */
+ private static Date getDateCreated(Node document) {
+ String dateTime = getMetaDataValue(document, "DateTime");
+ if (dateTime != null && !dateTime.equals("")) {
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ try {
+ return format.parse(dateTime);
+ } catch (ParseException e) {
+ Timber.d("Error occurred while parsing date %s", dateTime);
+ return new Date();
+ }
+ }
+ return new Date();
+ }
+
+ /**
+ * @param document
+ * @return Returns the url attribute from the imageInfo node
+ */
+ private static String getImageUrl(Node document) {
+ Element element = (Element) getImageInfo(document);
+ if (element != null) {
+ return element.getAttribute("url");
+ }
+ return null;
+ }
+
+ /**
+ * Takes the node document and gives out the attribute length from the node document
+ * @param document
+ * @return
+ */
+ private static long getDataLength(Node document) {
+ Element element = (Element) document;
+ if (element != null) {
+ String length = element.getAttribute("length");
+ if (length != null && !length.equals("")) {
+ return Long.parseLong(length);
+ }
+ }
+ return 0L;
+ }
+
+ /**
+ * Generic method to get the value of any meta as returned by the getMetaData function
+ * @param document node document as returned by API
+ * @param metaName the name of meta node to be returned
+ * @return
+ */
+ private static String getMetaDataValue(Node document, String metaName) {
+ Element metaData = getMetaData(document, metaName);
+ if (metaData != null) {
+ return metaData.getAttribute("value");
+ }
+ return null;
+ }
+
+ /**
+ * Generic method to return an element taking the node document and metaName as input
+ * @param document node document as returned by API
+ * @param metaName the name of meta node to be returned
+ * @return
+ */
+ @Nullable
+ private static Element getMetaData(Node document, String metaName) {
+ Node extraMetaData = getExtraMetaData(document);
+ if (extraMetaData != null) {
+ Node node = getNode(extraMetaData, metaName);
+ if (node != null) {
+ return (Element) node;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Extracts extmetadata from the response XML
+ * @param document
+ * @return
+ */
+ @Nullable
+ private static Node getExtraMetaData(Node document) {
+ Node imageInfo = getImageInfo(document);
+ if (imageInfo != null) {
+ return getNode(imageInfo, "extmetadata");
+ }
+ return null;
+ }
+
+ /**
+ * Extracts the ii node from the imageinfo node
+ * @param document
+ * @return
+ */
+ @Nullable
+ private static Node getImageInfo(Node document) {
+ Node imageInfo = getNode(document, "imageinfo");
+ if (imageInfo != null) {
+ return getNode(imageInfo, "ii");
+ }
+ return null;
+ }
+
+ /**
+ * Takes a parent node as input and returns a child node if present
+ * @param node parent node
+ * @param nodeName child node name
+ * @return
+ */
+ @Nullable
+ public static Node getNode(Node node, String nodeName) {
+ NodeList childNodes = node.getChildNodes();
+ for (int i = 0; i < childNodes.getLength(); i++) {
+ Node nodeItem = childNodes.item(i);
+ Element item = (Element) nodeItem;
+ if (item.getTagName().equals(nodeName)) {
+ return nodeItem;
+ }
+ }
+ return null;
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
new file mode 100644
index 000000000..1f385b258
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java
@@ -0,0 +1,160 @@
+package fr.free.nrw.commons.category;
+
+import android.content.Context;
+import android.content.Intent;
+import android.database.DataSetObserver;
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.app.FragmentTransaction;
+import android.view.View;
+import android.widget.AdapterView;
+
+import butterknife.ButterKnife;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.auth.AuthenticatedActivity;
+import fr.free.nrw.commons.media.MediaDetailPagerFragment;
+import timber.log.Timber;
+
+/**
+ * This activity displays pictures of a particular category
+ * Its generic and simply takes the name of category name in its start intent to load all images in
+ * a particular category. This activity is currently being used to display a list of featured images,
+ * which is nothing but another category on wikimedia commons.
+ */
+
+public class CategoryImagesActivity
+ extends AuthenticatedActivity
+ implements FragmentManager.OnBackStackChangedListener,
+ MediaDetailPagerFragment.MediaDetailProvider,
+ AdapterView.OnItemClickListener{
+
+
+ private FragmentManager supportFragmentManager;
+ private CategoryImagesListFragment categoryImagesListFragment;
+ private MediaDetailPagerFragment mediaDetails;
+
+ @Override
+ protected void onAuthCookieAcquired(String authCookie) {
+
+ }
+
+ @Override
+ protected void onAuthFailure() {
+
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_category_images);
+ ButterKnife.bind(this);
+
+ // Activity can call methods in the fragment by acquiring a
+ // reference to the Fragment from FragmentManager, using findFragmentById()
+ supportFragmentManager = getSupportFragmentManager();
+ setCategoryImagesFragment();
+ supportFragmentManager.addOnBackStackChangedListener(this);
+ if (savedInstanceState != null) {
+ mediaDetails = (MediaDetailPagerFragment) supportFragmentManager
+ .findFragmentById(R.id.fragmentContainer);
+
+ }
+ requestAuthToken();
+ initDrawer();
+ setPageTitle();
+ }
+
+ /**
+ * Gets the categoryName from the intent and initializes the fragment for showing images of that category
+ */
+ private void setCategoryImagesFragment() {
+ categoryImagesListFragment = new CategoryImagesListFragment();
+ String categoryName = getIntent().getStringExtra("categoryName");
+ if (getIntent() != null && categoryName != null) {
+ Bundle arguments = new Bundle();
+ arguments.putString("categoryName", categoryName);
+ categoryImagesListFragment.setArguments(arguments);
+ FragmentTransaction transaction = supportFragmentManager.beginTransaction();
+ transaction
+ .add(R.id.fragmentContainer, categoryImagesListFragment)
+ .commit();
+ }
+ }
+
+ /**
+ * Gets the passed title from the intents and displays it as the page title
+ */
+ private void setPageTitle() {
+ if (getIntent() != null && getIntent().getStringExtra("title") != null) {
+ setTitle(getIntent().getStringExtra("title"));
+ }
+ }
+
+ @Override
+ public void onBackStackChanged() {
+ }
+
+ @Override
+ public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
+ if (mediaDetails == null || !mediaDetails.isVisible()) {
+ // set isFeaturedImage true for featured images, to include author field on media detail
+ mediaDetails = new MediaDetailPagerFragment(false, true);
+ FragmentManager supportFragmentManager = getSupportFragmentManager();
+ supportFragmentManager
+ .beginTransaction()
+ .replace(R.id.fragmentContainer, mediaDetails)
+ .addToBackStack(null)
+ .commit();
+ supportFragmentManager.executePendingTransactions();
+ }
+ mediaDetails.showImage(i);
+ }
+
+ /**
+ * Consumers should be simply using this method to use this activity.
+ * @param context
+ * @param title Page title
+ * @param categoryName Name of the category for displaying its images
+ */
+ public static void startYourself(Context context, String title, String categoryName) {
+ Intent intent = new Intent(context, CategoryImagesActivity.class);
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.putExtra("title", title);
+ intent.putExtra("categoryName", categoryName);
+ context.startActivity(intent);
+ }
+
+ @Override
+ public Media getMediaAtPosition(int i) {
+ if (categoryImagesListFragment.getAdapter() == null) {
+ // not yet ready to return data
+ return null;
+ } else {
+ return (Media) categoryImagesListFragment.getAdapter().getItem(i);
+ }
+ }
+
+ @Override
+ public int getTotalMediaCount() {
+ if (categoryImagesListFragment.getAdapter() == null) {
+ return 0;
+ }
+ return categoryImagesListFragment.getAdapter().getCount();
+ }
+
+ @Override
+ public void notifyDatasetChanged() {
+
+ }
+
+ @Override
+ public void registerDataSetObserver(DataSetObserver observer) {
+
+ }
+
+ @Override
+ public void unregisterDataSetObserver(DataSetObserver observer) {
+
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
new file mode 100644
index 000000000..3b6734edd
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java
@@ -0,0 +1,227 @@
+package fr.free.nrw.commons.category;
+
+import android.annotation.SuppressLint;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.support.annotation.Nullable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.GridView;
+import android.widget.ListAdapter;
+import android.widget.ProgressBar;
+import android.widget.TextView;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import javax.inject.Inject;
+import javax.inject.Named;
+
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import dagger.android.support.DaggerFragment;
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.R;
+import fr.free.nrw.commons.utils.NetworkUtils;
+import fr.free.nrw.commons.utils.ViewUtil;
+import io.reactivex.Observable;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.schedulers.Schedulers;
+import timber.log.Timber;
+
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
+
+/**
+ * Displays images for a particular category with load more on scrolling incorporated
+ */
+public class CategoryImagesListFragment extends DaggerFragment {
+
+ private static int TIMEOUT_SECONDS = 15;
+
+ private GridViewAdapter gridAdapter;
+
+ @BindView(R.id.statusMessage)
+ TextView statusTextView;
+ @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar;
+ @BindView(R.id.categoryImagesList) GridView gridView;
+
+ private boolean hasMoreImages = true;
+ private boolean isLoading;
+ private String categoryName = null;
+
+ @Inject CategoryImageController controller;
+ @Inject @Named("category_prefs") SharedPreferences categoryPreferences;
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View v = inflater.inflate(R.layout.fragment_category_images, container, false);
+ ButterKnife.bind(this, v);
+ return v;
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
+ initViews();
+ }
+
+ /**
+ * Initializes the UI elements for the fragment
+ * Setup the grid view to and scroll listener for it
+ */
+ private void initViews() {
+ String categoryName = getArguments().getString("categoryName");
+ if (getArguments() != null && categoryName != null) {
+ this.categoryName = categoryName;
+ resetQueryContinueValues(categoryName);
+ initList();
+ setScrollListener();
+ }
+ }
+
+ /**
+ * Query continue values determine the last page that was loaded for the particular keyword
+ * This method resets those values, so that the results can be queried from the first page itself
+ * @param keyword
+ */
+ private void resetQueryContinueValues(String keyword) {
+ SharedPreferences.Editor editor = categoryPreferences.edit();
+ editor.remove(keyword);
+ editor.apply();
+ }
+
+ /**
+ * Checks for internet connection and then initializes the grid view with first 10 images of that category
+ */
+ @SuppressLint("CheckResult")
+ private void initList() {
+ if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+
+ isLoading = true;
+ progressBar.setVisibility(VISIBLE);
+ Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handleSuccess, this::handleError);
+ }
+
+ /**
+ * Handles the UI updates for no internet scenario
+ */
+ private void handleNoInternet() {
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_internet));
+ } else {
+ ViewUtil.showSnackbar(gridView, R.string.no_internet);
+ }
+ }
+
+ /**
+ * Logs and handles API error scenario
+ * @param throwable
+ */
+ private void handleError(Throwable throwable) {
+ Timber.e(throwable, "Error occurred while loading featured images");
+ initErrorView();
+ }
+
+ /**
+ * Handles the UI updates for a error scenario
+ */
+ private void initErrorView() {
+ ViewUtil.showSnackbar(gridView, R.string.error_loading_images);
+ progressBar.setVisibility(GONE);
+ if (gridAdapter == null || gridAdapter.isEmpty()) {
+ statusTextView.setVisibility(VISIBLE);
+ statusTextView.setText(getString(R.string.no_images_found));
+ } else {
+ statusTextView.setVisibility(GONE);
+ }
+ }
+
+ /**
+ * Initializes the adapter with a list of Media objects
+ * @param mediaList
+ */
+ private void setAdapter(List mediaList) {
+ gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList);
+ gridView.setAdapter(gridAdapter);
+ }
+
+ /**
+ * Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
+ * Checks if the category has more images before loading
+ * Also checks whether images are currently being fetched before triggering another request
+ */
+ private void setScrollListener() {
+ gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(AbsListView view, int scrollState) {
+ }
+
+ @Override
+ public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+ if (hasMoreImages && !isLoading && (firstVisibleItem + visibleItemCount + 1 >= totalItemCount)) {
+ isLoading = true;
+ fetchMoreImages();
+ }
+ }
+ });
+ }
+
+ /**
+ * Fetches more images for the category and adds it to the grid view adapter
+ */
+ @SuppressLint("CheckResult")
+ private void fetchMoreImages() {
+ if(!NetworkUtils.isInternetConnectionEstablished(getContext())) {
+ handleNoInternet();
+ return;
+ }
+
+ progressBar.setVisibility(VISIBLE);
+ Observable.fromCallable(() -> controller.getCategoryImages(categoryName))
+ .subscribeOn(Schedulers.io())
+ .observeOn(AndroidSchedulers.mainThread())
+ .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
+ .subscribe(this::handleSuccess, this::handleError);
+ }
+
+ /**
+ * Handles the success scenario
+ * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
+ * @param collection
+ */
+ private void handleSuccess(List collection) {
+ if(collection == null || collection.isEmpty()) {
+ initErrorView();
+ hasMoreImages = false;
+ return;
+ }
+
+ if(gridAdapter == null) {
+ setAdapter(collection);
+ } else {
+ gridAdapter.addItems(collection);
+ }
+
+ progressBar.setVisibility(GONE);
+ isLoading = false;
+ statusTextView.setVisibility(GONE);
+ }
+
+ public ListAdapter getAdapter() {
+ return gridView.getAdapter();
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
new file mode 100644
index 000000000..c8e6066f6
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java
@@ -0,0 +1,88 @@
+package fr.free.nrw.commons.category;
+
+import android.app.Activity;
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ArrayAdapter;
+import android.widget.TextView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import fr.free.nrw.commons.Media;
+import fr.free.nrw.commons.MediaWikiImageView;
+import fr.free.nrw.commons.R;
+
+/**
+ * This is created to only display UI implementation. Needs to be changed in real implementation
+ */
+
+public class GridViewAdapter extends ArrayAdapter {
+ private Context context;
+ private List data;
+
+ public GridViewAdapter(Context context, int layoutResourceId, List data) {
+ super(context, layoutResourceId, data);
+ this.context = context;
+ this.data = data;
+ }
+
+ /**
+ * Adds more item to the list
+ * Its triggered on scrolling down in the list
+ * @param images
+ */
+ public void addItems(List images) {
+ if (data == null) {
+ data = new ArrayList<>();
+ }
+ data.addAll(images);
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return data == null || data.isEmpty();
+ }
+
+ /**
+ * Sets up the UI for the category image item
+ * @param position
+ * @param convertView
+ * @param parent
+ * @return
+ */
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+
+ if (convertView == null) {
+ LayoutInflater inflater = ((Activity) context).getLayoutInflater();
+ convertView = inflater.inflate(R.layout.layout_category_images, null);
+ }
+
+ Media item = data.get(position);
+ MediaWikiImageView imageView = convertView.findViewById(R.id.categoryImageView);
+ TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
+ TextView author = convertView.findViewById(R.id.categoryImageAuthor);
+ fileName.setText(item.getFilename());
+ setAuthorView(item, author);
+ imageView.setMedia(item);
+ return convertView;
+ }
+
+ /**
+ * Shows author information if its present
+ * @param item
+ * @param author
+ */
+ private void setAuthorView(Media item, TextView author) {
+ if (item.getCreator() != null && !item.getCreator().equals("")) {
+ String uploadedByTemplate = context.getString(R.string.image_uploaded_by);
+ author.setText(String.format(uploadedByTemplate, item.getCreator()));
+ } else {
+ author.setVisibility(View.GONE);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java
new file mode 100644
index 000000000..e12d5a778
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/category/QueryContinue.java
@@ -0,0 +1,24 @@
+package fr.free.nrw.commons.category;
+
+/**
+ * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
+ * https://www.mediawiki.org/wiki/API:Raw_query_continue
+ */
+public class QueryContinue {
+ private String continueParam;
+ private String gcmContinueParam;
+
+ public QueryContinue(String continueParam, String gcmContinueParam) {
+ this.continueParam = continueParam;
+ this.gcmContinueParam = gcmContinueParam;
+ }
+
+ public String getGcmContinueParam() {
+ return gcmContinueParam;
+ }
+
+ public String getContinueParam() {
+ return continueParam;
+ }
+}
+
diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
index f88f3b34a..51aa85903 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java
@@ -7,7 +7,7 @@ import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.auth.SignupActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
-import fr.free.nrw.commons.featured.FeaturedImagesActivity;
+import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
@@ -49,5 +49,5 @@ public abstract class ActivityBuilderModule {
abstract NotificationActivity bindNotificationActivity();
@ContributesAndroidInjector
- abstract FeaturedImagesActivity bindFeaturedImagesActivity();
+ abstract CategoryImagesActivity bindFeaturedImagesActivity();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
index e8b915c7e..55281be7e 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationModule.java
@@ -6,6 +6,8 @@ import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.support.v4.util.LruCache;
+import com.google.gson.Gson;
+
import javax.inject.Named;
import javax.inject.Singleton;
@@ -85,6 +87,17 @@ public class CommonsApplicationModule {
return context.getSharedPreferences("prefs", MODE_PRIVATE);
}
+ /**
+ *
+ * @param context
+ * @return returns categoryPrefs
+ */
+ @Provides
+ @Named("category_prefs")
+ public SharedPreferences providesCategorySharedPreferences(Context context) {
+ return context.getSharedPreferences("categoryPrefs", MODE_PRIVATE);
+ }
+
@Provides
@Named("direct_nearby_upload_prefs")
public SharedPreferences providesDirectNearbyUploadPreferences(Context context) {
@@ -106,8 +119,11 @@ public class CommonsApplicationModule {
@Provides
@Singleton
- public MediaWikiApi provideMediaWikiApi(Context context, @Named("default_preferences") SharedPreferences sharedPreferences) {
- return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, sharedPreferences);
+ public MediaWikiApi provideMediaWikiApi(Context context,
+ @Named("default_preferences") SharedPreferences defaultPreferences,
+ @Named("category_prefs") SharedPreferences categoryPrefs,
+ Gson gson) {
+ return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, defaultPreferences, categoryPrefs, gson);
}
@Provides
@@ -116,6 +132,16 @@ public class CommonsApplicationModule {
return new LocationServiceManager(context);
}
+ /**
+ * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
+ * @return returns a singleton Gson instance
+ */
+ @Provides
+ @Singleton
+ public Gson provideGson() {
+ return new Gson();
+ }
+
@Provides
@Singleton
public CacheController provideCacheController() {
diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
index c5cdcb5a7..dfed64871 100644
--- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
+++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java
@@ -4,7 +4,7 @@ import dagger.Module;
import dagger.android.ContributesAndroidInjector;
import fr.free.nrw.commons.category.CategorizationFragment;
import fr.free.nrw.commons.contributions.ContributionsListFragment;
-import fr.free.nrw.commons.featured.FeaturedImagesListFragment;
+import fr.free.nrw.commons.category.CategoryImagesListFragment;
import fr.free.nrw.commons.media.MediaDetailFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.nearby.NearbyListFragment;
@@ -49,6 +49,6 @@ public abstract class FragmentBuilderModule {
abstract SingleUploadFragment bindSingleUploadFragment();
@ContributesAndroidInjector
- abstract FeaturedImagesListFragment bindFeaturedImagesListFragment();
+ abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImage.java b/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImage.java
deleted file mode 100644
index 853fba29e..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImage.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-
-import fr.free.nrw.commons.Media;
-
-/**
- * Object to hold FeaturedImage
- */
-
-public class FeaturedImage {
- private Media image;
- private String author;
- private String fileName;
-
- public FeaturedImage(Media image, String author, String fileName) {
- this.image = image;
- this.author = author;
- this.fileName = fileName;
- }
-
- public Media getImage() {
- return image;
- }
-
- public void setImage(Media image) {
- this.image = image;
- }
-
- public String getAuthor() {
- return author;
- }
-
- public void setAuthor(String author) {
- this.author = author;
- }
-
- public String getFileName() {
- return fileName;
- }
-
- public void setFileName(String fileName) {
- this.fileName = fileName;
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesActivity.java
deleted file mode 100644
index a2dc7b00b..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesActivity.java
+++ /dev/null
@@ -1,114 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-import android.database.DataSetObserver;
-import android.os.Bundle;
-import android.support.v4.app.FragmentManager;
-import android.view.View;
-import android.widget.AdapterView;
-
-import butterknife.ButterKnife;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.auth.AuthenticatedActivity;
-import fr.free.nrw.commons.media.MediaDetailPagerFragment;
-
-/**
- * This activity displays pic of the days of last xx days
- */
-
-public class FeaturedImagesActivity
- extends AuthenticatedActivity
- implements FragmentManager.OnBackStackChangedListener,
- MediaDetailPagerFragment.MediaDetailProvider,
- AdapterView.OnItemClickListener{
-
- private FeaturedImagesListFragment featuredImagesListFragment;
- private MediaDetailPagerFragment mediaDetails;
-
- @Override
- protected void onAuthCookieAcquired(String authCookie) {
-
- }
-
- @Override
- protected void onAuthFailure() {
-
- }
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_featured_images);
- ButterKnife.bind(this);
-
- // Activity can call methods in the fragment by acquiring a
- // reference to the Fragment from FragmentManager, using findFragmentById()
- FragmentManager supportFragmentManager = getSupportFragmentManager();
- featuredImagesListFragment = (FeaturedImagesListFragment)supportFragmentManager
- .findFragmentById(R.id.featuedListFragment);
-
- supportFragmentManager.addOnBackStackChangedListener(this);
- if (savedInstanceState != null) {
- mediaDetails = (MediaDetailPagerFragment)supportFragmentManager
- .findFragmentById(R.id.featuredFragmentContainer);
-
- }
- requestAuthToken();
- initDrawer();
- setTitle(getString(R.string.title_activity_featured_images));
- }
-
- @Override
- public void onBackStackChanged() {
-
- }
-
- @Override
- public void onItemClick(AdapterView> adapterView, View view, int i, long l) {
- if (mediaDetails == null || !mediaDetails.isVisible()) {
- // set isFeaturedImage true for featured images, to include author field on media detail
- mediaDetails = new MediaDetailPagerFragment(false, true);
- FragmentManager supportFragmentManager = getSupportFragmentManager();
- supportFragmentManager
- .beginTransaction()
- .replace(R.id.featuredFragmentContainer, mediaDetails)
- .addToBackStack(null)
- .commit();
- supportFragmentManager.executePendingTransactions();
- }
- mediaDetails.showImage(i);
- }
-
- @Override
- public Media getMediaAtPosition(int i) {
- if (featuredImagesListFragment.getAdapter() == null) {
- // not yet ready to return data
- return null;
- } else {
- return ((FeaturedImage)featuredImagesListFragment.getAdapter().getItem(i)).getImage();
- }
- }
-
- @Override
- public int getTotalMediaCount() {
- if (featuredImagesListFragment.getAdapter() == null) {
- return 0;
- }
- return featuredImagesListFragment.getAdapter().getCount();
- }
-
- @Override
- public void notifyDatasetChanged() {
-
- }
-
- @Override
- public void registerDataSetObserver(DataSetObserver observer) {
-
- }
-
- @Override
- public void unregisterDataSetObserver(DataSetObserver observer) {
-
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesListFragment.java
deleted file mode 100644
index 19e33b0ee..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/FeaturedImagesListFragment.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-import android.os.Bundle;
-import android.support.annotation.Nullable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.GridView;
-import android.widget.ListAdapter;
-
-import java.util.ArrayList;
-
-import butterknife.ButterKnife;
-import dagger.android.support.DaggerFragment;
-import fr.free.nrw.commons.Media;
-import fr.free.nrw.commons.R;
-
-public class FeaturedImagesListFragment extends DaggerFragment {
- private GridView gridView;
- private MockGridViewAdapter gridAdapter;
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
- View v = inflater.inflate(R.layout.fragment_featured_images, container, false);
- ButterKnife.bind(this, v);
- return v;
- }
-
- @Override
- public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
-
- gridView = getView().findViewById(R.id.featuredImagesList);
- gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
- gridAdapter = new MockGridViewAdapter(this.getContext(), R.layout.layout_featured_images, getMockFeaturedImages());
- gridView.setAdapter(gridAdapter);
-
- }
-
- private ArrayList getMockFeaturedImages(){
- ArrayList featuredImages = new ArrayList<>();
- for (int i=0; i<10; i++){
- featuredImages.add(new FeaturedImage(new Media("test.jpg"), "username: test", "test file name"));
- }
- return featuredImages;
- }
-
- public ListAdapter getAdapter() {
- return gridView.getAdapter();
- }
-}
diff --git a/app/src/main/java/fr/free/nrw/commons/featured/MockGridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/featured/MockGridViewAdapter.java
deleted file mode 100644
index 7aa2a8892..000000000
--- a/app/src/main/java/fr/free/nrw/commons/featured/MockGridViewAdapter.java
+++ /dev/null
@@ -1,50 +0,0 @@
-package fr.free.nrw.commons.featured;
-
-import android.app.Activity;
-import android.content.Context;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ArrayAdapter;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-
-import fr.free.nrw.commons.MediaWikiImageView;
-import fr.free.nrw.commons.R;
-
-/**
- * This is created to only display UI implementation. Needs to be changed in real implementation
- */
-
-public class MockGridViewAdapter extends ArrayAdapter {
- private Context context;
- private int layoutResourceId;
- private ArrayList data = new ArrayList();
-
- public MockGridViewAdapter(Context context, int layoutResourceId, ArrayList data) {
- super(context, layoutResourceId, data);
- this.layoutResourceId = layoutResourceId;
- this.context = context;
- this.data = data;
- }
-
- @Override
- public View getView(int position, View convertView, ViewGroup parent) {
-
- if (convertView == null) {
- LayoutInflater inflater = ((Activity) context).getLayoutInflater();
- convertView = inflater.inflate(R.layout.layout_featured_images, null);
- }
-
- FeaturedImage item = data.get(position);
- MediaWikiImageView imageView = convertView.findViewById(R.id.featuredImageView);
- TextView fileName = convertView.findViewById(R.id.featuredImageTitle);
- TextView author = convertView.findViewById(R.id.featuredImageAuthor);
- fileName.setText("Test file name");
- author.setText("Uploaded by: Test user name");
- imageView.setMedia(item.getImage());
- return convertView;
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
index 037f92e56..9614c4f00 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java
@@ -9,6 +9,7 @@ import android.os.AsyncTask;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.Editable;
+import android.text.TextUtils;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.view.LayoutInflater;
@@ -22,6 +23,9 @@ import android.widget.ScrollView;
import android.widget.TextView;
import android.widget.Toast;
+import butterknife.BindView;
+import butterknife.ButterKnife;
+import butterknife.OnClick;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
@@ -45,6 +49,8 @@ import fr.free.nrw.commons.mwapi.MediaWikiApi;
import fr.free.nrw.commons.ui.widget.CompatTextView;
import timber.log.Timber;
+import static android.view.View.GONE;
+import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@@ -74,23 +80,37 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Inject
MediaWikiApi mwApi;
-
- private MediaWikiImageView image;
- private MediaDetailSpacer spacer;
private int initialListTop = 0;
- private TextView title;
- private TextView desc;
- private TextView author;
- private TextView license;
- private TextView coordinates;
- private TextView uploadedDate;
- private TextView seeMore;
- private LinearLayout nominatedforDeletion;
- private LinearLayout categoryContainer;
- private LinearLayout authorLayout;
- private Button delete;
- private ScrollView scrollView;
+ @BindView(R.id.mediaDetailImage)
+ MediaWikiImageView image;
+ @BindView(R.id.mediaDetailSpacer)
+ MediaDetailSpacer spacer;
+ @BindView(R.id.mediaDetailTitle)
+ TextView title;
+ @BindView(R.id.mediaDetailDesc)
+ TextView desc;
+ @BindView(R.id.mediaDetailAuthor)
+ TextView author;
+ @BindView(R.id.mediaDetailLicense)
+ TextView license;
+ @BindView(R.id.mediaDetailCoordinates)
+ TextView coordinates;
+ @BindView(R.id.mediaDetailuploadeddate)
+ TextView uploadedDate;
+ @BindView(R.id.seeMore)
+ TextView seeMore;
+ @BindView(R.id.nominatedDeletionBanner)
+ LinearLayout nominatedForDeletion;
+ @BindView(R.id.mediaDetailCategoryContainer)
+ LinearLayout categoryContainer;
+ @BindView(R.id.authorLinearLayout)
+ LinearLayout authorLayout;
+ @BindView(R.id.nominateDeletion)
+ Button delete;
+ @BindView(R.id.mediaDetailScrollView)
+ ScrollView scrollView;
+
private ArrayList categoryNames;
private boolean categoriesLoaded = false;
private boolean categoriesPresent = false;
@@ -100,6 +120,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private AsyncTask detailFetchTask;
private LicenseList licenseList;
+ //Had to make this class variable, to implement various onClicks, which access the media, also I fell why make separate variables when one can serve the purpose
+ private Media media;
+
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
@@ -136,27 +159,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
- image = (MediaWikiImageView) view.findViewById(R.id.mediaDetailImage);
- scrollView = (ScrollView) view.findViewById(R.id.mediaDetailScrollView);
-
- // Detail consists of a list view with main pane in header view, plus category list.
- spacer = (MediaDetailSpacer) view.findViewById(R.id.mediaDetailSpacer);
- title = (TextView) view.findViewById(R.id.mediaDetailTitle);
- desc = (TextView) view.findViewById(R.id.mediaDetailDesc);
- author = (TextView) view.findViewById(R.id.mediaDetailAuthor);
- license = (TextView) view.findViewById(R.id.mediaDetailLicense);
- coordinates = (TextView) view.findViewById(R.id.mediaDetailCoordinates);
- uploadedDate = (TextView) view.findViewById(R.id.mediaDetailuploadeddate);
- seeMore = (TextView) view.findViewById(R.id.seeMore);
- nominatedforDeletion = (LinearLayout) view.findViewById(R.id.nominatedDeletionBanner);
- delete = (Button) view.findViewById(R.id.nominateDeletion);
- categoryContainer = (LinearLayout) view.findViewById(R.id.mediaDetailCategoryContainer);
- authorLayout = (LinearLayout) view.findViewById(R.id.authorLinearLayout);
+ ButterKnife.bind(this,view);
if (isFeaturedMedia){
- authorLayout.setVisibility(View.VISIBLE);
+ authorLayout.setVisibility(VISIBLE);
} else {
- authorLayout.setVisibility(View.GONE);
+ authorLayout.setVisibility(GONE);
}
licenseList = new LicenseList(getActivity());
@@ -195,7 +203,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
@Override
public void onResume() {
super.onResume();
- Media media = detailProvider.getMediaAtPosition(index);
+ media = detailProvider.getMediaAtPosition(index);
if (media == null) {
// Ask the detail provider to ping us when we're ready
Timber.d("MediaDetailFragment not yet ready to display details; registering observer");
@@ -208,17 +216,18 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
Timber.d("MediaDetailFragment ready to display delayed details!");
detailProvider.unregisterDataSetObserver(dataObserver);
dataObserver = null;
- displayMediaDetails(detailProvider.getMediaAtPosition(index));
+ media=detailProvider.getMediaAtPosition(index);
+ displayMediaDetails();
}
};
detailProvider.registerDataSetObserver(dataObserver);
} else {
Timber.d("MediaDetailFragment ready to display details");
- displayMediaDetails(media);
+ displayMediaDetails();
}
}
- private void displayMediaDetails(final Media media) {
+ private void displayMediaDetails() {
//Always load image from Internet to allow viewing the desc, license, and cats
image.setMedia(media);
@@ -255,7 +264,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
if (success) {
extractor.fill(media);
setTextFields(media);
- setOnClickListeners(media);
} else {
Timber.d("Failed to load photo details.");
}
@@ -306,73 +314,90 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
}
rebuildCatList();
+ if(media.getCreator() == null || media.getCreator().equals("")) {
+ authorLayout.setVisibility(GONE);
+ } else {
+ author.setText(media.getCreator());
+ }
+
checkDeletion(media);
}
- private void setOnClickListeners(final Media media) {
- if (licenseLink(media) != null) {
- license.setOnClickListener(v -> openWebBrowser(licenseLink(media)));
+ @OnClick(R.id.mediaDetailLicense)
+ public void onMediaDetailLicenceClicked(){
+ if (!TextUtils.isEmpty(licenseLink(media))) {
+ openWebBrowser(licenseLink(media));
} else {
- Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
- toast.show();
+ if(isFeaturedMedia) {
+ Timber.d("Unable to fetch license URL for %s", media.getLicense());
+ } else {
+ Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT);
+ toast.show();
+ }
}
+ }
+
+ @OnClick(R.id.mediaDetailCoordinates)
+ public void onMediaDetailCoordinatesClicked(){
if (media.getCoordinates() != null) {
- coordinates.setOnClickListener(v -> openMap(media.getCoordinates()));
+ openMap(media.getCoordinates());
}
- if (delete.getVisibility() == View.VISIBLE) {
- enableDeleteButton(true);
+ }
- delete.setOnClickListener(v -> {
+ @OnClick(R.id.nominateDeletion)
+ public void onDeleteButtonClicked(){
+ //Reviewer correct me if i have misunderstood something over here
+ //But how does this if (delete.getVisibility() == View.VISIBLE) {
+ // enableDeleteButton(true); makes sense ?
+ AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
+ alert.setMessage("Why should this file be deleted?");
+ final EditText input = new EditText(getActivity());
+ alert.setView(input);
+ input.requestFocus();
+ alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ String reason = input.getText().toString();
+ DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
+ deleteTask.execute();
+ enableDeleteButton(false);
+ }
+ });
+ alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface dialog, int whichButton) {
+ }
+ });
+ AlertDialog d = alert.create();
+ input.addTextChangedListener(new TextWatcher() {
+ private void handleText() {
+ final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE);
+ if (input.getText().length() == 0) {
+ okButton.setEnabled(false);
+ } else {
+ okButton.setEnabled(true);
+ }
+ }
- AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
- alert.setMessage("Why should this file be deleted?");
- final EditText input = new EditText(getActivity());
- alert.setView(input);
- input.requestFocus();
- alert.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int whichButton) {
- String reason = input.getText().toString();
- DeleteTask deleteTask = new DeleteTask(getActivity(), media, reason);
- deleteTask.execute();
- enableDeleteButton(false);
- }
- });
- alert.setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int whichButton) {
- }
- });
- AlertDialog d = alert.create();
- input.addTextChangedListener(new TextWatcher() {
- private void handleText() {
- final Button okButton = d.getButton(AlertDialog.BUTTON_POSITIVE);
- if (input.getText().length() == 0) {
- okButton.setEnabled(false);
- } else {
- okButton.setEnabled(true);
- }
- }
+ @Override
+ public void afterTextChanged(Editable arg0) {
+ handleText();
+ }
- @Override
- public void afterTextChanged(Editable arg0) {
- handleText();
- }
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+ }
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) {
- }
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ }
+ });
+ d.show();
+ d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
+ }
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count) {
- }
- });
- d.show();
- d.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false);
- });
- }
- if (nominatedforDeletion.getVisibility() == View.VISIBLE){
- seeMore.setOnClickListener(v -> {
- openWebBrowser(media.getFilePageTitle().getMobileUri().toString());
- });
+ @OnClick(R.id.seeMore)
+ public void onSeeMoreClicked(){
+ if(nominatedForDeletion.getVisibility()== VISIBLE) {
+ openWebBrowser(media.getFilePageTitle().getMobileUri().toString());
}
}
@@ -476,12 +501,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
private void checkDeletion(Media media){
if (media.getRequestedDeletion()){
- delete.setVisibility(View.GONE);
- nominatedforDeletion.setVisibility(View.VISIBLE);
+ delete.setVisibility(GONE);
+ nominatedForDeletion.setVisibility(VISIBLE);
}
else{
- delete.setVisibility(View.VISIBLE);
- nominatedforDeletion.setVisibility(View.GONE);
+ delete.setVisibility(VISIBLE);
+ nominatedForDeletion.setVisibility(GONE);
}
}
diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
index c0564c603..62d1261cf 100644
--- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java
@@ -26,6 +26,8 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
+import butterknife.BindView;
+import butterknife.ButterKnife;
import javax.inject.Inject;
import javax.inject.Named;
@@ -53,7 +55,8 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
@Named("default_preferences")
SharedPreferences prefs;
- private ViewPager pager;
+ @BindView(R.id.mediaDetailsPager)
+ ViewPager pager;
private Boolean editable;
private boolean isFeaturedImage;
@@ -72,7 +75,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_media_detail_pager, container, false);
- pager = (ViewPager) view.findViewById(R.id.mediaDetailsPager);
+ ButterKnife.bind(this,view);
pager.addOnPageChangeListener(this);
final MediaDetailAdapter adapter = new MediaDetailAdapter(getChildFragmentManager());
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
index 78051abd8..6629d0933 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java
@@ -9,6 +9,8 @@ import android.support.annotation.VisibleForTesting;
import android.text.TextUtils;
import android.util.Log;
+import com.google.gson.Gson;
+
import org.apache.http.HttpResponse;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
@@ -38,7 +40,10 @@ import java.util.Locale;
import java.util.concurrent.Callable;
import fr.free.nrw.commons.BuildConfig;
+import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.PageTitle;
+import fr.free.nrw.commons.category.CategoryImageUtils;
+import fr.free.nrw.commons.category.QueryContinue;
import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationUtils;
import in.yuvi.http.fluent.Http;
@@ -46,6 +51,8 @@ import io.reactivex.Observable;
import io.reactivex.Single;
import timber.log.Timber;
+import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue;
+
/**
* @author Addshore
*/
@@ -56,9 +63,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
private AbstractHttpClient httpClient;
private MWApi api;
private Context context;
- private SharedPreferences sharedPreferences;
+ private SharedPreferences defaultPreferences;
+ private SharedPreferences categoryPreferences;
+ private Gson gson;
- public ApacheHttpClientMediaWikiApi(Context context, String apiURL, SharedPreferences sharedPreferences) {
+ public ApacheHttpClientMediaWikiApi(Context context,
+ String apiURL,
+ SharedPreferences defaultPreferences,
+ SharedPreferences categoryPreferences,
+ Gson gson) {
this.context = context;
BasicHttpParams params = new BasicHttpParams();
SchemeRegistry schemeRegistry = new SchemeRegistry();
@@ -69,7 +82,9 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent());
httpClient = new DefaultHttpClient(cm, params);
api = new MWApi(apiURL, httpClient);
- this.sharedPreferences = sharedPreferences;
+ this.defaultPreferences = defaultPreferences;
+ this.categoryPreferences = categoryPreferences;
+ this.gson = gson;
}
@Override
@@ -160,7 +175,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
}
private void setAuthCookieOnLogin(boolean isLoggedIn) {
- SharedPreferences.Editor editor = sharedPreferences.edit();
+ SharedPreferences.Editor editor = defaultPreferences.edit();
if (isLoggedIn) {
editor.putBoolean("isUserLoggedIn", true);
editor.putString("getAuthCookie", api.getAuthCookie());
@@ -448,6 +463,81 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
return NotificationUtils.getNotificationsFromList(context, childNodes);
}
+ /**
+ * The method takes categoryName as input and returns a List of Media objects
+ * It uses the generator query API to get the images in a category, 10 at a time.
+ * Uses the query continue values for fetching paginated responses
+ * @param categoryName Category name as defined on commons
+ * @return
+ */
+ @Override
+ @NonNull
+ public List getCategoryImages(String categoryName) {
+ ApiResult apiResult = null;
+ try {
+ MWApi.RequestBuilder requestBuilder = api.action("query")
+ .param("generator", "categorymembers")
+ .param("format", "xml")
+ .param("gcmtype", "file")
+ .param("gcmtitle", categoryName)
+ .param("prop", "imageinfo")
+ .param("gcmlimit", "10")
+ .param("iiprop", "url|extmetadata");
+
+ QueryContinue queryContinueValues = getQueryContinueValues(categoryName);
+ if (queryContinueValues != null) {
+ requestBuilder.param("continue", queryContinueValues.getContinueParam());
+ requestBuilder.param("gcmcontinue", queryContinueValues.getGcmContinueParam());
+ }
+
+ apiResult = requestBuilder.get();
+ } catch (IOException e) {
+ Timber.e("Failed to obtain searchCategories", e);
+ }
+
+ if (apiResult == null) {
+ return new ArrayList<>();
+ }
+
+ ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages");
+ if (categoryImagesNode == null
+ || categoryImagesNode.getDocument() == null
+ || categoryImagesNode.getDocument().getChildNodes() == null
+ || categoryImagesNode.getDocument().getChildNodes().getLength() == 0) {
+ return new ArrayList<>();
+ }
+
+ QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument());
+ setQueryContinueValues(categoryName, queryContinue);
+
+ NodeList childNodes = categoryImagesNode.getDocument().getChildNodes();
+ return CategoryImageUtils.getMediaList(childNodes);
+ }
+
+ /**
+ * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages
+ * https://www.mediawiki.org/wiki/API:Raw_query_continue
+ * After fetching images a page of image for a particular category, shared prefs are updated with the latest QueryContinue Values
+ * @param keyword
+ * @param queryContinue
+ */
+ private void setQueryContinueValues(String keyword, QueryContinue queryContinue) {
+ SharedPreferences.Editor editor = categoryPreferences.edit();
+ editor.putString(keyword, gson.toJson(queryContinue));
+ editor.apply();
+ }
+
+ /**
+ * Before making a paginated API call, this method is called to get the latest query continue values to be used
+ * @param keyword
+ * @return
+ */
+ @Nullable
+ private QueryContinue getQueryContinueValues(String keyword) {
+ String queryContinueString = categoryPreferences.getString(keyword, null);
+ return gson.fromJson(queryContinueString, QueryContinue.class);
+ }
+
@Override
public boolean existingFile(String fileSha1) throws IOException {
return api.action("query")
diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
index fd213455d..c0bd2fd87 100644
--- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
+++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java
@@ -7,6 +7,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.List;
+import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.notification.Notification;
import io.reactivex.Observable;
import io.reactivex.Single;
@@ -34,6 +35,8 @@ public interface MediaWikiApi {
boolean logEvents(LogBuilder[] logBuilders);
+ List getCategoryImages(String categoryName);
+
@NonNull
UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException;
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java
index 2a423de93..3bf8beacf 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java
@@ -4,14 +4,18 @@ import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.content.SharedPreferences;
import android.content.pm.PackageManager;
+import android.graphics.Typeface;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
+import android.os.Handler;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomSheetBehavior;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AlertDialog;
+
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
@@ -25,6 +29,7 @@ import com.google.gson.GsonBuilder;
import java.util.List;
import javax.inject.Inject;
+import javax.inject.Named;
import butterknife.BindView;
import butterknife.ButterKnife;
@@ -41,6 +46,8 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.schedulers.Schedulers;
import timber.log.Timber;
+import uk.co.deanwild.materialshowcaseview.IShowcaseListener;
+import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener {
@@ -56,12 +63,15 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
LinearLayout bottomSheetDetails;
@BindView(R.id.transparentView)
View transparentView;
+ @BindView(R.id.fab_recenter)
+ View fabRecenter;
@Inject
LocationServiceManager locationManager;
@Inject
NearbyController nearbyController;
-
+ @Inject
+ @Named("application_preferences") SharedPreferences applicationPrefs;
private LatLng curLatLng;
private Bundle bundle;
private Disposable placesDisposable;
@@ -72,11 +82,18 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
private NearbyListFragment nearbyListFragment;
private static final String TAG_RETAINED_MAP_FRAGMENT = NearbyMapFragment.class.getSimpleName();
private static final String TAG_RETAINED_LIST_FRAGMENT = NearbyListFragment.class.getSimpleName();
+ private View listButton; // Reference to list button to use in tutorial
private final String NETWORK_INTENT_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
private BroadcastReceiver broadcastReceiver;
+
+ private boolean isListShowcaseAdded = false;
+ private boolean isMapShowCaseAdded = false;
+
private LatLng lastKnownLocation;
+ private MaterialShowcaseView secondSingleShowCaseView;
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -126,6 +143,39 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_nearby, menu);
+ new Handler().post(() -> {
+
+ listButton = findViewById(R.id.action_display_list);
+
+ secondSingleShowCaseView = new MaterialShowcaseView.Builder(this)
+ .setTarget(listButton)
+ .setDismissText(getString(R.string.showcase_view_got_it_button))
+ .setContentText(getString(R.string.showcase_view_list_icon))
+ .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
+ .singleUse(ViewUtil.SHOWCASE_VIEW_ID_1) // provide a unique ID used to ensure it is only shown once
+ .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
+ .setListener(new IShowcaseListener() {
+ @Override
+ public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {
+
+ }
+
+ // If dismissed, we can inform fragment to start showcase sequence there
+ @Override
+ public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
+ nearbyMapFragment.onNearbyMaterialShowcaseDismissed();
+ }
+ })
+ .build();
+
+ isListShowcaseAdded = true;
+
+ if (isMapShowCaseAdded) { // If map showcase is also ready, start ShowcaseSequence
+ // Probably this case is not possible. Just added to be careful
+ setMapViewTutorialShowCase();
+ }
+ });
+
return super.onCreateOptionsMenu(menu);
}
@@ -420,6 +470,45 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp
updateMapFragment(false);
updateListFragment();
}
+
+ isMapShowCaseAdded = true;
+ }
+
+ public void setMapViewTutorialShowCase() {
+ /*
+ *This showcase view will be the first step of our nearbyMaterialShowcaseSequence. The reason we use a
+ * single item instead of adding another step to nearbyMaterialShowcaseSequence is that we are not able to
+ * call withoutShape() method on steps. For mapView we need an showcase view without
+ * any circle on it, it should cover the whole page.
+ * */
+ MaterialShowcaseView firstSingleShowCaseView = new MaterialShowcaseView.Builder(this)
+ .setTarget(nearbyMapFragment.mapView)
+ .setDismissText(getString(R.string.showcase_view_got_it_button))
+ .setContentText(getString(R.string.showcase_view_whole_nearby_activity))
+ .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
+ .singleUse(ViewUtil.SHOWCASE_VIEW_ID_2) // provide a unique ID used to ensure it is only shown once
+ .withoutShape() // no shape on map view since there are no view to focus on
+ .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
+ .setListener(new IShowcaseListener() {
+ @Override
+ public void onShowcaseDisplayed(MaterialShowcaseView materialShowcaseView) {
+
+ }
+
+ @Override
+ public void onShowcaseDismissed(MaterialShowcaseView materialShowcaseView) {
+ /* Add other nearbyMaterialShowcaseSequence here, it will make the user feel as they are a
+ * nearbyMaterialShowcaseSequence whole together.
+ * */
+ secondSingleShowCaseView.show(NearbyActivity.this);
+ }
+ })
+ .build();
+
+ if (applicationPrefs.getBoolean("firstRunNearby", true)) {
+ applicationPrefs.edit().putBoolean("firstRunNearby", false).apply();
+ firstSingleShowCaseView.show(this);
+ }
}
private void lockNearbyView(boolean lock) {
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java
index 431132436..69041d286 100644
--- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java
@@ -7,6 +7,7 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.graphics.Color;
+import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
@@ -58,13 +59,14 @@ import fr.free.nrw.commons.contributions.ContributionController;
import fr.free.nrw.commons.utils.UriDeserializer;
import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
+import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView;
import static android.app.Activity.RESULT_OK;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
public class NearbyMapFragment extends DaggerFragment {
- private MapView mapView;
+ public MapView mapView;
private List baseMarkerOptions;
private fr.free.nrw.commons.location.LatLng curLatLng;
public fr.free.nrw.commons.location.LatLng[] boundaryCoordinates;
@@ -111,6 +113,10 @@ public class NearbyMapFragment extends DaggerFragment {
private final double CAMERA_TARGET_SHIFT_FACTOR_PORTRAIT = 0.06;
private final double CAMERA_TARGET_SHIFT_FACTOR_LANDSCAPE = 0.04;
+ private boolean isSecondMaterialShowcaseDismissed;
+ private boolean isMapReady;
+ private MaterialShowcaseView thirdSingleShowCaseView;
+
private Bundle bundleForUpdtes;// Carry information from activity about changed nearby places and current location
@Inject
@@ -163,7 +169,6 @@ public class NearbyMapFragment extends DaggerFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
-
Timber.d("onCreateView called");
if (curLatLng != null) {
Timber.d("curLatLng found, setting up map view...");
@@ -476,6 +481,7 @@ public class NearbyMapFragment extends DaggerFragment {
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(MapboxMap mapboxMap) {
+ ((NearbyActivity)getActivity()).setMapViewTutorialShowCase();
NearbyMapFragment.this.mapboxMap = mapboxMap;
updateMapSignificantly();
}
@@ -519,6 +525,7 @@ public class NearbyMapFragment extends DaggerFragment {
private void addNearbyMarkerstoMapBoxMap() {
mapboxMap.addMarkers(baseMarkerOptions);
+
mapboxMap.setOnInfoWindowCloseListener(marker -> {
if (marker == selected) {
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
@@ -534,6 +541,7 @@ public class NearbyMapFragment extends DaggerFragment {
});
mapboxMap.setOnMarkerClickListener(marker -> {
+
if (marker instanceof NearbyMarker) {
this.selected = marker;
NearbyMarker nearbyMarker = (NearbyMarker) marker;
@@ -541,6 +549,7 @@ public class NearbyMapFragment extends DaggerFragment {
passInfoToSheet(place);
bottomSheetListBehavior.setState(BottomSheetBehavior.STATE_HIDDEN);
bottomSheetDetailsBehavior.setState(BottomSheetBehavior.STATE_COLLAPSED);
+
}
return false;
});
@@ -634,7 +643,19 @@ public class NearbyMapFragment extends DaggerFragment {
addAnchorToSmallFABs(fabGallery, getActivity().findViewById(R.id.empty_view).getId());
addAnchorToSmallFABs(fabCamera, getActivity().findViewById(R.id.empty_view1).getId());
+ thirdSingleShowCaseView = new MaterialShowcaseView.Builder(this.getActivity())
+ .setTarget(fabPlus)
+ .setDismissText(getString(R.string.showcase_view_got_it_button))
+ .setContentText(getString(R.string.showcase_view_plus_fab))
+ .setDelay(500) // optional but starting animations immediately in onCreate can make them choppy
+ .singleUse(ViewUtil.SHOWCASE_VIEW_ID_3) // provide a unique ID used to ensure it is only shown once
+ .setDismissStyle(Typeface.defaultFromStyle(Typeface.BOLD))
+ .build();
+ isMapReady = true;
+ if (isSecondMaterialShowcaseDismissed) {
+ thirdSingleShowCaseView.show(getActivity());
+ }
}
@@ -791,6 +812,13 @@ public class NearbyMapFragment extends DaggerFragment {
this.bundleForUpdtes = bundleForUpdtes;
}
+ public void onNearbyMaterialShowcaseDismissed() {
+ isSecondMaterialShowcaseDismissed = true;
+ if (isMapReady) {
+ thirdSingleShowCaseView.show(getActivity());
+ }
+ }
+
@Override
public void onStart() {
diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMaterialShowcaseSequence.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMaterialShowcaseSequence.java
new file mode 100644
index 000000000..c6e46611d
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMaterialShowcaseSequence.java
@@ -0,0 +1,18 @@
+package fr.free.nrw.commons.nearby;
+
+import android.app.Activity;
+
+import uk.co.deanwild.materialshowcaseview.MaterialShowcaseSequence;
+import uk.co.deanwild.materialshowcaseview.ShowcaseConfig;
+
+
+public class NearbyMaterialShowcaseSequence extends MaterialShowcaseSequence {
+
+ public NearbyMaterialShowcaseSequence(Activity activity, String sequenceID) {
+ super(activity, sequenceID);
+ ShowcaseConfig config = new ShowcaseConfig();
+ config.setDelay(500); // half second between each showcase view
+ this.setConfig(config);
+ this.singleUse(sequenceID); // Display tutorial only once
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
index acd9b7646..4a7322b57 100644
--- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java
@@ -23,12 +23,11 @@ import fr.free.nrw.commons.AboutActivity;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
-import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.AccountUtil;
import fr.free.nrw.commons.auth.LoginActivity;
import fr.free.nrw.commons.contributions.ContributionsActivity;
-import fr.free.nrw.commons.featured.FeaturedImagesActivity;
+import fr.free.nrw.commons.category.CategoryImagesActivity;
import fr.free.nrw.commons.nearby.NearbyActivity;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.settings.SettingsActivity;
@@ -37,6 +36,8 @@ import timber.log.Timber;
public abstract class NavigationBaseActivity extends BaseActivity
implements NavigationView.OnNavigationItemSelectedListener {
+ private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons";
+
@BindView(R.id.toolbar)
Toolbar toolbar;
@BindView(R.id.navigation_view)
@@ -157,7 +158,7 @@ public abstract class NavigationBaseActivity extends BaseActivity
return true;
case R.id.action_featured_images:
drawerLayout.closeDrawer(navigationView);
- startActivityWithFlags(this, FeaturedImagesActivity.class, Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_featured_images), FEATURED_IMAGES_CATEGORY);
return true;
default:
Timber.e("Unknown option [%s] selected from the navigation menu", itemId);
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java
index c7f3c39c0..4390bcef4 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java
@@ -33,6 +33,7 @@ import dagger.android.support.AndroidSupportInjection;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.contributions.Contribution;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
+import fr.free.nrw.commons.utils.ViewUtil;
public class MultipleUploadListFragment extends Fragment {
@@ -129,7 +130,7 @@ public class MultipleUploadListFragment extends Fragment {
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getActivity().getCurrentFocus();
- hideKeyboard(target);
+ ViewUtil.hideKeyboard(target);
}
// FIXME: Wrong result type
@@ -178,22 +179,13 @@ public class MultipleUploadListFragment extends Fragment {
baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
return view;
}
- public void hideKeyboard(View view) {
- if (view != null) {
- InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
- if (inputMethodManager != null) {
- inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
- }
- }
- }
-
@Override
public void onDestroyView() {
baseTitle.removeTextChangedListener(textWatcher);
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java
index aea703f5e..6a59c8e30 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java
@@ -78,10 +78,10 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.mwapi.MediaWikiApi;
+import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
-
-
+import android.support.design.widget.FloatingActionButton;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
import static java.lang.Long.min;
@@ -121,6 +121,7 @@ public class ShareActivity
private Uri mediaUri;
private Contribution contribution;
private SimpleDraweeView backgroundImageView;
+ private FloatingActionButton maps_fragment;
private boolean cacheFound;
@@ -144,6 +145,8 @@ public class ShareActivity
private long ShortAnimationDuration;
private FloatingActionButton zoomInButton;
private FloatingActionButton zoomOutButton;
+ private FloatingActionButton mainFab;
+ private boolean isFABOpen = false;
/**
@@ -283,6 +286,24 @@ public class ShareActivity
if (mediaUri != null) {
backgroundImageView.setImageURI(mediaUri);
}
+
+ mainFab = (FloatingActionButton) findViewById(R.id.main_fab);
+ /*
+ * called when upper arrow floating button
+ */
+ mainFab.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if(!isFABOpen){
+ showFABMenu();
+ }else{
+ closeFABMenu();
+ }
+ }
+ });
+
+
+
zoomInButton = (FloatingActionButton) findViewById(R.id.media_upload_zoom_in);
try {
zoomInButton.setOnClickListener(new View.OnClickListener() {
@@ -357,7 +378,74 @@ public class ShareActivity
.commitAllowingStateLoss();
}
uploadController.prepareService();
+ maps_fragment = (FloatingActionButton) findViewById(R.id.media_map);
+ maps_fragment.setVisibility(View.VISIBLE);
+ if( imageObj == null || imageObj.imageCoordsExists != true){
+ maps_fragment.setVisibility(View.INVISIBLE);
+ }
+
+
+ maps_fragment.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if( imageObj != null && imageObj.imageCoordsExists == true) {
+ Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + imageObj.getDecLatitude() + "," + imageObj.getDecLongitude());
+ Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
+ mapIntent.setPackage("com.google.android.apps.maps");
+ startActivity(mapIntent);
+ }
+ }
+ });
}
+ /*
+ * Function to display the zoom and map FAB
+ */
+ private void showFABMenu(){
+ isFABOpen=true;
+
+ if( imageObj != null && imageObj.imageCoordsExists == true)
+ maps_fragment.setVisibility(View.VISIBLE);
+ zoomInButton.setVisibility(View.VISIBLE);
+
+ mainFab.animate().rotationBy(180);
+ maps_fragment.animate().translationY(-getResources().getDimension(R.dimen.second_fab));
+ zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab));
+ }
+
+ /*
+ * function to close the zoom and map FAB
+ */
+ private void closeFABMenu(){
+ isFABOpen=false;
+ mainFab.animate().rotationBy(-180);
+ maps_fragment.animate().translationY(0);
+ zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() {
+ @Override
+ public void onAnimationStart(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationEnd(Animator animator) {
+ if(!isFABOpen){
+ maps_fragment.setVisibility(View.GONE);
+ zoomInButton.setVisibility(View.GONE);
+ }
+
+ }
+
+ @Override
+ public void onAnimationCancel(Animator animator) {
+
+ }
+
+ @Override
+ public void onAnimationRepeat(Animator animator) {
+
+ }
+ });
+ }
+
@Override
public void onRequestPermissionsResult(int requestCode,
@@ -460,6 +548,9 @@ public class ShareActivity
detectUnwantedPicturesAsync.execute();
}
+ /*
+ * to display permission snackbar in share activity
+ */
private Snackbar requestPermissionUsingSnackBar(String rationale,
final String[] perms,
final int code) {
@@ -692,7 +783,9 @@ public class ShareActivity
return super.onOptionsItemSelected(item);
}
- // Get SHA1 of file from input stream
+ /*
+ * Get SHA1 of file from input stream
+ */
private String getSHA1(InputStream is) {
MessageDigest digest;
@@ -729,13 +822,18 @@ public class ShareActivity
}
}
+ /*
+ * function to provide pinch zoom
+ */
private void zoomImageFromThumb(final View thumbView, Uri imageuri ) {
// If there's an animation in progress, cancel it
// immediately and proceed with this one.
if (CurrentAnimator != null) {
CurrentAnimator.cancel();
}
- hideKeyboard(ShareActivity.this);
+ ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit | R.id.descEdit));
+ closeFABMenu();
+ mainFab.setVisibility(View.GONE);
InputStream input = null;
Bitmap scaled = null;
try {
@@ -865,7 +963,7 @@ public class ShareActivity
CurrentAnimator.cancel();
}
zoomOutButton.setVisibility(View.GONE);
- zoomInButton.setVisibility(View.VISIBLE);
+ mainFab.setVisibility(View.VISIBLE);
// Animate the four positioning/sizing properties in parallel,
// back to their original values.
@@ -905,12 +1003,5 @@ public class ShareActivity
});
}
- public static void hideKeyboard(Activity activity) {
- View view = activity.findViewById(R.id.titleEdit | R.id.descEdit);
- if (view != null) {
- InputMethodManager imm = (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE);
- imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
- }
- }
}
diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java
index 56d443d08..a32fb7b42 100644
--- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java
+++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java
@@ -1,8 +1,6 @@
package fr.free.nrw.commons.upload;
import android.annotation.SuppressLint;
-import android.app.Activity;
-
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
@@ -11,12 +9,10 @@ import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v4.view.ViewCompat;
import android.support.v7.app.AlertDialog;
-
import android.text.Editable;
import android.text.Html;
import android.text.TextWatcher;
import android.text.method.LinkMovementMethod;
-
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -24,7 +20,6 @@ import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.Button;
@@ -47,9 +42,9 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.settings.Prefs;
+import fr.free.nrw.commons.utils.ViewUtil;
import timber.log.Timber;
-import static android.view.MotionEvent.ACTION_DOWN;
import static android.view.MotionEvent.ACTION_UP;
public class SingleUploadFragment extends CommonsDaggerSupportFragment {
@@ -168,13 +163,13 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
if (!hasFocus) {
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
descEdit.setOnFocusChangeListener((v, hasFocus) -> {
if(!hasFocus){
- hideKeyboard(v);
+ ViewUtil.hideKeyboard(v);
}
});
@@ -183,15 +178,6 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
return rootView;
}
- public void hideKeyboard(View view) {
- if (view != null) {
- InputMethodManager inputMethodManager = (InputMethodManager) getActivity().getSystemService(Activity.INPUT_METHOD_SERVICE);
- if (inputMethodManager != null) {
- inputMethodManager.hideSoftInputFromWindow(view.getWindowToken(), 0);
- }
- }
- }
-
@Override
public void onDestroyView() {
titleEdit.removeTextChangedListener(textWatcher);
@@ -305,7 +291,7 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment {
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
View target = getActivity().getCurrentFocus();
- hideKeyboard(target);
+ ViewUtil.hideKeyboard(target);
}
@NonNull
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java
new file mode 100644
index 000000000..b05c8bc45
--- /dev/null
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ContinueUtils.java
@@ -0,0 +1,15 @@
+package fr.free.nrw.commons.utils;
+
+import org.w3c.dom.Element;
+import org.w3c.dom.Node;
+
+import fr.free.nrw.commons.category.QueryContinue;
+
+public class ContinueUtils {
+
+ public static QueryContinue getQueryContinue(Node document) {
+ Element continueElement = (Element) document;
+ return new QueryContinue(continueElement.getAttribute("continue"),
+ continueElement.getAttribute("gcmcontinue"));
+ }
+}
diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
index b4b26746b..0c22a40a2 100644
--- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
+++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java
@@ -5,10 +5,15 @@ import android.content.Context;
import android.support.design.widget.Snackbar;
import android.view.Display;
import android.view.View;
+import android.view.inputmethod.InputMethodManager;
import android.widget.Toast;
public class ViewUtil {
+ public static final String SHOWCASE_VIEW_ID_1 = "SHOWCASE_VIEW_ID_1";
+ public static final String SHOWCASE_VIEW_ID_2 = "SHOWCASE_VIEW_ID_2";
+ public static final String SHOWCASE_VIEW_ID_3 = "SHOWCASE_VIEW_ID_3";
+
public static void showSnackbar(View view, int messageResourceId) {
Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show();
}
@@ -27,4 +32,14 @@ public class ViewUtil {
}
}
+ public static void hideKeyboard(View view){
+ if (view != null) {
+ InputMethodManager manager = (InputMethodManager) view.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
+ view.clearFocus();
+ if (manager != null) {
+ manager.hideSoftInputFromWindow(view.getWindowToken(), 0);
+ }
+ }
+ }
+
}
diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up_black_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up_black_24dp.xml
new file mode 100644
index 000000000..bc010396b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_keyboard_arrow_up_black_24dp.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_share_black_24dp.xml b/app/src/main/res/drawable/ic_share_black_24dp.xml
index 01c81322d..203b1d84c 100644
--- a/app/src/main/res/drawable/ic_share_black_24dp.xml
+++ b/app/src/main/res/drawable/ic_share_black_24dp.xml
@@ -1,5 +1,5 @@
-
+ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
diff --git a/app/src/main/res/layout/activity_featured_images.xml b/app/src/main/res/layout/activity_category_images.xml
similarity index 69%
rename from app/src/main/res/layout/activity_featured_images.xml
rename to app/src/main/res/layout/activity_category_images.xml
index 755fe4983..c329e4458 100644
--- a/app/src/main/res/layout/activity_featured_images.xml
+++ b/app/src/main/res/layout/activity_category_images.xml
@@ -1,6 +1,5 @@
-
-
diff --git a/app/src/main/res/layout/activity_share.xml b/app/src/main/res/layout/activity_share.xml
index ca8097495..b6e523239 100644
--- a/app/src/main/res/layout/activity_share.xml
+++ b/app/src/main/res/layout/activity_share.xml
@@ -41,8 +41,6 @@
-
-
+
+
+
+
+
+
+
-
+ android:background="?attr/mainBackground">
@@ -33,7 +33,7 @@
android:padding="@dimen/small_gap"
>
Vermeide urheberrechtlich geschütztes Material, das du im Internet gefunden hast wie Bilder von Postern, Buchcovern etc.
Verstanden?
Ja!
+
Kategorien
Lade …
Keine ausgewählt
@@ -268,4 +269,5 @@
Fortfahren
Abbrechen
Erneut versuchen
+ App teilen
diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml
index b88e412e7..2b66709f0 100644
--- a/app/src/main/res/values-diq/strings.xml
+++ b/app/src/main/res/values-diq/strings.xml
@@ -83,7 +83,7 @@
Peyd rışten bırış (E-posta ra)
E-posta eyar nêbi
Karıyaye Kategoriyê peyêni
- Fına
+ Anciya bıcerrebne
Bıtexelne
Ron
Lisans
@@ -129,4 +129,5 @@
Keye
Bar ke
Veciyayış
+ Anciya bıcerrebne
diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml
index 759c44920..0dae5c65e 100644
--- a/app/src/main/res/values-el/strings.xml
+++ b/app/src/main/res/values-el/strings.xml
@@ -158,6 +158,7 @@
Αποφύγετε προστατευμένο υλικό που βρήκατε από το Internet, καθώς και εικόνες, αφίσες, εξώφυλλα βιβλίων, κλπ.
Τι λες, μπορείς;
Ναι!
+
Κατηγορίες
Φόρτωση…
Καμία επιλεγμένη
@@ -272,4 +273,5 @@
Συνέχεια
Ακύρωση
Ξαναπροσπαθήστε
+ Κοινοποίηση εφαρμογής
diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml
index 8494a7a36..442fc6bc5 100644
--- a/app/src/main/res/values-fr/strings.xml
+++ b/app/src/main/res/values-fr/strings.xml
@@ -279,4 +279,5 @@
Continuer
Annuler
Réessayer
+ Partager les applications
diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml
index e7c3a37c2..630311db4 100644
--- a/app/src/main/res/values-gl/strings.xml
+++ b/app/src/main/res/values-gl/strings.xml
@@ -90,6 +90,7 @@
Categorías
Configuracións
Rexistrarse
+ Imaxes destacadas
Acerca de
A aplicación Wikimedia Commons é unha aplicación de código aberto creada e mantida polos cesionarios e voluntarios da comunidade de Wikimedia. A Fundación Wikimedia non está involucrada na creación, desenvolvemento ou mantemento da aplicación.
Crear unha nova <a href=\"https://github.com/commons-app/apps-android-commons/issues\">incidencia</a> para informar de problemas e suxestións.
@@ -175,6 +176,8 @@
Título do ficheiro multimedia
Descrición
Aquí vai a descrición do ficheiro multimedia. Potencialmente, pode ser bastante longo, e necesitará agruparse en múltiples liñas. De tódolos xeitos esperamos que se vexa ben.
+ Autor
+ O nome de usuario do autor da imaxe destacada vai aquí.
Data de suba
Licenza
Coordenadas
@@ -217,6 +220,7 @@
Saír
Titorial
Notificacións
+ Destacados
Os sitios situados preto non poden visualizarse sen permisos de localización
non se atopou descrición
Páxina do ficheiro en Commons
diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml
index 5524afb0e..81e655b95 100644
--- a/app/src/main/res/values-hu/strings.xml
+++ b/app/src/main/res/values-hu/strings.xml
@@ -13,6 +13,7 @@
* ViDam
-->
+ Megjelenés
Általános
Visszajelzés
Helyszín
@@ -145,6 +146,7 @@
Híres emberek (a polgármestered, olimpikonok, akikkel találkoztál)
Kérjük, NE tölts fel:
- Szelfiket vagy képeket a barátaidról\n- Internetröl letöltött képeket\n- Kereskedelmi alkalmazások képernyőképeit
+ Az Internetről letöltött képek
Példa feltöltés:
- Cím: Sydney-i Operaház\n- Leírás: A Sydney-i Operaház az öböl túlpartjáról\n- Kategóriák: Sydney Opera House from the west, Sydney Opera House remote views
Cím: Sydney-i Operaház
@@ -191,6 +193,7 @@
Commons Logo
Commons weboldal
Commons Facebook-oldal
+ Commons Github forráskód
Háttérkép
Nem található kép
Kép feltöltése
@@ -223,6 +226,8 @@
Hiba a képek gyorsítótárazásakor
Egy egyedi, leíró cím a fájlnak, ami fájlnévként fog szolgálni. Egyszerű nyelvezetet használhatsz szóközökkel. Ne tedd bele a kiterjesztést.
Kérlek a lehető legteljesebb módon írd le a fájlt: hol készült, mit ábrázol, mi a kontextus? Kérlek add meg az objektumokat vagy személyeket a képen, valamint a nehezen kitalálható információkat (például a kép készítésének dátumát, ha az egy tájkép). Amennyiben a média valami szokatlant ábrázol, kérlek fejtsd ki, hogy mi teszi szokatlanná.
+ Ez a fénykép túl sötét, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg.
+ Ez a fénykép homályos, biztos fel akarod tölteni? A Wikimédia Commons csak enciklopédikus értékkel bíró képeket tart meg.
Engedély adása
Külső tárhely használata
Az alkalmazáson belüli kamerával készült képek mentése az eszközre
@@ -238,6 +243,7 @@
A hely nem érhető el.
Közeli helyek listájának megtekintéséhez engedély szükséges
Üdvözlünk a Wikimedia Commonson, %1$s! Örülünk, hogy itt vagy.
+ %1$s üzenetet hagyott a vitalapodon
Köszönjük a szerkesztésedet!
WIKIDATA
WIKIPÉDIA
@@ -249,6 +255,8 @@
Internet elérhető
Nincs értesítés
Nyelvek
+ Folytatás
Mégse
Újra
+ Alkalmazás megosztása
diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml
index 48750daf9..762a3be31 100644
--- a/app/src/main/res/values-iw/strings.xml
+++ b/app/src/main/res/values-iw/strings.xml
@@ -273,4 +273,5 @@
המשך
ביטול
לנסות שוב
+ שיתוף היישום
diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml
index 11faee9c6..d0e1c21ed 100644
--- a/app/src/main/res/values-ko/strings.xml
+++ b/app/src/main/res/values-ko/strings.xml
@@ -266,4 +266,5 @@
진행
취소
다시 시도
+ 앱 공유
diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml
index dd1e6f03e..4e44dad60 100644
--- a/app/src/main/res/values-mk/strings.xml
+++ b/app/src/main/res/values-mk/strings.xml
@@ -263,4 +263,5 @@
Продолжи
Откажи
Пробај пак
+ Сподели прилог
diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml
index 485e66acc..632cc0265 100644
--- a/app/src/main/res/values-ml/strings.xml
+++ b/app/src/main/res/values-ml/strings.xml
@@ -5,15 +5,23 @@
* Santhosh.thottingal
-->
- വിക്കിമീഡിയ കോമൺസ്
+ ദൃശ്യരൂപം
+ സാർവത്രികം
+ പ്രതികരണം
+ സ്ഥലം
+ കോമൺസ്
സജ്ജീകരണങ്ങൾ
ഉപയോക്തൃനാമം
രഹസ്യവാക്ക്
+ താങ്കളുടെ കോമൺസ് ബീറ്റ അംഗത്വത്തിൽ പ്രവേശിക്കുക
പ്രവേശിക്കുക
+ രഹസ്യവാക്ക് മറന്നോ?
+ അംഗത്വമെടുക്കുക
പ്രവേശിക്കുന്നു
ദയവായി കാത്തിരിക്കുക…
പ്രവേശനം വിജയകരം!
പ്രവേശനം പരാജയപ്പെട്ടു!
+ പ്രമാണം കണ്ടെത്താനായില്ല. ദയവായി മറ്റൊരു പ്രമാണം നോക്കുക.
സാധുതാനിർണ്ണയം പരാജയപ്പെട്ടു!
അപ്ലോഡ് തുടങ്ങി!
%1$s അപ്ലോഡ് ചെയ്തിരിക്കുന്നു!
@@ -23,27 +31,30 @@
%1$s അപ്ലോഡിങ് പൂർത്തിയാക്കുന്നു
%1$s അപ്ലോഡിങ് പരാജയപ്പെട്ടു
കാണാനായി ടാപ് ചെയ്യുക
-
- - 1 പ്രമാണം അപ്ലോഡ് ചെയ്യുന്നു
+
+ - ഒരു പ്രമാണം അപ്ലോഡ് ചെയ്യുന്നു
- %1$d പ്രമാണങ്ങൾ അപ്ലോഡ് ചെയ്യുന്നു
- എന്റെ അപ്ലോഡുകൾ
+ എന്റെ സമീപകാല അപ്ലോഡുകൾ
നിരയായി വെച്ചു
പരാജയപ്പെട്ടു
%1$d%% പൂർണ്ണം
അപ്ലോഡ് ചെയ്തുകൊണ്ടിരിക്കുന്നു
ചിത്രശാല
ചിത്രം എടുക്കുക
+ സമീപസ്ഥം
എന്റെ അപ്ലോഡുകൾ
പങ്ക് വെയ്ക്കുക
ബ്രൗസറിൽ കാണുക
തലക്കെട്ട്
+ ഈ പ്രമാണത്തിന് ഒരു തലക്കെട്ട് നൽകുക.
വിവരണം
പ്രവേശിക്കാനായില്ല - നെറ്റ്വർക്ക് പരാജയപ്പെട്ടു
പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ ഉപയോക്തൃനാമം പരിശോധിക്കുക
പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ രഹസ്യവാക്ക് പരിശോധിക്കുക
- നിരവധി വിജയകരമല്ലാത്ത ശ്രമങ്ങൾ നടന്നിരിക്കുന്നു. വീണ്ടും ശ്രമിക്കുന്നതിനു മുമ്പ് ഏതാനം മിനിറ്റുകൾ വിശ്രമിക്കുക
+ നിരവധി വിജയകരമല്ലാത്ത ശ്രമങ്ങൾ നടന്നിരിക്കുന്നു. വീണ്ടും ശ്രമിക്കുന്നതിനു മുമ്പ് ഏതാനം മിനിറ്റുകൾ വിശ്രമിക്കുക.
ക്ഷമിക്കുക, ഈ ഉപയോക്താവ് കോമൺസിൽ നിന്ന് തടയപ്പെട്ടിരിക്കുകയാണ്
+ താങ്കളുടെ ദ്വി-ഘടക സാധൂകരണ കോഡ് നൽകുക.
പ്രവേശനം പരാജയപ്പെട്ടു
അപ്ലോഡ്
ഈ ഗണത്തിന് പേരിടുക
@@ -51,6 +62,11 @@
അപ്ലോഡ്
വർഗ്ഗങ്ങളിൽ തിരയുക
സേവ് ചെയ്യുക
+ പുതുക്കുക
+ പട്ടിക
+ താങ്കളുടെ ഉപകരണത്തിൽ ജി.പി.എസ്. പ്രവർത്തനരഹിതമാണ്. അത് പ്രവർത്തനസജ്ജമാക്കണോ?
+ ജി.പി.എസ്. സജ്ജമാക്കുക
+ ഇതുവരെ അപ്ലോഡുകൾ ഒന്നുമില്ല
- ഒരു അപ്ലോഡും ചെയ്തില്ല
- ഒരു അപ്ലോഡ്
@@ -68,6 +84,8 @@
താങ്കളുടെ ചിത്രങ്ങൾ വിക്കിമീഡിയ കോമൺസിൽ കൂടുതൽ എളുപ്പത്തിൽ കണ്ടെത്തപ്പെടാനായി വർഗ്ഗങ്ങൾ ചേർക്കുക.\n\nവർഗ്ഗങ്ങൾ ചേർക്കാനായി ടൈപ്പ് ചെയ്ത് തുടങ്ങുക.\nഈ ഘട്ടം ഒഴിവാക്കാൻ ടാപ് ചെയ്യുക (അല്ലെങ്കിൽ പിന്നോട്ട് പോവുക).
വർഗ്ഗങ്ങൾ
സജ്ജീകരണങ്ങൾ
+ അംഗത്വമെടുക്കുക
+ തിരഞ്ഞെടുത്ത ചിത്രങ്ങൾ
വിവരണം
<a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">അപാച്ചേ അനുമതിപത്രം പതിപ്പ് 2</a> പ്രകാരം പുറത്തിറക്കപ്പെട്ട ഓപ്പൺ സോഴ്സ് സോഫ്റ്റ്വേർ
സ്രോതസ്സ് രൂപം <a href=\"https://github.com/commons-app/apps-android-commons\">ജിറ്റ്ഹബിൽ</a> ലഭ്യമാണ്.\nപ്രശ്നങ്ങൾ <a href=\" https://github.com/commons-app/apps-android-commons/issues\">ബഗ്സില്ലയിൽ</a> അറിയിക്കുക.
@@ -81,9 +99,11 @@
റദ്ദാക്കുക
ചിത്രം %1$s പ്രകാരം അനുമതി നൽകപ്പെടുന്നതാണ്
ഡൗൺലോഡ്
- അനുമതി
- സി.സി. ആട്രിബ്യൂഷൻ-ഷെയർഎലൈക് 3.0
- സി.സി. ആട്രിബ്യൂഷൻ 3.0
+ സ്വതേയുള്ള ഉപയോഗാനുമതി
+ ആട്രിബ്യൂഷൻ-ഷെയർഎലൈക് 4.0
+ ആട്രിബ്യൂഷൻ 4.0
+ ആട്രിബ്യൂഷൻ-ഷെയർഎലൈക് 3.0
+ ആട്രിബ്യൂഷൻ 3.0
സി.സി.0
സി.സി. ബൈ-എസ്.എ. 3.0
സി.സി. ബൈ-എസ്.എ. 3.0 (ഓസ്ട്രിയ)
@@ -97,6 +117,8 @@
സി.സി. ബൈ-എസ്.എ. 3.0 (പോളണ്ട്)
സി.സി. ബൈ-എസ്.എ. 3.0 (റൊമേനിയ)
സി.സി. ബൈ 3.0
+ സി.സി. ബൈ-എസ്.എ. 4.0
+ സി.സി. ബൈ 4.0
സി.സി. സീറോ
താങ്കളെടുക്കുന്ന ചിത്രങ്ങൾ സംഭാവന ചെയ്യുക. വിക്കിപീഡിയ ലേഖനങ്ങൾ ജീവസ്സുറ്റതാക്കിത്തീർക്കുക!
വിക്കിപീഡിയയിലുള്ള ചിത്രങ്ങൾ വിക്കിമീഡിയ കോമൺസിൽ നിന്നാണ്
@@ -105,9 +127,63 @@
മനസ്സിലായോ?
ശരി!
വർഗ്ഗങ്ങൾ
- ശേഖരിക്കുന്നു…
+ ശേഖരിക്കുന്നു…
ഒന്നും തിരഞ്ഞെടുത്തിട്ടില്ല
വിവരണമൊന്നുമില്ല
അജ്ഞാതമായ അനുമതി
പുതുക്കുക
+ ശരി
+ സമീപ സ്ഥലങ്ങൾ
+ മുന്നറിയിപ്പ്
+ ഈ പ്രമാണം കോമൺസിൽ നിലവിലുണ്ട്. തുടരണം എന്ന് താങ്കൾക്കുറപ്പാണോ?
+ അതെ
+ അല്ല
+ ശീർഷകം
+ മീഡിയയുടെ തലക്കെട്ട്
+ വിവരണം
+ സ്രഷ്ടാവ്
+ അപ്ലോഡ് ചെയ്ത തീയതി
+ ഉപയോഗാനുമതി
+ നിർദ്ദേശാങ്കങ്ങൾ
+ ഒന്നും നൽകിയിട്ടില്ല
+ കോമൺസ് ലോഗോ
+ കോമൺസ് വെബ്സൈറ്റ്
+ പശ്ചാത്തല ചിത്രം
+ ചിത്രം അപ്ലോഡ് ചെയ്യുക
+ സാവോ പർവ്വതം
+ ലാമകൾ
+ മഴവിൽ പാലം
+ തുലിപ്
+ സെൽഫികൾ വേണ്ട
+ പകർപ്പവകാശ സംരക്ഷിത ചിത്രം
+ സിഡ്നി ഓപെറാ ഹൗസ്
+ റദ്ദാക്കുക
+ തുറക്കുക
+ അടയ്ക്കുക
+ പ്രധാനം
+ അപ്ലോഡ്
+ സമീപസ്ഥം
+ വിവരണം
+ സജ്ജീകരണങ്ങൾ
+ പ്രതികരണം
+ ലോഗൗട്ട്
+ സഹായം
+ അറിയിപ്പുകൾ
+ തിരഞ്ഞെടുക്കപ്പെട്ടത്
+ വിവരണങ്ങൾ ഒന്നും കണ്ടെത്തിയില്ല
+ കോമൺസ് പ്രമാണ താൾ
+ വിക്കിഡേറ്റാ ഇനം
+ വിക്കിപീഡിയ ലേഖനം
+ അനുമതി നൽകുക
+ താങ്കളുടെ അംഗത്വത്തിൽ പ്രവേശിക്കുക
+ ബ്രൗസറിൽ കാണുക
+ വിക്കിഡേറ്റാ
+ വിക്കിപീഡിയ
+ കോമൺസ്
+ <u>പതിവുചോദ്യങ്ങൾ</u>
+ <u>പരിഭാഷപ്പെടുത്തുക</u>
+ ഭാഷകൾ
+ തുടരുക
+ റദ്ദാക്കുക
+ വീണ്ടും ശ്രമിക്കുക
diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml
index 0cd748031..30115f1ad 100644
--- a/app/src/main/res/values-pms/strings.xml
+++ b/app/src/main/res/values-pms/strings.xml
@@ -263,4 +263,5 @@
Andé anans
Anulé
Prové torna
+ Partagé j\'aplicassion
diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml
index 4bc478527..6722ae4bb 100644
--- a/app/src/main/res/values-pt-rBR/strings.xml
+++ b/app/src/main/res/values-pt-rBR/strings.xml
@@ -275,4 +275,5 @@
Avançar
Cancelar
Tentar novamente
+ Compartilhar o aplicativo
diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml
index 5e4862ecb..b0717f3dc 100644
--- a/app/src/main/res/values-pt/strings.xml
+++ b/app/src/main/res/values-pt/strings.xml
@@ -159,6 +159,7 @@
Evite materiais protegidos por direitos de autor que tenham sido encontrados na Internet, bem como imagens de cartazes, capas de livros, etc.
Acha que conseguiu?
Sim!
+
Categorias
A carregar…
Nenhuma selecionada
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
index c649398bb..4dc90489d 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -170,6 +170,7 @@
Избегайте материалов, защищённых авторским правом, например, найденных в Интернете, изображений плакатов, книжных обложек и т.п.
Вам это понятно?
Да!
+ *
Категории
Загрузка…
Ничего не выбрано
diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml
index fa5174ced..24ef60b61 100644
--- a/app/src/main/res/values-sv/strings.xml
+++ b/app/src/main/res/values-sv/strings.xml
@@ -156,6 +156,7 @@
Undvik upphovsrättsskyddat material som du hittar på Internet, samt bilder av affischer, bokomslag, etc.
Tror du att du förstår?
Ja!
+
Kategorier
Läser in…
Ingen markerad
@@ -270,4 +271,5 @@
Fortsätt
Avbryt
Försök igen
+ Dela app
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 4966d9c08..a2878cddf 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -271,4 +271,5 @@
已進行
取消
重試
+ 分享應用程式
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index 83650b408..d3d049ea2 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -269,4 +269,5 @@
已处理
取消
重试
+ 分享应用
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 1e98a1e04..7eaf97880 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -23,4 +23,6 @@
20sp
16sp
14sp
+ 15dp
+ 25dp
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index efdeeec7e..dcea51bcc 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -270,4 +270,17 @@
Proceed
Cancel
Retry
+
+ Got it!
+ These are the places near you that need pictures to illustrate their Wikipedia articles
+ Tapping this button brings up a list of these places
+ You can upload a picture for any place from your gallery or camera
+
+ No images found!
+ Error occurred while loading images.
+ Uploaded by: %1$s
+
+ Share App
+ Coordinates were not specified during image selection
+
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt
index 73760dd40..b1de29143 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt
@@ -3,6 +3,7 @@ package fr.free.nrw.commons
import android.content.Context
import android.content.SharedPreferences
import android.support.v4.util.LruCache
+import com.google.gson.Gson
import com.nhaarman.mockito_kotlin.mock
import com.squareup.leakcanary.RefWatcher
import fr.free.nrw.commons.auth.AccountUtil
@@ -36,6 +37,7 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
val accountUtil: AccountUtil = mock()
val appSharedPreferences: SharedPreferences = mock()
val defaultSharedPreferences: SharedPreferences = mock()
+ val categorySharedPreferences: SharedPreferences = mock()
val otherSharedPreferences: SharedPreferences = mock()
val uploadController: UploadController = mock()
val mockSessionManager: SessionManager = mock()
@@ -45,6 +47,7 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
val mockDbOpenHelper: DBOpenHelper = mock()
val nearbyPlaces: NearbyPlaces = mock()
val lruCache: LruCache = mock()
+ val gson: Gson = Gson()
override fun providesAccountUtil(context: Context): AccountUtil = accountUtil
@@ -58,7 +61,7 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu
override fun providesSessionManager(context: Context, mediaWikiApi: MediaWikiApi, sharedPreferences: SharedPreferences): SessionManager = mockSessionManager
- override fun provideMediaWikiApi(context: Context, sharedPreferences: SharedPreferences): MediaWikiApi = mediaWikiApi
+ override fun provideMediaWikiApi(context: Context, sharedPreferences: SharedPreferences, categorySharedPreferences: SharedPreferences, gson: Gson): MediaWikiApi = mediaWikiApi
override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager
diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt
index c51d354c2..686a90ef2 100644
--- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt
+++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt
@@ -3,6 +3,7 @@ package fr.free.nrw.commons.mwapi
import android.content.SharedPreferences
import android.os.Build
import android.preference.PreferenceManager
+import com.google.gson.Gson
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.TestCommonsApplication
import okhttp3.mockwebserver.MockResponse
@@ -26,12 +27,14 @@ class ApacheHttpClientMediaWikiApiTest {
private lateinit var testObject: ApacheHttpClientMediaWikiApi
private lateinit var server: MockWebServer
private lateinit var sharedPreferences: SharedPreferences
+ private lateinit var categoryPreferences: SharedPreferences
@Before
fun setUp() {
server = MockWebServer()
sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application)
- testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", sharedPreferences)
+ categoryPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application)
+ testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", sharedPreferences, categoryPreferences, Gson())
testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/")
}