mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Multiple uploads with over haul (#1968)
* Added new upload activity that receives shared files from the gallery. Cards show and hide, plus titles are correct. Displayed thumbnails for the shared images * Better handling of the view paging plus error handling for required fields. * Code cleanup to make things more readable. * Extracted a model from the category search fragment that can possibly be shared with the new upload activity. * Added category selection to the combined upload screen. * Cleanup before the home-stretch on the GUI. * Adding license selection. * Fixed build warnings + cleanup * Start to support the dark theme. * Work in progress to add quality checking. * Fixing merge. * GPSExtractor: optimized away the EXifInterface object * Implemented submit functionality, temporarily fixed jacoco crash by disabling DUMMY UploadView object. * Implemented uploading of categories along with the picture. The category screen now displays GPS and recent categories when nothing is searched. * Implemented caching of files. Did some work on picture quality detection. * Implemented too dark picture detection. * Added a side card for zoom and map buttons along with pretty animations for stuff. * Added duplicate image on commons checking and fixed files not getting proper file extensions in several places. * Added support for map button and switched in-app upload buttons to UploadActivity * Pretty pretty animations! * Implemented zoom functionality for th background image. Just pinching on the image works instead of requiring buttons. * Added multi-language descriptions with categories by region. * Reimplemented the duplicate title checker and implemented a check against putting the same language twice in the description. * Javadocs for Description and UploadPresenter, plus some general cleanup. * Small code changes. * Implemented login checks for the Upload screen. * Implement receiving data from Nearby. * Feature/permissions library (#1855) * Added permission for Dexter, the runtime permission handling library * [Preparing fir issue #1773] Added a utility function which would take the user to app settings screen where he could manually give us the required permission * Added an alert dialog with positive and negative callback [Preparing fir issue #1773] * Improvements in the way External Storage Permission is handled in MultipleShareActivity[Bug fix #1697] 1. Used dexter to handle the external storage permission 2. Behaviour changes : When user tries to share(uppload) images to commons via MultipleShareActivity, following decision tree is followed a. If the app has permission for external storage, normal upload operation is followed b. If the app does not has the permission for external storage, dexter is used to ask for the same c. If the user gives us the required permission, normal upload flow is proceeded d. If the doesnot gives us the required permission a rationale dialog is shown with the appropriate message to let him know why we need the permission e. If he presses okay, steps a-c are followed and if he presses cancel, we close the app. f. If while asking for permission, the user chooses never ask again, then next time he tries to upload an image via MSA, the rational dialog follows the app setting screen where he could manually give us the required permission and the onActivityResult of same is handled * Added a Constants class to handle request and result codes from one place and other related constants common to the all app elements * replaced hardcoded strings ok and cancel in DialogUtil to string resources * init permission rationale dialog in activities onCreate * Code formatting, updated access modifiers wherever required, added javadocs for new methods created * *shifted constants to app class *Added JavaDocs in PermissionUtils * removed class REQUEST_CODES from CommonsApplication and instead put the enclosing constants in the App class itself * Made Codacy happy. * Abstarcted permission acquisition into new class DexterPermissionObtainer * Fixed Nearby upload detection * Migrated bad picture detection from AsyncTask to RxJava. * Removed ShareActivity and related dead code * Removed dead or duplicate code from FileProcessor * Added info button to title EditText * Fixed the add description button not disappearing. Added "Starting Upload" toast. Added link to the license on final screen. Made it so that the map button is hidden when image lacks gps coords. * Support in app multiple uploads * Minor changes to fix build * Changes to fix pending issues with upload flow * Fix display of similar image fragment * When uploading several files at once the date is missing #1854 (#2) * Bug fix issue #1854 * updated ContributionsDao to save create date, which it was not doing currently [it was instead saving current date] * UploadItem accepts are dateCreated param * Added a function in UploadModel, getFileCreatedDate which tries to fetched the file creaction date from all possible content providers. * Fix pending issues in upload flow * Make multiple uploads work for Google Photos * Fix default state for upload activity * Fix keyboard state for license screen * Fix descriptions for uploads * wip * Fix language spinner
This commit is contained in:
parent
4930a82ea2
commit
f607c1c14d
139 changed files with 4012 additions and 3212 deletions
|
|
@ -31,6 +31,7 @@ dependencies {
|
||||||
transitive = true
|
transitive = true
|
||||||
}
|
}
|
||||||
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0'
|
||||||
|
|
||||||
//noinspection GradleCompatible
|
//noinspection GradleCompatible
|
||||||
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION"
|
||||||
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION"
|
||||||
|
|
@ -43,6 +44,7 @@ dependencies {
|
||||||
implementation 'com.squareup.okio:okio:1.14.0'
|
implementation 'com.squareup.okio:okio:1.14.0'
|
||||||
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
implementation 'io.reactivex.rxjava2:rxandroid:2.1.0'
|
||||||
// Because RxAndroid releases are few and far between, it is recommended you also
|
// Because RxAndroid releases are few and far between, it is recommended you also
|
||||||
|
|
||||||
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
// explicitly depend on RxJava's latest version for bug fixes and new features.
|
||||||
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
|
implementation 'io.reactivex.rxjava2:rxjava:2.2.0'
|
||||||
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
implementation 'com.jakewharton.rxbinding2:rxbinding:2.1.1'
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.support.test.InstrumentationRegistry;
|
|
||||||
import android.support.test.runner.AndroidJUnit4;
|
|
||||||
|
|
||||||
import org.junit.Test;
|
|
||||||
import org.junit.runner.RunWith;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
|
|
||||||
import static org.hamcrest.CoreMatchers.is;
|
|
||||||
import static org.junit.Assert.assertThat;
|
|
||||||
|
|
||||||
@RunWith(AndroidJUnit4.class)
|
|
||||||
public class FileUtilsTest {
|
|
||||||
@Test
|
|
||||||
public void isSelfOwned() throws Exception {
|
|
||||||
Uri uri = Uri.parse("content://" + BuildConfig.APPLICATION_ID + ".provider/document/1");
|
|
||||||
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
|
||||||
assertThat(selfOwned, is(true));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void isNotSelfOwned() throws Exception {
|
|
||||||
Uri uri = Uri.parse("content://com.android.providers.media.documents/document/1");
|
|
||||||
boolean selfOwned = FileUtils.isSelfOwned(InstrumentationRegistry.getTargetContext(), uri);
|
|
||||||
assertThat(selfOwned, is(false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
|
<uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />
|
||||||
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
<uses-permission android:name="com.google.android.apps.photos.permission.GOOGLE_PHOTOS" />
|
||||||
<uses-permission android:name="android.permission.READ_LOGS"/>
|
<uses-permission android:name="android.permission.READ_LOGS" />
|
||||||
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -41,42 +41,35 @@
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity android:name=".WelcomeActivity" />
|
<activity android:name=".WelcomeActivity" />
|
||||||
|
|
||||||
<activity
|
<activity android:name=".upload.UploadActivity"
|
||||||
android:name=".upload.ShareActivity"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name">
|
android:label="@string/app_name">
|
||||||
<intent-filter android:label="@string/intent_share_upload_label">
|
<intent-filter android:label="@string/intent_share_upload_label">
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
<data android:mimeType="audio/ogg" />
|
<data android:mimeType="audio/ogg" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
<intent-filter>
|
||||||
|
|
||||||
<activity
|
|
||||||
android:name=".upload.MultipleShareActivity"
|
|
||||||
android:icon="@drawable/ic_launcher"
|
|
||||||
android:label="@string/app_name">
|
|
||||||
<intent-filter android:label="@string/intent_share_upload_label">
|
|
||||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="image/*" />
|
<data android:mimeType="image/*" />
|
||||||
<data android:mimeType="audio/ogg" />
|
<data android:mimeType="audio/ogg" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".contributions.MainActivity"
|
android:name=".contributions.MainActivity"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name" />
|
android:label="@string/app_name" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:label="@string/title_activity_settings" />
|
android:label="@string/title_activity_settings" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".AboutActivity"
|
android:name=".AboutActivity"
|
||||||
android:label="@string/title_activity_about"
|
android:label="@string/title_activity_about"
|
||||||
|
|
@ -135,24 +128,24 @@
|
||||||
android:name="android.accounts.AccountAuthenticator"
|
android:name="android.accounts.AccountAuthenticator"
|
||||||
android:resource="@xml/authenticator" />
|
android:resource="@xml/authenticator" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".contributions.ContributionsSyncService"
|
android:name=".contributions.ContributionsSyncService"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/contributions_sync_adapter" />
|
android:resource="@xml/contributions_sync_adapter" />
|
||||||
</service>
|
</service>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".modifications.ModificationsSyncService"
|
android:name=".modifications.ModificationsSyncService"
|
||||||
android:exported="true">
|
android:exported="true">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.content.SyncAdapter" />
|
<action android:name="android.content.SyncAdapter" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.content.SyncAdapter"
|
android:name="android.content.SyncAdapter"
|
||||||
android:resource="@xml/modifications_sync_adapter" />
|
android:resource="@xml/modifications_sync_adapter" />
|
||||||
|
|
@ -172,21 +165,18 @@
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".contributions.ContributionsContentProvider"
|
android:name=".contributions.ContributionsContentProvider"
|
||||||
android:authorities="${applicationId}.contributions.contentprovider"
|
android:authorities="${applicationId}.contributions.contentprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/provider_contributions"
|
android:label="@string/provider_contributions"
|
||||||
android:syncable="true" />
|
android:syncable="true" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".modifications.ModificationsContentProvider"
|
android:name=".modifications.ModificationsContentProvider"
|
||||||
android:authorities="${applicationId}.modifications.contentprovider"
|
android:authorities="${applicationId}.modifications.contentprovider"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/provider_modifications"
|
android:label="@string/provider_modifications"
|
||||||
android:syncable="true" />
|
android:syncable="true" />
|
||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name=".category.CategoryContentProvider"
|
android:name=".category.CategoryContentProvider"
|
||||||
android:authorities="${applicationId}.categories.contentprovider"
|
android:authorities="${applicationId}.categories.contentprovider"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons;
|
package fr.free.nrw.commons;
|
||||||
|
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
|
|
@ -14,12 +15,11 @@ import android.widget.Toast;
|
||||||
import org.apache.commons.codec.binary.Hex;
|
import org.apache.commons.codec.binary.Hex;
|
||||||
import org.apache.commons.codec.digest.DigestUtils;
|
import org.apache.commons.codec.digest.DigestUtils;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
|
@ -110,6 +110,31 @@ public class Utils {
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
throw new RuntimeException("Unrecognized license value: " + license);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates license url with given ID
|
||||||
|
* @param license License ID
|
||||||
|
* @return Url of license
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String licenseUrlFor(String license) {
|
||||||
|
switch (license) {
|
||||||
|
case Prefs.Licenses.CC_BY_3:
|
||||||
|
return "https://creativecommons.org/licenses/by/3.0/";
|
||||||
|
case Prefs.Licenses.CC_BY_4:
|
||||||
|
return "https://creativecommons.org/licenses/by/4.0/";
|
||||||
|
case Prefs.Licenses.CC_BY_SA_3:
|
||||||
|
return "https://creativecommons.org/licenses/by-sa/3.0/";
|
||||||
|
case Prefs.Licenses.CC_BY_SA_4:
|
||||||
|
return "https://creativecommons.org/licenses/by-sa/4.0/";
|
||||||
|
case Prefs.Licenses.CC0:
|
||||||
|
return "https://creativecommons.org/publicdomain/zero/1.0/";
|
||||||
|
default:
|
||||||
|
throw new RuntimeException("Unrecognized license value: " + license);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected
|
* Adds extension to filename. Converts to .jpg if system provides .jpeg, adds .jpg if no extension detected
|
||||||
* @param title File name
|
* @param title File name
|
||||||
|
|
@ -176,6 +201,18 @@ public class Utils {
|
||||||
customTabsIntent.launchUrl(context, url);
|
customTabsIntent.launchUrl(context, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void handleGeoCoordinates(Context context, String coords) {
|
||||||
|
try {
|
||||||
|
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + coords);
|
||||||
|
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
||||||
|
mapIntent.setPackage("com.google.android.apps.maps");
|
||||||
|
context.startActivity(mapIntent);
|
||||||
|
} catch (ActivityNotFoundException ex) {
|
||||||
|
Toast toast = Toast.makeText(context, context.getString(R.string.map_application_missing), LENGTH_SHORT);
|
||||||
|
toast.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* To take screenshot of the screen and return it in Bitmap format
|
* To take screenshot of the screen and return it in Bitmap format
|
||||||
*
|
*
|
||||||
|
|
@ -190,4 +227,14 @@ public class Utils {
|
||||||
return bitmap;
|
return bitmap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static <K,V> Map<K,V> arraysToMap(K[] kArray, V[] vArray){
|
||||||
|
if(kArray.length!=vArray.length)
|
||||||
|
throw new RuntimeException("arraysToMap array sizes don't match");
|
||||||
|
Map<K,V> map=new LinkedHashMap<>();
|
||||||
|
for (int i=0;i<vArray.length;i++){
|
||||||
|
map.put(kArray[i], vArray[i]);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,8 @@ import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE;
|
||||||
|
|
||||||
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
public abstract class AuthenticatedActivity extends NavigationBaseActivity {
|
||||||
|
|
||||||
@Inject SessionManager sessionManager;
|
@Inject
|
||||||
|
protected SessionManager sessionManager;
|
||||||
@Inject
|
@Inject
|
||||||
MediaWikiApi mediaWikiApi;
|
MediaWikiApi mediaWikiApi;
|
||||||
private String authCookie;
|
private String authCookie;
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,23 @@
|
||||||
package fr.free.nrw.commons.category;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
import com.pedrogomez.renderers.ListAdapteeCollection;
|
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
|
||||||
import com.pedrogomez.renderers.RendererBuilder;
|
import com.pedrogomez.renderers.RendererBuilder;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
class CategoriesAdapterFactory {
|
public class CategoriesAdapterFactory {
|
||||||
private final CategoriesRenderer.CategoryClickedListener listener;
|
private final CategoryClickedListener listener;
|
||||||
|
|
||||||
CategoriesAdapterFactory(CategoriesRenderer.CategoryClickedListener listener) {
|
public CategoriesAdapterFactory(CategoryClickedListener listener) {
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RVRendererAdapter<CategoryItem> create(List<CategoryItem> placeList) {
|
public CategoryRendererAdapter create(List<CategoryItem> placeList) {
|
||||||
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
|
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
|
||||||
.bind(CategoryItem.class, new CategoriesRenderer(listener));
|
.bind(CategoryItem.class, new CategoriesRenderer(listener));
|
||||||
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
|
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
|
||||||
placeList != null ? placeList : Collections.<CategoryItem>emptyList());
|
placeList != null ? placeList : Collections.<CategoryItem>emptyList());
|
||||||
return new RVRendererAdapter<>(builder, collection);
|
return new CategoryRendererAdapter(builder, collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,227 @@
|
||||||
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.upload.GpsCategoryModel;
|
||||||
|
import fr.free.nrw.commons.utils.StringSortingUtils;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class CategoriesModel implements CategoryClickedListener {
|
||||||
|
private static final int SEARCH_CATS_LIMIT = 25;
|
||||||
|
|
||||||
|
private final MediaWikiApi mwApi;
|
||||||
|
private final CategoryDao categoryDao;
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
private final SharedPreferences directPrefs;
|
||||||
|
|
||||||
|
private HashMap<String, ArrayList<String>> categoriesCache;
|
||||||
|
private List<CategoryItem> selectedCategories;
|
||||||
|
|
||||||
|
@Inject GpsCategoryModel gpsCategoryModel;
|
||||||
|
@Inject
|
||||||
|
public CategoriesModel(MediaWikiApi mwApi,
|
||||||
|
CategoryDao categoryDao,
|
||||||
|
@Named("default_preferences") SharedPreferences prefs,
|
||||||
|
@Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) {
|
||||||
|
this.mwApi = mwApi;
|
||||||
|
this.categoryDao = categoryDao;
|
||||||
|
this.prefs = prefs;
|
||||||
|
this.directPrefs = directPrefs;
|
||||||
|
this.categoriesCache = new HashMap<>();
|
||||||
|
this.selectedCategories = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
//region Misc. utility methods
|
||||||
|
public Comparator<CategoryItem> sortBySimilarity(final String filter) {
|
||||||
|
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
|
||||||
|
return (firstItem, secondItem) -> stringSimilarityComparator
|
||||||
|
.compare(firstItem.getName(), secondItem.getName());
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean containsYear(String item) {
|
||||||
|
//Check for current and previous year to exclude these categories from removal
|
||||||
|
Calendar now = Calendar.getInstance();
|
||||||
|
int year = now.get(Calendar.YEAR);
|
||||||
|
String yearInString = String.valueOf(year);
|
||||||
|
|
||||||
|
int prevYear = year - 1;
|
||||||
|
String prevYearInString = String.valueOf(prevYear);
|
||||||
|
Timber.d("Previous year: %s", prevYearInString);
|
||||||
|
|
||||||
|
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
||||||
|
//And that item does not equal the current year or previous year
|
||||||
|
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
||||||
|
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
|
||||||
|
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
||||||
|
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|
||||||
|
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void updateCategoryCount(CategoryItem item) {
|
||||||
|
Category category = categoryDao.find(item.getName());
|
||||||
|
|
||||||
|
// Newly used category...
|
||||||
|
if (category == null) {
|
||||||
|
category = new Category(null, item.getName(), new Date(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
category.incTimesUsed();
|
||||||
|
categoryDao.save(category);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Category Caching
|
||||||
|
public void cacheAll(HashMap<String, ArrayList<String>> categories) {
|
||||||
|
categoriesCache.putAll(categories);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HashMap<String, ArrayList<String>> getCategoriesCache() {
|
||||||
|
return categoriesCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean cacheContainsKey(String term) {
|
||||||
|
return categoriesCache.containsKey(term);
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Category searching
|
||||||
|
public Observable<CategoryItem> searchAll(String term, List<String> imageTitleList) {
|
||||||
|
//If user hasn't typed anything in yet, get GPS and recent items
|
||||||
|
if (TextUtils.isEmpty(term)) {
|
||||||
|
return gpsCategories()
|
||||||
|
.concatWith(titleCategories(imageTitleList))
|
||||||
|
.concatWith(recentCategories());
|
||||||
|
}
|
||||||
|
|
||||||
|
//if user types in something that is in cache, return cached category
|
||||||
|
if (cacheContainsKey(term)) {
|
||||||
|
return Observable.fromIterable(getCachedCategories(term))
|
||||||
|
.map(name -> new CategoryItem(name, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
//otherwise, search API for matching categories
|
||||||
|
return mwApi
|
||||||
|
.allCategories(term, SEARCH_CATS_LIMIT)
|
||||||
|
.map(name -> new CategoryItem(name, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Observable<CategoryItem> searchCategories(String term, List<String> imageTitleList) {
|
||||||
|
//If user hasn't typed anything in yet, get GPS and recent items
|
||||||
|
if (TextUtils.isEmpty(term)) {
|
||||||
|
return gpsCategories()
|
||||||
|
.concatWith(titleCategories(imageTitleList))
|
||||||
|
.concatWith(recentCategories());
|
||||||
|
}
|
||||||
|
|
||||||
|
return mwApi
|
||||||
|
.searchCategories(term, SEARCH_CATS_LIMIT)
|
||||||
|
.map(s -> new CategoryItem(s, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<String> getCachedCategories(String term) {
|
||||||
|
return categoriesCache.get(term);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Observable<CategoryItem> defaultCategories(List<String> titleList) {
|
||||||
|
Observable<CategoryItem> directCat = directCategories();
|
||||||
|
if (hasDirectCategories()) {
|
||||||
|
Timber.d("Image has direct Cat");
|
||||||
|
return directCat
|
||||||
|
.concatWith(gpsCategories())
|
||||||
|
.concatWith(titleCategories(titleList))
|
||||||
|
.concatWith(recentCategories());
|
||||||
|
} else {
|
||||||
|
Timber.d("Image has no direct Cat");
|
||||||
|
return gpsCategories()
|
||||||
|
.concatWith(titleCategories(titleList))
|
||||||
|
.concatWith(recentCategories());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasDirectCategories() {
|
||||||
|
return !directPrefs.getString("Category", "").equals("");
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<CategoryItem> directCategories() {
|
||||||
|
String directCategory = directPrefs.getString("Category", "");
|
||||||
|
List<String> categoryList = new ArrayList<>();
|
||||||
|
Timber.d("Direct category found: " + directCategory);
|
||||||
|
|
||||||
|
if (!directCategory.equals("")) {
|
||||||
|
categoryList.add(directCategory);
|
||||||
|
Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
|
||||||
|
}
|
||||||
|
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
Observable<CategoryItem> gpsCategories() {
|
||||||
|
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
|
||||||
|
.map(name -> new CategoryItem(name, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<CategoryItem> titleCategories(List<String> titleList) {
|
||||||
|
return Observable.fromIterable(titleList)
|
||||||
|
.concatMap(this::getTitleCategories);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<CategoryItem> getTitleCategories(String title) {
|
||||||
|
return mwApi.searchTitles(title, SEARCH_CATS_LIMIT)
|
||||||
|
.map(name -> new CategoryItem(name, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<CategoryItem> recentCategories() {
|
||||||
|
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
|
||||||
|
.map(s -> new CategoryItem(s, false));
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Category Selection
|
||||||
|
@Override
|
||||||
|
public void categoryClicked(CategoryItem item) {
|
||||||
|
if (item.isSelected()) {
|
||||||
|
selectCategory(item);
|
||||||
|
updateCategoryCount(item);
|
||||||
|
} else {
|
||||||
|
unselectCategory(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void selectCategory(CategoryItem item) {
|
||||||
|
selectedCategories.add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unselectCategory(CategoryItem item) {
|
||||||
|
selectedCategories.remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int selectedCategoriesCount() {
|
||||||
|
return selectedCategories.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<CategoryItem> getSelectedCategories() {
|
||||||
|
return selectedCategories;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getCategoryStringList() {
|
||||||
|
List<String> output = new ArrayList<>();
|
||||||
|
for (CategoryItem item : selectedCategories) {
|
||||||
|
output.add(item.getName());
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package fr.free.nrw.commons.category;
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -11,7 +12,7 @@ import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
class CategoriesRenderer extends Renderer<CategoryItem> {
|
public class CategoriesRenderer extends Renderer<CategoryItem> {
|
||||||
@BindView(R.id.tvName) CheckedTextView checkedView;
|
@BindView(R.id.tvName) CheckedTextView checkedView;
|
||||||
private final CategoryClickedListener listener;
|
private final CategoryClickedListener listener;
|
||||||
|
|
||||||
|
|
@ -44,11 +45,8 @@ class CategoriesRenderer extends Renderer<CategoryItem> {
|
||||||
@Override
|
@Override
|
||||||
public void render() {
|
public void render() {
|
||||||
CategoryItem item = getContent();
|
CategoryItem item = getContent();
|
||||||
|
Log.e("Commons", "Rendering: "+item);
|
||||||
checkedView.setChecked(item.isSelected());
|
checkedView.setChecked(item.isSelected());
|
||||||
checkedView.setText(item.getName());
|
checkedView.setText(item.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryClickedListener {
|
|
||||||
void categoryClicked(CategoryItem item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,421 +0,0 @@
|
||||||
package fr.free.nrw.commons.category;
|
|
||||||
|
|
||||||
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.jakewharton.rxbinding2.view.RxView;
|
|
||||||
import com.jakewharton.rxbinding2.widget.RxTextView;
|
|
||||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Calendar;
|
|
||||||
import java.util.Comparator;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
import fr.free.nrw.commons.upload.GpsCategoryModel;
|
|
||||||
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;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static android.view.KeyEvent.ACTION_UP;
|
|
||||||
import static android.view.KeyEvent.KEYCODE_BACK;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays the category suggestion and selection screen. Category search is initiated here.
|
|
||||||
*/
|
|
||||||
public class CategorizationFragment extends CommonsDaggerSupportFragment {
|
|
||||||
|
|
||||||
public static final int SEARCH_CATS_LIMIT = 25;
|
|
||||||
|
|
||||||
@BindView(R.id.categoriesListBox)
|
|
||||||
RecyclerView categoriesList;
|
|
||||||
@BindView(R.id.categoriesSearchBox)
|
|
||||||
EditText categoriesFilter;
|
|
||||||
@BindView(R.id.categoriesSearchInProgress)
|
|
||||||
ProgressBar categoriesSearchInProgress;
|
|
||||||
@BindView(R.id.categoriesNotFound)
|
|
||||||
TextView categoriesNotFoundView;
|
|
||||||
@BindView(R.id.categoriesExplanation)
|
|
||||||
TextView categoriesSkip;
|
|
||||||
|
|
||||||
@Inject MediaWikiApi mwApi;
|
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
|
||||||
@Inject @Named("prefs") SharedPreferences prefsPrefs;
|
|
||||||
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
|
||||||
@Inject CategoryDao categoryDao;
|
|
||||||
@Inject GpsCategoryModel gpsCategoryModel;
|
|
||||||
|
|
||||||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
|
||||||
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
|
||||||
private HashMap<String, ArrayList<String>> categoriesCache;
|
|
||||||
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
|
||||||
private TitleTextWatcher textWatcher = new TitleTextWatcher();
|
|
||||||
private boolean hasDirectCategories = false;
|
|
||||||
|
|
||||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
|
||||||
if (item.isSelected()) {
|
|
||||||
selectedCategories.add(item);
|
|
||||||
updateCategoryCount(item);
|
|
||||||
} else {
|
|
||||||
selectedCategories.remove(item);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_categorization, container, false);
|
|
||||||
ButterKnife.bind(this, rootView);
|
|
||||||
|
|
||||||
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
|
|
||||||
ArrayList<CategoryItem> items = new ArrayList<>();
|
|
||||||
categoriesCache = new HashMap<>();
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
items.addAll(savedInstanceState.getParcelableArrayList("currentCategories"));
|
|
||||||
//noinspection unchecked
|
|
||||||
categoriesCache.putAll((HashMap<String, ArrayList<String>>) savedInstanceState
|
|
||||||
.getSerializable("categoriesCache"));
|
|
||||||
}
|
|
||||||
|
|
||||||
categoriesAdapter = adapterFactory.create(items);
|
|
||||||
categoriesList.setAdapter(categoriesAdapter);
|
|
||||||
|
|
||||||
|
|
||||||
categoriesFilter.addTextChangedListener(textWatcher);
|
|
||||||
|
|
||||||
categoriesFilter.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
if (!hasFocus) {
|
|
||||||
ViewUtil.hideKeyboard(v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
RxTextView.textChanges(categoriesFilter)
|
|
||||||
.takeUntil(RxView.detaches(categoriesFilter))
|
|
||||||
.debounce(500, TimeUnit.MILLISECONDS)
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(filter -> updateCategoryList(filter.toString()));
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
categoriesFilter.removeTextChangedListener(textWatcher);
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
menu.clear();
|
|
||||||
inflater.inflate(R.menu.fragment_categorization, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
View rootView = getView();
|
|
||||||
if (rootView != null) {
|
|
||||||
rootView.setFocusableInTouchMode(true);
|
|
||||||
rootView.requestFocus();
|
|
||||||
rootView.setOnKeyListener((v, keyCode, event) -> {
|
|
||||||
if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) {
|
|
||||||
showBackButtonDialog();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
int itemCount = categoriesAdapter.getItemCount();
|
|
||||||
ArrayList<CategoryItem> items = new ArrayList<>(itemCount);
|
|
||||||
for (int i = 0; i < itemCount; i++) {
|
|
||||||
items.add(categoriesAdapter.getItem(i));
|
|
||||||
}
|
|
||||||
outState.putParcelableArrayList("currentCategories", items);
|
|
||||||
outState.putSerializable("categoriesCache", categoriesCache);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
|
||||||
switch (menuItem.getItemId()) {
|
|
||||||
case R.id.menu_save_categories:
|
|
||||||
if (selectedCategories.size() > 0) {
|
|
||||||
//Some categories selected, proceed to submission
|
|
||||||
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
|
|
||||||
} else {
|
|
||||||
//No categories selected, prompt the user to select some
|
|
||||||
showConfirmationDialog();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(menuItem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(Bundle savedInstanceState) {
|
|
||||||
super.onActivityCreated(savedInstanceState);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
|
||||||
getActivity().setTitle(R.string.categories_activity_title);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCategoryList(String filter) {
|
|
||||||
Observable.fromIterable(selectedCategories)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.doOnSubscribe(disposable -> {
|
|
||||||
categoriesSearchInProgress.setVisibility(View.VISIBLE);
|
|
||||||
categoriesNotFoundView.setVisibility(View.GONE);
|
|
||||||
categoriesSkip.setVisibility(View.GONE);
|
|
||||||
categoriesAdapter.clear();
|
|
||||||
})
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.concatWith(
|
|
||||||
searchAll(filter)
|
|
||||||
.mergeWith(searchCategories(filter))
|
|
||||||
.concatWith(TextUtils.isEmpty(filter)
|
|
||||||
? defaultCategories() : Observable.empty())
|
|
||||||
)
|
|
||||||
.filter(categoryItem -> !containsYear(categoryItem.getName()))
|
|
||||||
.distinct()
|
|
||||||
.sorted(sortBySimilarity(filter))
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(
|
|
||||||
s -> categoriesAdapter.add(s),
|
|
||||||
Timber::e,
|
|
||||||
() -> {
|
|
||||||
categoriesAdapter.notifyDataSetChanged();
|
|
||||||
categoriesSearchInProgress.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
if (categoriesAdapter.getItemCount() == selectedCategories.size()) {
|
|
||||||
// There are no suggestions
|
|
||||||
if (TextUtils.isEmpty(filter)) {
|
|
||||||
// Allow to send image with no categories
|
|
||||||
categoriesSkip.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
// Inform the user that the searched term matches no category
|
|
||||||
categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
|
|
||||||
categoriesNotFoundView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Comparator<CategoryItem> sortBySimilarity(final String filter) {
|
|
||||||
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
|
|
||||||
return (firstItem, secondItem) -> stringSimilarityComparator
|
|
||||||
.compare(firstItem.getName(), secondItem.getName());
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<String> getStringList(List<CategoryItem> input) {
|
|
||||||
List<String> output = new ArrayList<>();
|
|
||||||
for (CategoryItem item : input) {
|
|
||||||
output.add(item.getName());
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<CategoryItem> defaultCategories() {
|
|
||||||
Observable<CategoryItem> directCat = directCategories();
|
|
||||||
if (hasDirectCategories) {
|
|
||||||
Timber.d("Image has direct Cat");
|
|
||||||
return directCat
|
|
||||||
.concatWith(gpsCategories())
|
|
||||||
.concatWith(titleCategories())
|
|
||||||
.concatWith(recentCategories());
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Timber.d("Image has no direct Cat");
|
|
||||||
return gpsCategories()
|
|
||||||
.concatWith(titleCategories())
|
|
||||||
.concatWith(recentCategories());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<CategoryItem> directCategories() {
|
|
||||||
String directCategory = directPrefs.getString("Category", "");
|
|
||||||
// Strip newlines to prevent blank categories, and to tidy existing categories
|
|
||||||
directCategory = directCategory.replace("\n", "");
|
|
||||||
|
|
||||||
List<String> categoryList = new ArrayList<>();
|
|
||||||
Timber.d("Direct category found: " + "'" + directCategory + "'");
|
|
||||||
|
|
||||||
if (!directCategory.equals("")) {
|
|
||||||
hasDirectCategories = true;
|
|
||||||
categoryList.add(directCategory);
|
|
||||||
Timber.d("DirectCat does not equal emptyString. Direct Cat list has " + categoryList);
|
|
||||||
}
|
|
||||||
return Observable.fromIterable(categoryList).map(name -> new CategoryItem(name, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<CategoryItem> gpsCategories() {
|
|
||||||
return Observable.fromIterable(gpsCategoryModel.getCategoryList())
|
|
||||||
.map(name -> new CategoryItem(name, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<CategoryItem> titleCategories() {
|
|
||||||
//Retrieve the title that was saved when user tapped submit icon
|
|
||||||
String title = prefs.getString("Title", "");
|
|
||||||
|
|
||||||
return mwApi
|
|
||||||
.searchTitles(title, SEARCH_CATS_LIMIT)
|
|
||||||
.map(name -> new CategoryItem(name, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<CategoryItem> recentCategories() {
|
|
||||||
return Observable.fromIterable(categoryDao.recentCategories(SEARCH_CATS_LIMIT))
|
|
||||||
.map(s -> new CategoryItem(s, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<CategoryItem> searchAll(String term) {
|
|
||||||
//If user hasn't typed anything in yet, get GPS and recent items
|
|
||||||
if (TextUtils.isEmpty(term)) {
|
|
||||||
return Observable.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
//if user types in something that is in cache, return cached category
|
|
||||||
if (categoriesCache.containsKey(term)) {
|
|
||||||
return Observable.fromIterable(categoriesCache.get(term))
|
|
||||||
.map(name -> new CategoryItem(name, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
//otherwise, search API for matching categories
|
|
||||||
return mwApi
|
|
||||||
.allCategories(term, SEARCH_CATS_LIMIT)
|
|
||||||
.map(name -> new CategoryItem(name, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<CategoryItem> searchCategories(String term) {
|
|
||||||
//If user hasn't typed anything in yet, get GPS and recent items
|
|
||||||
if (TextUtils.isEmpty(term)) {
|
|
||||||
return Observable.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
return mwApi
|
|
||||||
.searchCategories(term, SEARCH_CATS_LIMIT)
|
|
||||||
.map(s -> new CategoryItem(s, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean containsYear(String item) {
|
|
||||||
//Check for current and previous year to exclude these categories from removal
|
|
||||||
Calendar now = Calendar.getInstance();
|
|
||||||
int year = now.get(Calendar.YEAR);
|
|
||||||
String yearInString = String.valueOf(year);
|
|
||||||
|
|
||||||
int prevYear = year - 1;
|
|
||||||
String prevYearInString = String.valueOf(prevYear);
|
|
||||||
Timber.d("Previous year: %s", prevYearInString);
|
|
||||||
|
|
||||||
//Check if item contains a 4-digit word anywhere within the string (.* is wildcard)
|
|
||||||
//And that item does not equal the current year or previous year
|
|
||||||
//And if it is an irrelevant category such as Media_needing_categories_as_of_16_June_2017(Issue #750)
|
|
||||||
//Check if the year in the form of XX(X)0s is relevant, i.e. in the 2000s or 2010s as stated in Issue #1029
|
|
||||||
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
|
||||||
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)")
|
|
||||||
|| (item.matches(".*0s.*") && !item.matches(".*(200|201)0s.*")));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateCategoryCount(CategoryItem item) {
|
|
||||||
Category category = categoryDao.find(item.getName());
|
|
||||||
|
|
||||||
// Newly used category...
|
|
||||||
if (category == null) {
|
|
||||||
category = new Category(null, item.getName(), new Date(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
category.incTimesUsed();
|
|
||||||
categoryDao.save(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCurrentSelectedCount() {
|
|
||||||
return selectedCategories.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show dialog asking for confirmation to leave without saving categories.
|
|
||||||
*/
|
|
||||||
public void showBackButtonDialog() {
|
|
||||||
new AlertDialog.Builder(getActivity())
|
|
||||||
.setMessage("Are you sure you want to go back? The image will not "
|
|
||||||
+ "have any categories saved.")
|
|
||||||
.setTitle("Warning")
|
|
||||||
.setPositiveButton(android.R.string.no, (dialog, id) -> {
|
|
||||||
//No need to do anything, user remains on categorization screen
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.yes, (dialog, id) -> getActivity().finish())
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showConfirmationDialog() {
|
|
||||||
new AlertDialog.Builder(getActivity())
|
|
||||||
.setMessage("Images without categories are rarely usable. "
|
|
||||||
+ "Are you sure you want to submit without selecting "
|
|
||||||
+ "categories?")
|
|
||||||
.setTitle("No Categories Selected")
|
|
||||||
.setPositiveButton(android.R.string.no, (dialog, id) -> {
|
|
||||||
//Exit menuItem so user can select their categories
|
|
||||||
})
|
|
||||||
.setNegativeButton(android.R.string.yes, (dialog, id) -> {
|
|
||||||
//Proceed to submission
|
|
||||||
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
|
|
||||||
})
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TitleTextWatcher implements TextWatcher {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable editable) {
|
|
||||||
if (getActivity() != null) {
|
|
||||||
getActivity().invalidateOptionsMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
|
public interface CategoryClickedListener {
|
||||||
|
void categoryClicked(CategoryItem item);
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,7 @@ package fr.free.nrw.commons.category;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
class CategoryItem implements Parcelable {
|
public class CategoryItem implements Parcelable {
|
||||||
private final String name;
|
private final String name;
|
||||||
private boolean selected;
|
private boolean selected;
|
||||||
|
|
||||||
|
|
@ -71,4 +71,9 @@ class CategoryItem implements Parcelable {
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return name.hashCode();
|
return name.hashCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return "CategoryItem: '" + name + '\'';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package fr.free.nrw.commons.category;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.AdapteeCollection;
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
import com.pedrogomez.renderers.RendererBuilder;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class CategoryRendererAdapter extends RVRendererAdapter<CategoryItem> {
|
||||||
|
CategoryRendererAdapter(RendererBuilder<CategoryItem> rendererBuilder, AdapteeCollection<CategoryItem> collection) {
|
||||||
|
super(rendererBuilder, collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected ArrayList<CategoryItem> allItems() {
|
||||||
|
int itemCount = getItemCount();
|
||||||
|
ArrayList<CategoryItem> items = new ArrayList<>(itemCount);
|
||||||
|
for (int i = 0; i < itemCount; i++) {
|
||||||
|
items.add(getItem(i));
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,11 @@ package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.StringDef;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
@ -13,6 +16,8 @@ import fr.free.nrw.commons.CommonsApplication;
|
||||||
import fr.free.nrw.commons.Media;
|
import fr.free.nrw.commons.Media;
|
||||||
import fr.free.nrw.commons.settings.Prefs;
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
public class Contribution extends Media {
|
public class Contribution extends Media {
|
||||||
|
|
||||||
public static Creator<Contribution> CREATOR = new Creator<Contribution>() {
|
public static Creator<Contribution> CREATOR = new Creator<Contribution>() {
|
||||||
|
|
@ -33,6 +38,10 @@ public class Contribution extends Media {
|
||||||
public static final int STATE_QUEUED = 2;
|
public static final int STATE_QUEUED = 2;
|
||||||
public static final int STATE_IN_PROGRESS = 3;
|
public static final int STATE_IN_PROGRESS = 3;
|
||||||
|
|
||||||
|
@Retention(SOURCE)
|
||||||
|
@StringDef({SOURCE_CAMERA, SOURCE_GALLERY, SOURCE_EXTERNAL})
|
||||||
|
public @interface FileSource {}
|
||||||
|
|
||||||
public static final String SOURCE_CAMERA = "camera";
|
public static final String SOURCE_CAMERA = "camera";
|
||||||
public static final String SOURCE_GALLERY = "gallery";
|
public static final String SOURCE_GALLERY = "gallery";
|
||||||
public static final String SOURCE_EXTERNAL = "external";
|
public static final String SOURCE_EXTERNAL = "external";
|
||||||
|
|
@ -40,7 +49,6 @@ public class Contribution extends Media {
|
||||||
private Uri contentUri;
|
private Uri contentUri;
|
||||||
private String source;
|
private String source;
|
||||||
private String editSummary;
|
private String editSummary;
|
||||||
private Date timestamp;
|
|
||||||
private int state;
|
private int state;
|
||||||
private long transferred;
|
private long transferred;
|
||||||
private String decimalCoords;
|
private String decimalCoords;
|
||||||
|
|
@ -48,14 +56,13 @@ public class Contribution extends Media {
|
||||||
private String wikiDataEntityId;
|
private String wikiDataEntityId;
|
||||||
private Uri contentProviderUri;
|
private Uri contentProviderUri;
|
||||||
|
|
||||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp,
|
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
|
||||||
int state, long dataLength, Date dateUploaded, long transferred,
|
int state, long dataLength, Date dateUploaded, long transferred,
|
||||||
String source, String description, String creator, boolean isMultiple,
|
String source, String description, String creator, boolean isMultiple,
|
||||||
int width, int height, String license) {
|
int width, int height, String license) {
|
||||||
super(localUri, imageUrl, filename, description, dataLength, timestamp, dateUploaded, creator);
|
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||||
this.contentUri = contentUri;
|
this.contentUri = contentUri;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
this.timestamp = timestamp;
|
|
||||||
this.transferred = transferred;
|
this.transferred = transferred;
|
||||||
this.source = source;
|
this.source = source;
|
||||||
this.isMultiple = isMultiple;
|
this.isMultiple = isMultiple;
|
||||||
|
|
@ -69,14 +76,12 @@ public class Contribution extends Media {
|
||||||
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||||
this.decimalCoords = decimalCoords;
|
this.decimalCoords = decimalCoords;
|
||||||
this.editSummary = editSummary;
|
this.editSummary = editSummary;
|
||||||
timestamp = new Date(System.currentTimeMillis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Contribution(Parcel in) {
|
public Contribution(Parcel in) {
|
||||||
super(in);
|
super(in);
|
||||||
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
contentUri = in.readParcelable(Uri.class.getClassLoader());
|
||||||
source = in.readString();
|
source = in.readString();
|
||||||
timestamp = (Date) in.readSerializable();
|
|
||||||
state = in.readInt();
|
state = in.readInt();
|
||||||
transferred = in.readLong();
|
transferred = in.readLong();
|
||||||
isMultiple = in.readInt() == 1;
|
isMultiple = in.readInt() == 1;
|
||||||
|
|
@ -87,12 +92,13 @@ public class Contribution extends Media {
|
||||||
super.writeToParcel(parcel, flags);
|
super.writeToParcel(parcel, flags);
|
||||||
parcel.writeParcelable(contentUri, flags);
|
parcel.writeParcelable(contentUri, flags);
|
||||||
parcel.writeString(source);
|
parcel.writeString(source);
|
||||||
parcel.writeSerializable(timestamp);
|
|
||||||
parcel.writeInt(state);
|
parcel.writeInt(state);
|
||||||
parcel.writeLong(transferred);
|
parcel.writeLong(transferred);
|
||||||
parcel.writeInt(isMultiple ? 1 : 0);
|
parcel.writeInt(isMultiple ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public boolean getMultiple() {
|
public boolean getMultiple() {
|
||||||
return isMultiple;
|
return isMultiple;
|
||||||
}
|
}
|
||||||
|
|
@ -121,14 +127,6 @@ public class Contribution extends Media {
|
||||||
this.contentUri = contentUri;
|
this.contentUri = contentUri;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date getTimestamp() {
|
|
||||||
return timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTimestamp(Date timestamp) {
|
|
||||||
this.timestamp = timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getState() {
|
public int getState() {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
@ -141,10 +139,6 @@ public class Contribution extends Media {
|
||||||
this.dateUploaded = date;
|
this.dateUploaded = date;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getTrackingTemplates() {
|
|
||||||
return "{{subst:unc}}"; // Remove when we have categorization
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPageContents() {
|
public String getPageContents() {
|
||||||
StringBuilder buffer = new StringBuilder();
|
StringBuilder buffer = new StringBuilder();
|
||||||
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd", Locale.ENGLISH);
|
||||||
|
|
@ -169,8 +163,15 @@ public class Contribution extends Media {
|
||||||
|
|
||||||
buffer.append("== {{int:license-header}} ==\n")
|
buffer.append("== {{int:license-header}} ==\n")
|
||||||
.append(licenseTemplateFor(getLicense())).append("\n\n")
|
.append(licenseTemplateFor(getLicense())).append("\n\n")
|
||||||
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n")
|
.append("{{Uploaded from Mobile|platform=Android|version=").append(BuildConfig.VERSION_NAME).append("}}\n");
|
||||||
.append(getTrackingTemplates());
|
if(categories!=null&&categories.size()!=0) {
|
||||||
|
for (int i = 0; i < categories.size(); i++) {
|
||||||
|
String category = categories.get(i);
|
||||||
|
buffer.append("\n[[Category:").append(category).append("]]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
buffer.append("{{subst:unc}}");
|
||||||
return buffer.toString();
|
return buffer.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,7 +185,7 @@ public class Contribution extends Media {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Contribution() {
|
public Contribution() {
|
||||||
timestamp = new Date(System.currentTimeMillis());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getSource() {
|
public String getSource() {
|
||||||
|
|
@ -232,7 +233,7 @@ public class Contribution extends Media {
|
||||||
/**
|
/**
|
||||||
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
|
* When the corresponding wikidata entity is known as in case of nearby uploads, it can be set
|
||||||
* using the setter method
|
* using the setter method
|
||||||
* @param wikiDataEntityId
|
* @param wikiDataEntityId wikiDataEntityId
|
||||||
*/
|
*/
|
||||||
public void setWikiDataEntityId(String wikiDataEntityId) {
|
public void setWikiDataEntityId(String wikiDataEntityId) {
|
||||||
this.wikiDataEntityId = wikiDataEntityId;
|
this.wikiDataEntityId = wikiDataEntityId;
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,26 @@ import android.content.Intent;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.content.pm.ResolveInfo;
|
import android.content.pm.ResolveInfo;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.annotation.RequiresApi;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.app.FragmentActivity;
|
import android.support.v4.app.FragmentActivity;
|
||||||
import android.support.v4.content.FileProvider;
|
import android.support.v4.content.FileProvider;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import fr.free.nrw.commons.upload.ShareActivity;
|
import fr.free.nrw.commons.upload.UploadActivity;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.content.Intent.ACTION_GET_CONTENT;
|
import static android.content.Intent.ACTION_GET_CONTENT;
|
||||||
import static android.content.Intent.ACTION_SEND;
|
import static android.content.Intent.ACTION_SEND;
|
||||||
|
import static android.content.Intent.ACTION_SEND_MULTIPLE;
|
||||||
import static android.content.Intent.EXTRA_STREAM;
|
import static android.content.Intent.EXTRA_STREAM;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
|
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA;
|
||||||
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
|
import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY;
|
||||||
|
|
@ -31,6 +35,7 @@ public class ContributionController {
|
||||||
|
|
||||||
public static final int SELECT_FROM_GALLERY = 1;
|
public static final int SELECT_FROM_GALLERY = 1;
|
||||||
public static final int SELECT_FROM_CAMERA = 2;
|
public static final int SELECT_FROM_CAMERA = 2;
|
||||||
|
public static final int PICK_IMAGE_MULTIPLE = 3;
|
||||||
|
|
||||||
private Fragment fragment;
|
private Fragment fragment;
|
||||||
|
|
||||||
|
|
@ -79,6 +84,14 @@ public class ContributionController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startGalleryPick() {
|
public void startGalleryPick() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||||
|
startMultipleGalleryPick();
|
||||||
|
} else {
|
||||||
|
startSingleGalleryPick();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void startSingleGalleryPick() {
|
||||||
//FIXME: Starts gallery (opens Google Photos)
|
//FIXME: Starts gallery (opens Google Photos)
|
||||||
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
|
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
|
||||||
pickImageIntent.setType("image/*");
|
pickImageIntent.setType("image/*");
|
||||||
|
|
@ -87,15 +100,41 @@ public class ContributionController {
|
||||||
Timber.d("Fragment is not added, startActivityForResult cannot be called");
|
Timber.d("Fragment is not added, startActivityForResult cannot be called");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Timber.d("startGalleryPick() called with pickImageIntent");
|
Timber.d("startSingleGalleryPick() called with pickImageIntent");
|
||||||
|
|
||||||
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
|
fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
|
||||||
|
public void startMultipleGalleryPick() {
|
||||||
|
Intent pickImageIntent = new Intent(ACTION_GET_CONTENT);
|
||||||
|
pickImageIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
|
||||||
|
pickImageIntent.setType("image/*");
|
||||||
|
if (!fragment.isAdded()) {
|
||||||
|
Timber.d("Fragment is not added, startActivityForResult cannot be called");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Timber.d("startMultipleGalleryPick() called with pickImageIntent");
|
||||||
|
|
||||||
|
fragment.startActivityForResult(pickImageIntent, PICK_IMAGE_MULTIPLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void handleImagesPicked(int requestCode, @Nullable ArrayList<Uri> uri) {
|
||||||
|
FragmentActivity activity = fragment.getActivity();
|
||||||
|
Intent shareIntent = new Intent(activity, UploadActivity.class);
|
||||||
|
shareIntent.setAction(ACTION_SEND_MULTIPLE);
|
||||||
|
shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY);
|
||||||
|
shareIntent.putExtra(EXTRA_STREAM, uri);
|
||||||
|
shareIntent.setType("image/jpeg");
|
||||||
|
if (activity != null) {
|
||||||
|
activity.startActivity(shareIntent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
|
public void handleImagePicked(int requestCode, @Nullable Uri uri, boolean isDirectUpload, String wikiDataEntityId) {
|
||||||
FragmentActivity activity = fragment.getActivity();
|
FragmentActivity activity = fragment.getActivity();
|
||||||
Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId);
|
Timber.d("handleImagePicked() called with onActivityResult(). Boolean isDirectUpload: " + isDirectUpload + "String wikiDataEntityId: " + wikiDataEntityId);
|
||||||
Intent shareIntent = new Intent(activity, ShareActivity.class);
|
Intent shareIntent = new Intent(activity, UploadActivity.class);
|
||||||
shareIntent.setAction(ACTION_SEND);
|
shareIntent.setAction(ACTION_SEND);
|
||||||
switch (requestCode) {
|
switch (requestCode) {
|
||||||
case SELECT_FROM_GALLERY:
|
case SELECT_FROM_GALLERY:
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,8 @@ public class ContributionDao {
|
||||||
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
|
cv.put(Table.COLUMN_UPLOADED, contribution.getDateUploaded().getTime());
|
||||||
}
|
}
|
||||||
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
|
cv.put(Table.COLUMN_LENGTH, contribution.getDataLength());
|
||||||
cv.put(Table.COLUMN_TIMESTAMP, contribution.getTimestamp().getTime());
|
//This was always meant to store the date created..If somehow date created is not fetched while actually saving the contribution, lets save today's date
|
||||||
|
cv.put(Table.COLUMN_TIMESTAMP, contribution.getDateCreated()==null?System.currentTimeMillis():contribution.getDateCreated().getTime());
|
||||||
cv.put(Table.COLUMN_STATE, contribution.getState());
|
cv.put(Table.COLUMN_STATE, contribution.getState());
|
||||||
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
|
cv.put(Table.COLUMN_TRANSFERRED, contribution.getTransferred());
|
||||||
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
|
cv.put(Table.COLUMN_SOURCE, contribution.getSource());
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
package fr.free.nrw.commons.contributions;
|
package fr.free.nrw.commons.contributions;
|
||||||
|
|
||||||
|
import android.content.ClipData;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
@ -11,6 +13,7 @@ import android.support.annotation.Nullable;
|
||||||
import android.support.design.widget.FloatingActionButton;
|
import android.support.design.widget.FloatingActionButton;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v7.app.AlertDialog;
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
|
@ -20,9 +23,9 @@ import android.widget.AdapterView;
|
||||||
import android.widget.GridView;
|
import android.widget.GridView;
|
||||||
import android.widget.ListAdapter;
|
import android.widget.ListAdapter;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
@ -39,7 +42,9 @@ import timber.log.Timber;
|
||||||
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
|
||||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||||
import static android.app.Activity.RESULT_OK;
|
import static android.app.Activity.RESULT_OK;
|
||||||
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
||||||
import static android.view.View.GONE;
|
import static android.view.View.GONE;
|
||||||
|
import static fr.free.nrw.commons.contributions.ContributionController.SELECT_FROM_GALLERY;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Created by root on 01.06.2018.
|
* Created by root on 01.06.2018.
|
||||||
|
|
@ -251,6 +256,8 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
// If coming from camera, pass null as uri. Because camera photos get saved to a
|
// If coming from camera, pass null as uri. Because camera photos get saved to a
|
||||||
// fixed directory
|
// fixed directory
|
||||||
controller.handleImagePicked(requestCode, null, false, null);
|
controller.handleImagePicked(requestCode, null, false, null);
|
||||||
|
} else if (requestCode == ContributionController.PICK_IMAGE_MULTIPLE) {
|
||||||
|
handleMultipleImages(requestCode, data);
|
||||||
} else if (requestCode == ContributionController.SELECT_FROM_GALLERY){
|
} else if (requestCode == ContributionController.SELECT_FROM_GALLERY){
|
||||||
controller.handleImagePicked(requestCode, data.getData(), false, null);
|
controller.handleImagePicked(requestCode, data.getData(), false, null);
|
||||||
}
|
}
|
||||||
|
|
@ -294,6 +301,28 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleMultipleImages(int requestCode, Intent data) {
|
||||||
|
if (getContext() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
|
||||||
|
&& data.getClipData() != null) {
|
||||||
|
ClipData mClipData = data.getClipData();
|
||||||
|
ArrayList<Uri> mArrayUri = new ArrayList<Uri>();
|
||||||
|
for (int i = 0; i < mClipData.getItemCount(); i++) {
|
||||||
|
|
||||||
|
ClipData.Item item = mClipData.getItemAt(i);
|
||||||
|
Uri uri = item.getUri();
|
||||||
|
mArrayUri.add(uri);
|
||||||
|
}
|
||||||
|
Log.v("LOG_TAG", "Selected Images" + mArrayUri.size());
|
||||||
|
controller.handleImagesPicked(requestCode, mArrayUri);
|
||||||
|
} else if(data.getData() != null) {
|
||||||
|
controller.handleImagePicked(SELECT_FROM_GALLERY, data.getData(), false, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Responsible to set progress bar invisible and visible
|
* Responsible to set progress bar invisible and visible
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,7 @@ import fr.free.nrw.commons.explore.SearchActivity;
|
||||||
|
|
||||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||||
import fr.free.nrw.commons.settings.SettingsActivity;
|
import fr.free.nrw.commons.settings.SettingsActivity;
|
||||||
import fr.free.nrw.commons.upload.MultipleShareActivity;
|
import fr.free.nrw.commons.upload.UploadActivity;
|
||||||
import fr.free.nrw.commons.upload.ShareActivity;
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
|
|
@ -28,12 +27,6 @@ public abstract class ActivityBuilderModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract WelcomeActivity bindWelcomeActivity();
|
abstract WelcomeActivity bindWelcomeActivity();
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract ShareActivity bindShareActivity();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract MultipleShareActivity bindMultipleShareActivity();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract MainActivity bindContributionsActivity();
|
abstract MainActivity bindContributionsActivity();
|
||||||
|
|
||||||
|
|
@ -52,6 +45,9 @@ public abstract class ActivityBuilderModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract CategoryImagesActivity bindFeaturedImagesActivity();
|
abstract CategoryImagesActivity bindFeaturedImagesActivity();
|
||||||
|
|
||||||
|
@ContributesAndroidInjector
|
||||||
|
abstract UploadActivity bindUploadActivity();
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract SearchActivity bindSearchActivity();
|
abstract SearchActivity bindSearchActivity();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,17 @@
|
||||||
package fr.free.nrw.commons.di;
|
package fr.free.nrw.commons.di;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.ContentProviderClient;
|
import android.content.ContentProviderClient;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.support.v4.util.LruCache;
|
import android.support.v4.util.LruCache;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import javax.inject.Named;
|
import javax.inject.Named;
|
||||||
import javax.inject.Singleton;
|
import javax.inject.Singleton;
|
||||||
|
|
@ -12,12 +19,14 @@ import javax.inject.Singleton;
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
import fr.free.nrw.commons.BuildConfig;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.AccountUtil;
|
import fr.free.nrw.commons.auth.AccountUtil;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
import fr.free.nrw.commons.nearby.NearbyPlaces;
|
||||||
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
import fr.free.nrw.commons.upload.UploadController;
|
import fr.free.nrw.commons.upload.UploadController;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
import fr.free.nrw.commons.wikidata.WikidataEditListener;
|
||||||
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
|
import fr.free.nrw.commons.wikidata.WikidataEditListenerImpl;
|
||||||
|
|
@ -38,6 +47,35 @@ public class CommonsApplicationModule {
|
||||||
return this.applicationContext;
|
return this.applicationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
public InputMethodManager provideInputMethodManager() {
|
||||||
|
return (InputMethodManager) applicationContext.getSystemService(Activity.INPUT_METHOD_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("licenses")
|
||||||
|
public List<String> provideLicenses(Context context) {
|
||||||
|
List<String> licenseItems = new ArrayList<>();
|
||||||
|
licenseItems.add(context.getString(R.string.license_name_cc0));
|
||||||
|
licenseItems.add(context.getString(R.string.license_name_cc_by));
|
||||||
|
licenseItems.add(context.getString(R.string.license_name_cc_by_sa));
|
||||||
|
licenseItems.add(context.getString(R.string.license_name_cc_by_four));
|
||||||
|
licenseItems.add(context.getString(R.string.license_name_cc_by_sa_four));
|
||||||
|
return licenseItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Named("licenses_by_name")
|
||||||
|
public Map<String, String> provideLicensesByName(Context context) {
|
||||||
|
Map<String, String> byName = new HashMap<>();
|
||||||
|
byName.put(context.getString(R.string.license_name_cc0), Prefs.Licenses.CC0);
|
||||||
|
byName.put(context.getString(R.string.license_name_cc_by), Prefs.Licenses.CC_BY_3);
|
||||||
|
byName.put(context.getString(R.string.license_name_cc_by_sa), Prefs.Licenses.CC_BY_SA_3);
|
||||||
|
byName.put(context.getString(R.string.license_name_cc_by_four), Prefs.Licenses.CC_BY_4);
|
||||||
|
byName.put(context.getString(R.string.license_name_cc_by_sa_four), Prefs.Licenses.CC_BY_SA_4);
|
||||||
|
return byName;
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
public AccountUtil providesAccountUtil(Context context) {
|
public AccountUtil providesAccountUtil(Context context) {
|
||||||
return new AccountUtil(context);
|
return new AccountUtil(context);
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import dagger.Module;
|
||||||
import dagger.android.ContributesAndroidInjector;
|
import dagger.android.ContributesAndroidInjector;
|
||||||
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
|
||||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
|
||||||
import fr.free.nrw.commons.category.CategorizationFragment;
|
|
||||||
import fr.free.nrw.commons.category.CategoryImagesListFragment;
|
import fr.free.nrw.commons.category.CategoryImagesListFragment;
|
||||||
import fr.free.nrw.commons.category.SubCategoryListFragment;
|
import fr.free.nrw.commons.category.SubCategoryListFragment;
|
||||||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
||||||
|
|
@ -19,16 +18,11 @@ import fr.free.nrw.commons.nearby.NearbyListFragment;
|
||||||
import fr.free.nrw.commons.nearby.NearbyMapFragment;
|
import fr.free.nrw.commons.nearby.NearbyMapFragment;
|
||||||
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
|
import fr.free.nrw.commons.nearby.NoPermissionsFragment;
|
||||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||||
import fr.free.nrw.commons.upload.MultipleUploadListFragment;
|
|
||||||
import fr.free.nrw.commons.upload.SingleUploadFragment;
|
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||||
public abstract class FragmentBuilderModule {
|
public abstract class FragmentBuilderModule {
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract CategorizationFragment bindCategorizationFragment();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract ContributionsListFragment bindContributionsListFragment();
|
abstract ContributionsListFragment bindContributionsListFragment();
|
||||||
|
|
||||||
|
|
@ -50,12 +44,6 @@ public abstract class FragmentBuilderModule {
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract SettingsFragment bindSettingsFragment();
|
abstract SettingsFragment bindSettingsFragment();
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract MultipleUploadListFragment bindMultipleUploadListFragment();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
|
||||||
abstract SingleUploadFragment bindSingleUploadFragment();
|
|
||||||
|
|
||||||
@ContributesAndroidInjector
|
@ContributesAndroidInjector
|
||||||
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
|
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.explore.images;
|
package fr.free.nrw.commons.explore.images;
|
||||||
|
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
@ -123,6 +124,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
||||||
* Checks for internet connection and then initializes the recycler view with 25 images of the searched query
|
* Checks for internet connection and then initializes the recycler view with 25 images of the searched query
|
||||||
* Clearing imageAdapter every time new keyword is searched so that user can see only new results
|
* Clearing imageAdapter every time new keyword is searched so that user can see only new results
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
public void updateImageList(String query) {
|
public void updateImageList(String query) {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
if (imagesNotFoundView != null) {
|
if (imagesNotFoundView != null) {
|
||||||
|
|
@ -146,6 +148,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
||||||
/**
|
/**
|
||||||
* Adds more results to existing search results
|
* Adds more results to existing search results
|
||||||
*/
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
public void addImagesToList(String query) {
|
public void addImagesToList(String query) {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
progressBar.setVisibility(View.VISIBLE);
|
||||||
|
|
@ -163,13 +166,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
||||||
*/
|
*/
|
||||||
private void handlePaginationSuccess(List<Media> mediaList) {
|
private void handlePaginationSuccess(List<Media> mediaList) {
|
||||||
progressBar.setVisibility(View.GONE);
|
progressBar.setVisibility(View.GONE);
|
||||||
if (mediaList.size()!=0){
|
if (mediaList.size() != 0 || !queryList.get(queryList.size() - 1).getFilename().equals(mediaList.get(mediaList.size() - 1).getFilename())) {
|
||||||
if (!queryList.get(queryList.size()-1).getFilename().equals(mediaList.get(mediaList.size()-1).getFilename())) {
|
|
||||||
queryList.addAll(mediaList);
|
queryList.addAll(mediaList);
|
||||||
imagesAdapter.addAll(mediaList);
|
imagesAdapter.addAll(mediaList);
|
||||||
imagesAdapter.notifyDataSetChanged();
|
imagesAdapter.notifyDataSetChanged();
|
||||||
((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
|
((SearchActivity) getContext()).viewPagerNotifyDataSetChanged();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -47,11 +47,11 @@ class DirectUpload {
|
||||||
fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP);
|
fragment.getActivity().requestPermissions(new String[]{READ_EXTERNAL_STORAGE}, PermissionUtils.GALLERY_PERMISSION_FROM_NEARBY_MAP);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
controller.startGalleryPick();
|
controller.startSingleGalleryPick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
controller.startGalleryPick();
|
controller.startSingleGalleryPick();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
|
import fr.free.nrw.commons.di.CommonsDaggerAppCompatActivity;
|
||||||
|
|
||||||
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
|
public abstract class BaseActivity extends CommonsDaggerAppCompatActivity {
|
||||||
boolean currentTheme;
|
protected boolean currentTheme;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,72 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.text.TextUtils;
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holds a description of an item being uploaded by {@link UploadActivity}
|
||||||
|
*/
|
||||||
class Description {
|
class Description {
|
||||||
|
|
||||||
private String languageId;
|
private String languageCode;
|
||||||
private String languageDisplayText;
|
|
||||||
private String descriptionText;
|
private String descriptionText;
|
||||||
private boolean set;
|
|
||||||
private int selectedLanguageIndex = -1;
|
private int selectedLanguageIndex = -1;
|
||||||
|
|
||||||
public String getLanguageId() {
|
/**
|
||||||
return languageId;
|
* @return The language code ie. "en" or "fr"
|
||||||
|
*/
|
||||||
|
String getLanguageCode() {
|
||||||
|
return languageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLanguageId(String languageId) {
|
/**
|
||||||
this.languageId = languageId;
|
* @param languageCode The language code ie. "en" or "fr"
|
||||||
|
*/
|
||||||
|
void setLanguageCode(String languageCode) {
|
||||||
|
this.languageCode = languageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getLanguageDisplayText() {
|
String getDescriptionText() {
|
||||||
return languageDisplayText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLanguageDisplayText(String languageDisplayText) {
|
|
||||||
this.languageDisplayText = languageDisplayText;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescriptionText() {
|
|
||||||
return descriptionText;
|
return descriptionText;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDescriptionText(String descriptionText) {
|
void setDescriptionText(String descriptionText) {
|
||||||
this.descriptionText = descriptionText;
|
this.descriptionText = descriptionText;
|
||||||
|
|
||||||
if (!TextUtils.isEmpty(descriptionText)) {
|
|
||||||
set = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSet() {
|
/**
|
||||||
return set;
|
* @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
|
||||||
}
|
*/
|
||||||
|
int getSelectedLanguageIndex() {
|
||||||
public void setSet(boolean set) {
|
|
||||||
this.set = set;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSelectedLanguageIndex() {
|
|
||||||
return selectedLanguageIndex;
|
return selectedLanguageIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectedLanguageIndex(int selectedLanguageIndex) {
|
/**
|
||||||
|
* @param selectedLanguageIndex the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
|
||||||
|
*/
|
||||||
|
void setSelectedLanguageIndex(int selectedLanguageIndex) {
|
||||||
this.selectedLanguageIndex = selectedLanguageIndex;
|
this.selectedLanguageIndex = selectedLanguageIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats the list of descriptions into the format Commons requires for uploads.
|
||||||
|
*
|
||||||
|
* @param descriptions the list of descriptions, description is ignored if text is null.
|
||||||
|
* @return a string with the pattern of {{en|1=descriptionText}}
|
||||||
|
*/
|
||||||
|
static String formatList(List<Description> descriptions) {
|
||||||
|
StringBuilder descListString = new StringBuilder();
|
||||||
|
for (Description description : descriptions) {
|
||||||
|
if (!description.isEmpty()) {
|
||||||
|
String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageCode(),
|
||||||
|
description.getDescriptionText());
|
||||||
|
descListString.append(individualDescription);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return descListString.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return descriptionText == null || descriptionText.isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,12 @@ package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.view.ViewCompat;
|
import android.support.v4.view.ViewCompat;
|
||||||
import android.support.v7.widget.AppCompatSpinner;
|
import android.support.v7.widget.AppCompatSpinner;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
@ -17,154 +17,196 @@ import android.widget.AdapterView.OnItemSelectedListener;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import butterknife.OnTouch;
|
import butterknife.OnTouch;
|
||||||
|
import butterknife.Optional;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.utils.AbstractTextWatcher;
|
||||||
|
import fr.free.nrw.commons.utils.BiMap;
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
|
import io.reactivex.subjects.BehaviorSubject;
|
||||||
|
import io.reactivex.subjects.Subject;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
import static android.view.MotionEvent.ACTION_UP;
|
import static android.view.MotionEvent.ACTION_UP;
|
||||||
|
|
||||||
class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
|
class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
|
||||||
|
|
||||||
List<Description> descriptions;
|
private Title title;
|
||||||
List<Language> languages;
|
private List<Description> descriptions;
|
||||||
private Context context;
|
private Context context;
|
||||||
private Callback callback;
|
private Callback callback;
|
||||||
|
private Subject<String> titleChangedSubject;
|
||||||
|
|
||||||
public DescriptionsAdapter() {
|
private BiMap<AdapterView, String> selectedLanguages;
|
||||||
|
private UploadView uploadView;
|
||||||
|
|
||||||
|
DescriptionsAdapter(UploadView uploadView) {
|
||||||
|
title = new Title();
|
||||||
descriptions = new ArrayList<>();
|
descriptions = new ArrayList<>();
|
||||||
descriptions.add(new Description());
|
titleChangedSubject = BehaviorSubject.create();
|
||||||
languages = new ArrayList<>();
|
selectedLanguages = new BiMap<>();
|
||||||
|
this.uploadView = uploadView;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCallback(Callback callback) {
|
void setCallback(Callback callback) {
|
||||||
this.callback = callback;
|
this.callback = callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDescriptions(List<Description> descriptions) {
|
void setItems(Title title, List<Description> descriptions) {
|
||||||
this.descriptions = descriptions;
|
this.descriptions = descriptions;
|
||||||
|
this.title = title;
|
||||||
|
selectedLanguages = new BiMap<>();
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLanguages(List<Language> languages) {
|
@Override
|
||||||
this.languages = languages;
|
public int getItemViewType(int position) {
|
||||||
|
if (position == 0) return 1;
|
||||||
|
else return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
View view = LayoutInflater.from(parent.getContext())
|
View view;
|
||||||
|
if (viewType == 1) {
|
||||||
|
view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.row_item_title, parent, false);
|
||||||
|
} else {
|
||||||
|
view = LayoutInflater.from(parent.getContext())
|
||||||
.inflate(R.layout.row_item_description, parent, false);
|
.inflate(R.layout.row_item_description, parent, false);
|
||||||
|
}
|
||||||
context = parent.getContext();
|
context = parent.getContext();
|
||||||
ViewHolder viewHolder = new ViewHolder(view);
|
return new ViewHolder(view);
|
||||||
return viewHolder;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
holder.init(position);
|
holder.init(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() {
|
public int getItemCount() {
|
||||||
return descriptions.size();
|
return descriptions.size() + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Description> getDescriptions() {
|
List<Description> getDescriptions() {
|
||||||
return descriptions;
|
return descriptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addDescription(Description description) {
|
void addDescription(Description description) {
|
||||||
this.descriptions.add(description);
|
this.descriptions.add(description);
|
||||||
notifyItemInserted(descriptions.size() - 1);
|
notifyItemInserted(descriptions.size() + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Title getTitle() {
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitle(Title title) {
|
||||||
|
this.title = title;
|
||||||
|
notifyItemInserted(0);
|
||||||
|
}
|
||||||
|
|
||||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
@BindView(R.id.spinner_description_languages)
|
@BindView(R.id.spinner_description_languages)
|
||||||
AppCompatSpinner spinnerDescriptionLanguages;
|
AppCompatSpinner spinnerDescriptionLanguages;
|
||||||
@BindView(R.id.et_description_text)
|
|
||||||
EditText etDescriptionText;
|
|
||||||
private View view;
|
|
||||||
|
|
||||||
|
@BindView(R.id.description_item_edit_text)
|
||||||
|
EditText descItemEditText;
|
||||||
|
|
||||||
|
private View view;
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
public ViewHolder(View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
ButterKnife.bind(this, itemView);
|
ButterKnife.bind(this, itemView);
|
||||||
this.view = itemView;
|
this.view = itemView;
|
||||||
|
Timber.i("descItemEditText:" + descItemEditText);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(int position) {
|
public void init(int position) {
|
||||||
Description description = descriptions.get(position);
|
if (position == 0) {
|
||||||
if (!TextUtils.isEmpty(description.getDescriptionText())) {
|
Timber.d("Title is " + title);
|
||||||
etDescriptionText.setText(description.getDescriptionText());
|
if (!title.isEmpty()) {
|
||||||
|
descItemEditText.setText(title.toString());
|
||||||
} else {
|
} else {
|
||||||
etDescriptionText.setText("");
|
descItemEditText.setText("");
|
||||||
}
|
}
|
||||||
Drawable drawableRight = context.getResources()
|
|
||||||
.getDrawable(R.drawable.mapbox_info_icon_default);
|
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
|
||||||
if (position != 0) {
|
|
||||||
etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
|
descItemEditText.addTextChangedListener(new AbstractTextWatcher(titleText ->{
|
||||||
|
title.setTitleText(titleText);
|
||||||
|
titleChangedSubject.onNext(titleText);
|
||||||
|
}));
|
||||||
|
|
||||||
|
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
|
if (!hasFocus) {
|
||||||
|
ViewUtil.hideKeyboard(v);
|
||||||
} else {
|
} else {
|
||||||
etDescriptionText.setCompoundDrawablesWithIntrinsicBounds(null, null, drawableRight, null);
|
uploadView.setTopCardState(false);
|
||||||
}
|
|
||||||
|
|
||||||
etDescriptionText.addTextChangedListener(new TextWatcher() {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable editable) {
|
|
||||||
description.setDescriptionText(editable.toString());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
etDescriptionText.setOnFocusChangeListener((v, hasFocus) -> {
|
} else {
|
||||||
|
Description description = descriptions.get(position - 1);
|
||||||
|
Timber.d("Description is " + description);
|
||||||
|
if (!TextUtils.isEmpty(description.getDescriptionText())) {
|
||||||
|
descItemEditText.setText(description.getDescriptionText());
|
||||||
|
} else {
|
||||||
|
descItemEditText.setText("");
|
||||||
|
}
|
||||||
|
if (position == 1) {
|
||||||
|
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(), null);
|
||||||
|
} else {
|
||||||
|
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
descItemEditText.addTextChangedListener(new AbstractTextWatcher(descriptionText -> {
|
||||||
|
description.setDescriptionText(descriptionText);
|
||||||
|
}));
|
||||||
|
descItemEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||||
if (!hasFocus) {
|
if (!hasFocus) {
|
||||||
ViewUtil.hideKeyboard(v);
|
ViewUtil.hideKeyboard(v);
|
||||||
|
} else {
|
||||||
|
uploadView.setTopCardState(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context,
|
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(context,
|
||||||
R.layout.row_item_languages_spinner);
|
R.layout.row_item_languages_spinner, selectedLanguages);
|
||||||
Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayLanguage()
|
|
||||||
.compareTo(t1.getLocale().getDisplayLanguage().toString()));
|
|
||||||
languagesAdapter.setLanguages(languages);
|
|
||||||
languagesAdapter.notifyDataSetChanged();
|
languagesAdapter.notifyDataSetChanged();
|
||||||
spinnerDescriptionLanguages.setAdapter(languagesAdapter);
|
spinnerDescriptionLanguages.setAdapter(languagesAdapter);
|
||||||
|
|
||||||
if (description.getSelectedLanguageIndex() == -1) {
|
if (description.getSelectedLanguageIndex() == -1) {
|
||||||
if (position == 0) {
|
if (position == 1) {
|
||||||
int defaultLocaleIndex = getIndexOfUserDefaultLocale();
|
int defaultLocaleIndex = languagesAdapter.getIndexOfUserDefaultLocale(context);
|
||||||
spinnerDescriptionLanguages.setSelection(defaultLocaleIndex);
|
spinnerDescriptionLanguages.setSelection(defaultLocaleIndex);
|
||||||
} else {
|
} else {
|
||||||
spinnerDescriptionLanguages.setSelection(0);
|
spinnerDescriptionLanguages.setSelection(0);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
|
spinnerDescriptionLanguages.setSelection(description.getSelectedLanguageIndex());
|
||||||
|
selectedLanguages.put(spinnerDescriptionLanguages, description.getLanguageCode());
|
||||||
}
|
}
|
||||||
|
|
||||||
languages.get(spinnerDescriptionLanguages.getSelectedItemPosition()).setSet(true);
|
|
||||||
|
|
||||||
//TODO do it the butterknife way
|
//TODO do it the butterknife way
|
||||||
spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
|
spinnerDescriptionLanguages.setOnItemSelectedListener(new OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
|
public void onItemSelected(AdapterView<?> adapterView, View view, int position,
|
||||||
long l) {
|
long l) {
|
||||||
//TODO handle case when user tries to select an already selected language
|
description.setSelectedLanguageIndex(position);
|
||||||
updateDescriptionBasedOnSelectedLanguageIndex(description, position);
|
String languageCode = ((SpinnerLanguagesAdapter) adapterView.getAdapter()).getLanguageCode(position);
|
||||||
|
description.setLanguageCode(languageCode);
|
||||||
|
selectedLanguages.remove(adapterView);
|
||||||
|
selectedLanguages.put(adapterView, languageCode);
|
||||||
|
((SpinnerLanguagesAdapter) adapterView.getAdapter()).selectedLangCode = languageCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -172,27 +214,44 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnTouch(R.id.et_description_text)
|
@Optional
|
||||||
|
@OnTouch(R.id.description_item_edit_text)
|
||||||
boolean descriptionInfo(View view, MotionEvent motionEvent) {
|
boolean descriptionInfo(View view, MotionEvent motionEvent) {
|
||||||
|
//Title info is visible only for the title
|
||||||
if (getAdapterPosition() == 0) {
|
if (getAdapterPosition() == 0) {
|
||||||
//Description info is visible only for the first item
|
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||||
|
final int value = view.getRight() - descItemEditText
|
||||||
|
.getCompoundDrawables()[2]
|
||||||
|
.getBounds().width();
|
||||||
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
|
||||||
|
callback.showAlert(R.string.media_detail_title, R.string.title_info);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final int value = descItemEditText.getLeft() + descItemEditText
|
||||||
|
.getCompoundDrawables()[0]
|
||||||
|
.getBounds().width();
|
||||||
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
|
||||||
|
callback.showAlert(R.string.media_detail_title, R.string.title_info);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//Description info is visible only for the first description
|
||||||
|
} else if (getAdapterPosition() == 1) {
|
||||||
final int value;
|
final int value;
|
||||||
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
if (ViewCompat.getLayoutDirection(view) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
||||||
value = etDescriptionText.getRight() - etDescriptionText
|
value = view.getRight() - descItemEditText.getCompoundDrawables()[2].getBounds().width();
|
||||||
.getCompoundDrawables()[2]
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
|
||||||
.getBounds().width() - etDescriptionText.getPaddingRight();
|
|
||||||
if (motionEvent.getAction() == ACTION_UP && motionEvent.getX() >= value) {
|
|
||||||
callback.showAlert(R.string.media_detail_description,
|
callback.showAlert(R.string.media_detail_description,
|
||||||
R.string.description_info);
|
R.string.description_info);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = etDescriptionText.getLeft() + etDescriptionText
|
value = descItemEditText.getLeft() + descItemEditText
|
||||||
.getCompoundDrawables()[0]
|
.getCompoundDrawables()[0]
|
||||||
.getBounds().width();
|
.getBounds().width();
|
||||||
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
|
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
|
||||||
|
|
@ -206,27 +265,12 @@ class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getIndexOfUserDefaultLocale() {
|
private Drawable getInfoIcon() {
|
||||||
for (int i = 0; i < languages.size(); i++) {
|
return context.getResources()
|
||||||
if (languages.get(i).getLocale()
|
.getDrawable(R.drawable.mapbox_info_icon_default);
|
||||||
.equals(context.getResources().getConfiguration().locale)) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateDescriptionBasedOnSelectedLanguageIndex(Description description,
|
|
||||||
int position) {
|
|
||||||
Language language = languages.get(position);
|
|
||||||
Locale locale = language.getLocale();
|
|
||||||
description.setSelectedLanguageIndex(position);
|
|
||||||
description.setLanguageDisplayText(locale.getDisplayName());
|
|
||||||
description.setLanguageId(locale.getLanguage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Callback {
|
public interface Callback {
|
||||||
|
|
||||||
void showAlert(int mediaDetailDescription, int descriptionInfo);
|
void showAlert(int mediaDetailDescription, int descriptionInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.BitmapRegionDecoder;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
|
||||||
import fr.free.nrw.commons.utils.ImageUtils;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Created by bluesir9 on 16/9/17.
|
|
||||||
*
|
|
||||||
* <p>Responsible for checking if the picture that the user is trying to upload is useful or not. Will attempt to filter
|
|
||||||
* away completely black,fuzzy/blurry pictures(for now).
|
|
||||||
*
|
|
||||||
* <p>todo: Detect selfies?
|
|
||||||
*/
|
|
||||||
|
|
||||||
public class DetectUnwantedPicturesAsync extends AsyncTask<Void, Void, ImageUtils.Result> {
|
|
||||||
|
|
||||||
private final String imageMediaFilePath;
|
|
||||||
public final WeakReference<Activity> activityWeakReference;
|
|
||||||
|
|
||||||
DetectUnwantedPicturesAsync(WeakReference<Activity> activityWeakReference, String imageMediaFilePath) {
|
|
||||||
//this.callback = callback;
|
|
||||||
this.imageMediaFilePath = imageMediaFilePath;
|
|
||||||
this.activityWeakReference = activityWeakReference;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected ImageUtils.Result doInBackground(Void... voids) {
|
|
||||||
try {
|
|
||||||
Timber.d("FilePath: " + imageMediaFilePath);
|
|
||||||
if (imageMediaFilePath == null) {
|
|
||||||
return ImageUtils.Result.IMAGE_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(imageMediaFilePath,false);
|
|
||||||
|
|
||||||
return ImageUtils.checkIfImageIsTooDark(decoder);
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Timber.e(ioe, "IO Exception");
|
|
||||||
return ImageUtils.Result.IMAGE_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(ImageUtils.Result result) {
|
|
||||||
super.onPostExecute(result);
|
|
||||||
Activity activity = activityWeakReference.get();
|
|
||||||
|
|
||||||
if (result != ImageUtils.Result.IMAGE_OK) {
|
|
||||||
//show appropriate error message
|
|
||||||
String errorMessage = result == ImageUtils.Result.IMAGE_DARK ? activity.getString(R.string.upload_image_too_dark) : activity.getString(R.string.upload_image_blurry);
|
|
||||||
AlertDialog.Builder errorDialogBuilder = new AlertDialog.Builder(activity);
|
|
||||||
errorDialogBuilder.setMessage(errorMessage);
|
|
||||||
errorDialogBuilder.setTitle(activity.getString(R.string.warning));
|
|
||||||
errorDialogBuilder.setPositiveButton(activity.getString(R.string.no), (dialogInterface, i) -> {
|
|
||||||
//user does not wish to upload the picture, take them back to MainActivity
|
|
||||||
Intent intent = new Intent(activity, MainActivity.class);
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
activity.startActivity(intent);
|
|
||||||
});
|
|
||||||
errorDialogBuilder.setNegativeButton(activity.getString(R.string.yes), (dialogInterface, i) -> {
|
|
||||||
//user wishes to go ahead with the upload of this picture, just dismiss this dialog
|
|
||||||
dialogInterface.dismiss();
|
|
||||||
});
|
|
||||||
|
|
||||||
AlertDialog errorDialog = errorDialogBuilder.create();
|
|
||||||
if (!activity.isFinishing()) {
|
|
||||||
errorDialog.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
|
||||||
|
import com.karumi.dexter.Dexter;
|
||||||
|
import com.karumi.dexter.DexterBuilder;
|
||||||
|
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
||||||
|
import com.karumi.dexter.listener.PermissionGrantedResponse;
|
||||||
|
import com.karumi.dexter.listener.single.BasePermissionListener;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil;
|
||||||
|
import fr.free.nrw.commons.utils.ExternalStorageUtils;
|
||||||
|
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.subjects.CompletableSubject;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class DexterPermissionObtainer {
|
||||||
|
private final String requestedPermission;
|
||||||
|
private android.app.AlertDialog storagePermissionInfoDialog;
|
||||||
|
private DexterBuilder dexterStoragePermissionBuilder;
|
||||||
|
|
||||||
|
private PermissionDeniedResponse permissionDeniedResponse;
|
||||||
|
|
||||||
|
private boolean storagePromptInProgress;
|
||||||
|
|
||||||
|
private final String rationaleTitle;
|
||||||
|
private final String rationaleText;
|
||||||
|
|
||||||
|
private Activity activity;
|
||||||
|
|
||||||
|
private CompletableSubject storagePromptObservable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param activity The activity that is requesting the permission
|
||||||
|
* @param requestedPermission The permission being requested in the form of Manifest.permission.*
|
||||||
|
* @param rationaleTitle The title of the rationale dialog
|
||||||
|
* @param rationaleText The text inside the rationale dialog
|
||||||
|
*/
|
||||||
|
DexterPermissionObtainer(Activity activity, String requestedPermission, String rationaleTitle, String rationaleText) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.rationaleTitle = rationaleTitle;
|
||||||
|
this.rationaleText = rationaleText;
|
||||||
|
this.requestedPermission = requestedPermission;
|
||||||
|
this.storagePromptObservable = CompletableSubject.create();
|
||||||
|
initPermissionsRationaleDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if storage permissions are obtained, prompts the users to grant storage permissions if necessary.
|
||||||
|
* When storage permission is present, onPermissionObtained is called.
|
||||||
|
*/
|
||||||
|
Completable confirmStoragePermissions() {
|
||||||
|
if (ExternalStorageUtils.isStoragePermissionGranted(activity)) {
|
||||||
|
Timber.i("Storage permissions already granted.");
|
||||||
|
storagePromptObservable.onComplete();
|
||||||
|
} else if (!storagePromptInProgress) {
|
||||||
|
if (storagePromptObservable.hasComplete()) {
|
||||||
|
storagePromptObservable = CompletableSubject.create();
|
||||||
|
}
|
||||||
|
//If permission is not there, ask for it
|
||||||
|
storagePromptInProgress = true;
|
||||||
|
askDexterToHandleExternalStoragePermission();
|
||||||
|
}
|
||||||
|
return storagePromptObservable;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* To be called when the user returns to the original activity after manually enabling storage permissions.
|
||||||
|
*/
|
||||||
|
void onManualPermissionReturned() {
|
||||||
|
//OnActivity result, no matter what the result is, our function can handle that.
|
||||||
|
askDexterToHandleExternalStoragePermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
|
||||||
|
* only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks
|
||||||
|
* for the required permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
|
||||||
|
*/
|
||||||
|
private void askDexterToHandleExternalStoragePermission() {
|
||||||
|
Timber.d("External storage permission is being requested");
|
||||||
|
if (null == dexterStoragePermissionBuilder) {
|
||||||
|
dexterStoragePermissionBuilder = Dexter.withActivity(activity)
|
||||||
|
.withPermission(requestedPermission)
|
||||||
|
.withListener(new BasePermissionListener() {
|
||||||
|
@Override
|
||||||
|
public void onPermissionGranted(PermissionGrantedResponse response) {
|
||||||
|
Timber.d("User has granted us the permission for writing the external storage");
|
||||||
|
//If permission is granted, well and good
|
||||||
|
storagePromptInProgress = false;
|
||||||
|
storagePromptObservable.onComplete();
|
||||||
|
//onPermissionObtained.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPermissionDenied(PermissionDeniedResponse response) {
|
||||||
|
Timber.d("User has granted us the permission for writing the external storage");
|
||||||
|
//If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
|
||||||
|
permissionDeniedResponse = response;
|
||||||
|
if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
|
||||||
|
.isShowing()) {
|
||||||
|
storagePermissionInfoDialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
dexterStoragePermissionBuilder.check();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We have agreed to show a dialog showing why we need a particular permission.
|
||||||
|
* This method is used to initialise the dialog which is going to show the permission's rationale.
|
||||||
|
* The dialog is initialised along with a callback for positive and negative user actions.
|
||||||
|
*/
|
||||||
|
private void initPermissionsRationaleDialog() {
|
||||||
|
if (storagePermissionInfoDialog == null) {
|
||||||
|
storagePermissionInfoDialog = DialogUtil
|
||||||
|
.getAlertDialogWithPositiveAndNegativeCallbacks(
|
||||||
|
activity,
|
||||||
|
rationaleTitle, rationaleText,
|
||||||
|
R.drawable.ic_launcher, new DialogUtil.Callback() {
|
||||||
|
@Override
|
||||||
|
public void onPositiveButtonClicked() {
|
||||||
|
//If the user is willing to give us the permission
|
||||||
|
//But had somehow previously choose never ask again, we take him to app settings to manually enable permission
|
||||||
|
if (null == permissionDeniedResponse) {
|
||||||
|
//Dexter returned null, lets see if this ever happens
|
||||||
|
Timber.w("Dexter returned null as permissionDeniedResponse");
|
||||||
|
} else if (permissionDeniedResponse.isPermanentlyDenied()) {
|
||||||
|
PermissionUtils.askUserToManuallyEnablePermissionFromSettings(activity);
|
||||||
|
Timber.i("Permission permanently denied.");
|
||||||
|
} else {
|
||||||
|
//or if we still have chance to show runtime permission dialog, we show him that.
|
||||||
|
askDexterToHandleExternalStoragePermission();
|
||||||
|
Timber.d("Asking via Dexter for permission.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNegativeButtonClicked() {
|
||||||
|
//This was the behaviour as of now, I was planning to maybe snack him with some message
|
||||||
|
//and then call finish after some time, or may be it could be associated with some action
|
||||||
|
// on the snack. If the user does not want us to give the permission, even after showing
|
||||||
|
// rationale dialog, lets not trouble him any more.
|
||||||
|
activity.finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.contributions.MainActivity;
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends asynchronous queries to the Commons MediaWiki API to check that file doesn't already exist
|
|
||||||
* Displays a warning to the user if the file already exists on Commons
|
|
||||||
*/
|
|
||||||
public class ExistingFileAsync extends AsyncTask<Void, Void, Boolean> {
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
void onResult(Result result);
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum Result {
|
|
||||||
NO_DUPLICATE,
|
|
||||||
DUPLICATE_PROCEED,
|
|
||||||
DUPLICATE_CANCELLED
|
|
||||||
}
|
|
||||||
|
|
||||||
private final WeakReference<Activity> activity;
|
|
||||||
private final MediaWikiApi api;
|
|
||||||
private final String fileSha1;
|
|
||||||
private final WeakReference<Context> context;
|
|
||||||
private final Callback callback;
|
|
||||||
|
|
||||||
public ExistingFileAsync(WeakReference<Activity> activity, String fileSha1, WeakReference<Context> context, Callback callback, MediaWikiApi mwApi) {
|
|
||||||
this.activity = activity;
|
|
||||||
this.fileSha1 = fileSha1;
|
|
||||||
this.context = context;
|
|
||||||
this.callback = callback;
|
|
||||||
this.api = mwApi;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPreExecute() {
|
|
||||||
super.onPreExecute();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Boolean doInBackground(Void... voids) {
|
|
||||||
|
|
||||||
// https://commons.wikimedia.org/w/api.php?action=query&list=allimages&format=xml&aisha1=801957214aba50cb63bb6eb1b0effa50188900ba
|
|
||||||
boolean fileExists;
|
|
||||||
try {
|
|
||||||
String fileSha1 = this.fileSha1;
|
|
||||||
fileExists = api.existingFile(fileSha1);
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "IO Exception: ");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("File already exists in Commons: %s", fileExists);
|
|
||||||
return fileExists;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Boolean fileExists) {
|
|
||||||
super.onPostExecute(fileExists);
|
|
||||||
|
|
||||||
// If file exists, display warning to user.
|
|
||||||
// Use soft warning for now (user able to choose to proceed) until have determined that implementation works without bugs
|
|
||||||
if (fileExists) {
|
|
||||||
AlertDialog.Builder builder = new AlertDialog.Builder(context.get());
|
|
||||||
builder.setMessage(R.string.file_exists)
|
|
||||||
.setTitle(R.string.warning);
|
|
||||||
builder.setPositiveButton(R.string.no, (dialog, id) -> {
|
|
||||||
//Go back to MainActivity
|
|
||||||
Intent intent = new Intent(context.get(), MainActivity.class);
|
|
||||||
context.get().startActivity(intent);
|
|
||||||
callback.onResult(Result.DUPLICATE_CANCELLED);
|
|
||||||
});
|
|
||||||
builder.setNegativeButton(R.string.yes, (dialog, id) -> callback.onResult(Result.DUPLICATE_PROCEED));
|
|
||||||
|
|
||||||
AlertDialog dialog = builder.create();
|
|
||||||
if (!activity.get().isFinishing()) {
|
|
||||||
dialog.show();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
callback.onResult(Result.NO_DUPLICATE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +1,21 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.media.ExifInterface;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.app.AppCompatActivity;
|
import android.support.v7.app.AppCompatActivity;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
|
@ -44,89 +44,44 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||||
@Inject
|
@Inject
|
||||||
@Named("default_preferences")
|
@Named("default_preferences")
|
||||||
SharedPreferences prefs;
|
SharedPreferences prefs;
|
||||||
private Uri mediaUri;
|
private String filePath;
|
||||||
private ContentResolver contentResolver;
|
private ContentResolver contentResolver;
|
||||||
private GPSExtractor imageObj;
|
private GPSExtractor imageObj;
|
||||||
private Context context;
|
private Context context;
|
||||||
private String decimalCoords;
|
private String decimalCoords;
|
||||||
private boolean haveCheckedForOtherImages = false;
|
private ExifInterface exifInterface;
|
||||||
private String filePath;
|
|
||||||
private boolean useExtStorage;
|
private boolean useExtStorage;
|
||||||
private boolean cacheFound;
|
private boolean haveCheckedForOtherImages = false;
|
||||||
private GPSExtractor tempImageObj;
|
private GPSExtractor tempImageObj;
|
||||||
|
|
||||||
FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) {
|
FileProcessor(@NonNull String filePath, ContentResolver contentResolver, Context context) {
|
||||||
this.mediaUri = mediaUri;
|
this.filePath = filePath;
|
||||||
this.contentResolver = contentResolver;
|
this.contentResolver = contentResolver;
|
||||||
this.context = context;
|
this.context = context;
|
||||||
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
|
ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this);
|
||||||
|
try {
|
||||||
|
exifInterface=new ExifInterface(filePath);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Timber.e(e);
|
||||||
|
}
|
||||||
useExtStorage = prefs.getBoolean("useExternalStorage", true);
|
useExtStorage = prefs.getBoolean("useExternalStorage", true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets file path from media URI.
|
|
||||||
* In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead.
|
|
||||||
*
|
|
||||||
* @return file path of media
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private String getPathOfMediaOrCopy() {
|
|
||||||
filePath = FileUtils.getPath(context, mediaUri);
|
|
||||||
Timber.d("Filepath: " + filePath);
|
|
||||||
if (filePath == null) {
|
|
||||||
String copyPath = null;
|
|
||||||
try {
|
|
||||||
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
|
|
||||||
if (descriptor != null) {
|
|
||||||
if (useExtStorage) {
|
|
||||||
copyPath = FileUtils.createCopyPath(descriptor);
|
|
||||||
return copyPath;
|
|
||||||
}
|
|
||||||
copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg";
|
|
||||||
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
|
|
||||||
Timber.d("Filepath (copied): %s", copyPath);
|
|
||||||
return copyPath;
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.w(e, "Error in file " + copyPath);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Processes file coordinates, either from EXIF data or user location
|
* Processes file coordinates, either from EXIF data or user location
|
||||||
*
|
|
||||||
* @param gpsEnabled if true use GPS
|
|
||||||
*/
|
*/
|
||||||
GPSExtractor processFileCoordinates(boolean gpsEnabled) {
|
GPSExtractor processFileCoordinates(SimilarImageInterface similarImageInterface) {
|
||||||
Timber.d("Calling GPSExtractor");
|
Timber.d("Calling GPSExtractor");
|
||||||
try {
|
imageObj = new GPSExtractor(exifInterface);
|
||||||
ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r");
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
if (descriptor != null) {
|
|
||||||
imageObj = new GPSExtractor(descriptor.getFileDescriptor());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String filePath = getPathOfMediaOrCopy();
|
|
||||||
if (filePath != null) {
|
|
||||||
imageObj = new GPSExtractor(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
decimalCoords = imageObj.getCoords();
|
decimalCoords = imageObj.getCoords();
|
||||||
if (decimalCoords == null || !imageObj.imageCoordsExists) {
|
if (decimalCoords == null || !imageObj.imageCoordsExists) {
|
||||||
//Find other photos taken around the same time which has gps coordinates
|
//Find other photos taken around the same time which has gps coordinates
|
||||||
if (!haveCheckedForOtherImages)
|
if (!haveCheckedForOtherImages)
|
||||||
findOtherImages();// Do not do repeat the process
|
findOtherImages(similarImageInterface);// Do not do repeat the process
|
||||||
} else {
|
} else {
|
||||||
useImageCoords();
|
useImageCoords();
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
Timber.w("File not found: " + mediaUri, e);
|
|
||||||
}
|
|
||||||
return imageObj;
|
return imageObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -136,10 +91,10 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find other images around the same location that were taken within the last 20 sec
|
* Find other images around the same location that were taken within the last 20 sec
|
||||||
*
|
* @param similarImageInterface
|
||||||
*/
|
*/
|
||||||
private void findOtherImages() {
|
private void findOtherImages(SimilarImageInterface similarImageInterface) {
|
||||||
Timber.d("filePath" + getPathOfMediaOrCopy());
|
Timber.d("filePath" + filePath);
|
||||||
|
|
||||||
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
|
long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created
|
||||||
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
|
File folder = new File(filePath.substring(0, filePath.lastIndexOf('/')));
|
||||||
|
|
@ -154,7 +109,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||||
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
|
tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos
|
||||||
ParcelFileDescriptor descriptor = null;
|
ParcelFileDescriptor descriptor = null;
|
||||||
try {
|
try {
|
||||||
descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r");
|
descriptor = contentResolver.openFileDescriptor(Uri.fromFile(file), "r");
|
||||||
} catch (FileNotFoundException e) {
|
} catch (FileNotFoundException e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
}
|
}
|
||||||
|
|
@ -173,12 +128,7 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||||
if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) {
|
if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) {
|
||||||
// Current image has gps coordinates and it's not current gps locaiton
|
// Current image has gps coordinates and it's not current gps locaiton
|
||||||
Timber.d("This file has image coords:" + file.getAbsolutePath());
|
Timber.d("This file has image coords:" + file.getAbsolutePath());
|
||||||
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
|
similarImageInterface.showSimilarImageFragment(filePath, file.getAbsolutePath());
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putString("originalImagePath", filePath);
|
|
||||||
args.putString("possibleImagePath", file.getAbsolutePath());
|
|
||||||
newFragment.setArguments(args);
|
|
||||||
newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,7 +160,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||||
|
|
||||||
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
|
// If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories
|
||||||
if (catListEmpty) {
|
if (catListEmpty) {
|
||||||
cacheFound = false;
|
|
||||||
apiCall.request(decimalCoords)
|
apiCall.request(decimalCoords)
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(Schedulers.io())
|
.observeOn(Schedulers.io())
|
||||||
|
|
@ -223,7 +172,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||||
);
|
);
|
||||||
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
|
Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList);
|
||||||
} else {
|
} else {
|
||||||
cacheFound = true;
|
|
||||||
Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
|
Timber.d("Cache found, setting categoryList in model to %s", displayCatList);
|
||||||
gpsCategoryModel.setCategoryList(displayCatList);
|
gpsCategoryModel.setCategoryList(displayCatList);
|
||||||
}
|
}
|
||||||
|
|
@ -232,20 +180,6 @@ public class FileProcessor implements SimilarImageDialogFragment.onResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isCacheFound() {
|
|
||||||
return cacheFound;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Calls the async task that detects if image is fuzzy, too dark, etc
|
|
||||||
*/
|
|
||||||
void detectUnwantedPictures() {
|
|
||||||
String imageMediaFilePath = FileUtils.getPath(context, mediaUri);
|
|
||||||
DetectUnwantedPicturesAsync detectUnwantedPicturesAsync
|
|
||||||
= new DetectUnwantedPicturesAsync(new WeakReference<Activity>((Activity) context), imageMediaFilePath);
|
|
||||||
detectUnwantedPicturesAsync.execute();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPositiveResponse() {
|
public void onPositiveResponse() {
|
||||||
imageObj = tempImageObj;
|
imageObj = tempImageObj;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.ContentResolver;
|
||||||
import android.content.ContentUris;
|
import android.content.ContentUris;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
@ -12,6 +13,7 @@ import android.os.ParcelFileDescriptor;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
import android.provider.DocumentsContract;
|
import android.provider.DocumentsContract;
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
import android.provider.OpenableColumns;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
|
@ -33,6 +35,8 @@ import java.util.Date;
|
||||||
|
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext;
|
||||||
|
|
||||||
public class FileUtils {
|
public class FileUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -76,21 +80,32 @@ public class FileUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
|
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
|
||||||
|
*
|
||||||
* @return path of copy
|
* @return path of copy
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@NonNull
|
||||||
static String createCopyPath(ParcelFileDescriptor descriptor) {
|
static String createExternalCopyPathAndCopy(Uri uri, ContentResolver contentResolver) throws IOException {
|
||||||
try {
|
FileDescriptor fileDescriptor = contentResolver.openFileDescriptor(uri, "r").getFileDescriptor();
|
||||||
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg";
|
String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + "." + getFileExt(uri, contentResolver);
|
||||||
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
|
File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp");
|
||||||
newFile.mkdir();
|
newFile.mkdir();
|
||||||
FileUtils.copy(descriptor.getFileDescriptor(), copyPath);
|
FileUtils.copy(fileDescriptor, copyPath);
|
||||||
Timber.d("Filepath (copied): %s", copyPath);
|
Timber.d("Filepath (copied): %s", copyPath);
|
||||||
return copyPath;
|
return copyPath;
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead.
|
||||||
|
*
|
||||||
|
* @return path of copy
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
static String createCopyPathAndCopy(Uri uri, Context context) throws IOException {
|
||||||
|
FileDescriptor fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r").getFileDescriptor();
|
||||||
|
String copyPath = context.getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + "." + getFileExt(uri, context.getContentResolver());
|
||||||
|
FileUtils.copy(fileDescriptor, copyPath);
|
||||||
|
Timber.d("Filepath (copied): %s", copyPath);
|
||||||
|
return copyPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -304,6 +319,7 @@ public class FileUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Read and return the content of a resource file as string.
|
* Read and return the content of a resource file as string.
|
||||||
|
*
|
||||||
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
|
* @param fileName asset file's path (e.g. "/queries/nearby_query.rq")
|
||||||
* @return the content of the file
|
* @return the content of the file
|
||||||
*/
|
*/
|
||||||
|
|
@ -330,6 +346,7 @@ public class FileUtils {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes files.
|
* Deletes files.
|
||||||
|
*
|
||||||
* @param file context
|
* @param file context
|
||||||
*/
|
*/
|
||||||
public static boolean deleteFile(File file) {
|
public static boolean deleteFile(File file) {
|
||||||
|
|
@ -355,7 +372,7 @@ public class FileUtils {
|
||||||
commonsAppDirectory.mkdir();
|
commonsAppDirectory.mkdir();
|
||||||
}
|
}
|
||||||
|
|
||||||
File logsFile = new File(commonsAppDirectory,"logs.txt");
|
File logsFile = new File(commonsAppDirectory, "logs.txt");
|
||||||
if (logsFile.exists()) {
|
if (logsFile.exists()) {
|
||||||
//old logs file is useless
|
//old logs file is useless
|
||||||
logsFile.delete();
|
logsFile.delete();
|
||||||
|
|
@ -377,4 +394,39 @@ public class FileUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getFilename(Uri uri, ContentResolver contentResolver) {
|
||||||
|
if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.JELLY_BEAN)
|
||||||
|
return "";
|
||||||
|
String result = null;
|
||||||
|
if (uri.getScheme().equals("content")) {
|
||||||
|
try (Cursor cursor = contentResolver.query(uri, null, null, null, null)) {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
result = cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (result == null) {
|
||||||
|
result = uri.getPath();
|
||||||
|
int cut = result.lastIndexOf('/');
|
||||||
|
if (cut != -1) {
|
||||||
|
result = result.substring(cut + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getFileExt(String fileName){
|
||||||
|
//Default file extension
|
||||||
|
String extension=".jpg";
|
||||||
|
|
||||||
|
int i = fileName.lastIndexOf('.');
|
||||||
|
if (i > 0) {
|
||||||
|
extension = fileName.substring(i+1);
|
||||||
|
}
|
||||||
|
return extension;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getFileExt(Uri uri, ContentResolver contentResolver) {
|
||||||
|
return getFileExt(getFilename(uri, contentResolver));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,11 +16,22 @@ import timber.log.Timber;
|
||||||
*/
|
*/
|
||||||
public class GPSExtractor {
|
public class GPSExtractor {
|
||||||
|
|
||||||
private ExifInterface exif;
|
public static final GPSExtractor DUMMY= new GPSExtractor();
|
||||||
private double decLatitude;
|
private double decLatitude;
|
||||||
private double decLongitude;
|
private double decLongitude;
|
||||||
public boolean imageCoordsExists;
|
public boolean imageCoordsExists;
|
||||||
|
private String latitude;
|
||||||
|
private String longitude;
|
||||||
|
private String latitudeRef;
|
||||||
|
private String longitudeRef;
|
||||||
|
private String decimalCoords;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dummy constructor.
|
||||||
|
*/
|
||||||
|
private GPSExtractor(){
|
||||||
|
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Construct from the file descriptor of the image (only for API 24 or newer).
|
* Construct from the file descriptor of the image (only for API 24 or newer).
|
||||||
* @param fileDescriptor the file descriptor of the image
|
* @param fileDescriptor the file descriptor of the image
|
||||||
|
|
@ -28,7 +39,8 @@ public class GPSExtractor {
|
||||||
@RequiresApi(24)
|
@RequiresApi(24)
|
||||||
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
|
public GPSExtractor(@NonNull FileDescriptor fileDescriptor) {
|
||||||
try {
|
try {
|
||||||
exif = new ExifInterface(fileDescriptor);
|
ExifInterface exif = new ExifInterface(fileDescriptor);
|
||||||
|
processCoords(exif);
|
||||||
} catch (IOException | IllegalArgumentException e) {
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
Timber.w(e);
|
Timber.w(e);
|
||||||
}
|
}
|
||||||
|
|
@ -41,39 +53,46 @@ public class GPSExtractor {
|
||||||
*/
|
*/
|
||||||
public GPSExtractor(@NonNull String path) {
|
public GPSExtractor(@NonNull String path) {
|
||||||
try {
|
try {
|
||||||
exif = new ExifInterface(path);
|
ExifInterface exif = new ExifInterface(path);
|
||||||
|
processCoords(exif);
|
||||||
} catch (IOException | IllegalArgumentException e) {
|
} catch (IOException | IllegalArgumentException e) {
|
||||||
Timber.w(e);
|
Timber.w(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct from the file path of the image.
|
||||||
|
* @param exif exif interface of the image
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
public GPSExtractor(@NonNull ExifInterface exif){
|
||||||
|
processCoords(exif);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void processCoords(ExifInterface exif){
|
||||||
|
//If image has no EXIF data and user has enabled GPS setting, get user's location
|
||||||
|
//Always return null as a temporary fix for #1599
|
||||||
|
if (exif != null && exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) != null) {
|
||||||
|
//If image has EXIF data, extract image coords
|
||||||
|
imageCoordsExists = true;
|
||||||
|
Timber.d("EXIF data has location info");
|
||||||
|
|
||||||
|
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
|
||||||
|
latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
|
||||||
|
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
|
||||||
|
longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts geolocation (either of image from EXIF data, or of user)
|
* Extracts geolocation (either of image from EXIF data, or of user)
|
||||||
* @return coordinates as string (needs to be passed as a String in API query)
|
* @return coordinates as string (needs to be passed as a String in API query)
|
||||||
*/
|
*/
|
||||||
@Nullable
|
@Nullable
|
||||||
public String getCoords() {
|
public String getCoords() {
|
||||||
String latitude;
|
if(decimalCoords!=null){
|
||||||
String longitude;
|
return decimalCoords;
|
||||||
String latitudeRef;
|
}else if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
|
||||||
String longitudeRef;
|
|
||||||
String decimalCoords;
|
|
||||||
|
|
||||||
//If image has no EXIF data and user has enabled GPS setting, get user's location
|
|
||||||
//TODO: Always return null as a temporary fix for #1599
|
|
||||||
if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
//If image has EXIF data, extract image coords
|
|
||||||
imageCoordsExists = true;
|
|
||||||
Timber.d("EXIF data has location info");
|
|
||||||
|
|
||||||
latitude = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE);
|
|
||||||
latitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE_REF);
|
|
||||||
longitude = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE);
|
|
||||||
longitudeRef = exif.getAttribute(ExifInterface.TAG_GPS_LONGITUDE_REF);
|
|
||||||
|
|
||||||
if (latitude!=null && latitudeRef!=null && longitude!=null && longitudeRef!=null) {
|
|
||||||
Timber.d("Latitude: %s %s", latitude, latitudeRef);
|
Timber.d("Latitude: %s %s", latitude, latitudeRef);
|
||||||
Timber.d("Longitude: %s %s", longitude, longitudeRef);
|
Timber.d("Longitude: %s %s", longitude, longitudeRef);
|
||||||
|
|
||||||
|
|
@ -83,7 +102,6 @@ public class GPSExtractor {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public double getDecLatitude() {
|
public double getDecLatitude() {
|
||||||
return decLatitude;
|
return decLatitude;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.view.Display;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Ilgaz Er on 8/7/2018.
|
||||||
|
*/
|
||||||
|
public class HeightLimitedRecyclerView extends RecyclerView {
|
||||||
|
|
||||||
|
int height;
|
||||||
|
|
||||||
|
|
||||||
|
public HeightLimitedRecyclerView(Context context) {
|
||||||
|
super(context);
|
||||||
|
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||||
|
((Activity) getContext()).getWindowManager()
|
||||||
|
.getDefaultDisplay()
|
||||||
|
.getMetrics(displayMetrics);
|
||||||
|
height=displayMetrics.heightPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||||
|
((Activity) getContext()).getWindowManager()
|
||||||
|
.getDefaultDisplay()
|
||||||
|
.getMetrics(displayMetrics);
|
||||||
|
height=displayMetrics.heightPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HeightLimitedRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
DisplayMetrics displayMetrics = new DisplayMetrics();
|
||||||
|
((Activity) getContext()).getWindowManager()
|
||||||
|
.getDefaultDisplay()
|
||||||
|
.getMetrics(displayMetrics);
|
||||||
|
height=displayMetrics.heightPixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||||
|
heightSpec = MeasureSpec.makeMeasureSpec((int) (height*0.3), MeasureSpec.AT_MOST);
|
||||||
|
super.onMeasure(widthSpec, heightSpec);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package fr.free.nrw.commons.upload;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
class Language {
|
class Language {
|
||||||
|
|
||||||
private Locale locale;
|
private Locale locale;
|
||||||
private boolean isSet = false;
|
private boolean isSet = false;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,493 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.Manifest.permission;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.ProgressDialog;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.database.DataSetObserver;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import android.support.v4.app.ActivityCompat;
|
|
||||||
import android.support.v4.app.FragmentManager;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.inputmethod.InputMethodManager;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.karumi.dexter.Dexter;
|
|
||||||
import com.karumi.dexter.DexterBuilder;
|
|
||||||
import com.karumi.dexter.listener.PermissionDeniedResponse;
|
|
||||||
import com.karumi.dexter.listener.PermissionGrantedResponse;
|
|
||||||
import com.karumi.dexter.listener.single.BasePermissionListener;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.Media;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.category.CategorizationFragment;
|
|
||||||
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
|
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
|
||||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
|
||||||
import fr.free.nrw.commons.modifications.CategoryModifier;
|
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
|
||||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
import fr.free.nrw.commons.utils.ContributionUtils;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil;
|
|
||||||
import fr.free.nrw.commons.utils.DialogUtil.Callback;
|
|
||||||
import fr.free.nrw.commons.utils.ExternalStorageUtils;
|
|
||||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it.
|
|
||||||
|
|
||||||
public class MultipleShareActivity extends AuthenticatedActivity
|
|
||||||
implements MediaDetailPagerFragment.MediaDetailProvider,
|
|
||||||
AdapterView.OnItemClickListener,
|
|
||||||
FragmentManager.OnBackStackChangedListener,
|
|
||||||
MultipleUploadListFragment.OnMultipleUploadInitiatedHandler,
|
|
||||||
OnCategoriesSaveHandler,
|
|
||||||
ActivityCompat.OnRequestPermissionsResultCallback{
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
MediaWikiApi mwApi;
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
@Inject
|
|
||||||
UploadController uploadController;
|
|
||||||
@Inject
|
|
||||||
ModifierSequenceDao modifierSequenceDao;
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
SharedPreferences prefs;
|
|
||||||
|
|
||||||
private ArrayList<Contribution> photosList = null;
|
|
||||||
|
|
||||||
private MultipleUploadListFragment uploadsList;
|
|
||||||
private MediaDetailPagerFragment mediaDetails;
|
|
||||||
private CategorizationFragment categorizationFragment;
|
|
||||||
|
|
||||||
private boolean locationPermitted = false;
|
|
||||||
private boolean isMultipleUploadsPrepared = false;
|
|
||||||
private boolean isMultipleUploadsFinalised = false; // Checks is user clicked to upload button or regret before this phase
|
|
||||||
private final String TAG="#MultipleShareActivity#";
|
|
||||||
private AlertDialog storagePermissionInfoDialog;
|
|
||||||
private DexterBuilder dexterStoragePermissionBuilder;
|
|
||||||
|
|
||||||
private PermissionDeniedResponse permissionDeniedResponse;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Media getMediaAtPosition(int i) {
|
|
||||||
return photosList.get(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getTotalMediaCount() {
|
|
||||||
if (photosList == null) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
return photosList.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void notifyDatasetChanged() {
|
|
||||||
if (uploadsList != null) {
|
|
||||||
uploadsList.notifyDatasetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void registerDataSetObserver(DataSetObserver observer) {
|
|
||||||
// fixme implement me if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void unregisterDataSetObserver(DataSetObserver observer) {
|
|
||||||
// fixme implement me if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onItemClick(AdapterView<?> adapterView, View view, int index, long item) {
|
|
||||||
showDetail(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void OnMultipleUploadInitiated() {
|
|
||||||
// No need to request external permission here, because if user can reach this point, then she permission granted
|
|
||||||
Timber.d("OnMultipleUploadInitiated");
|
|
||||||
multipleUploadBegins();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void multipleUploadBegins() {
|
|
||||||
|
|
||||||
Timber.d("Multiple upload begins");
|
|
||||||
final ProgressDialog dialog = new ProgressDialog(this);
|
|
||||||
dialog.setIndeterminate(false);
|
|
||||||
dialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL);
|
|
||||||
dialog.setMax(photosList.size());
|
|
||||||
dialog.setTitle(getResources().getQuantityString(R.plurals.starting_multiple_uploads, photosList.size(), photosList.size()));
|
|
||||||
dialog.show();
|
|
||||||
|
|
||||||
for (int i = 0; i < photosList.size(); i++) {
|
|
||||||
Contribution up = photosList.get(i);
|
|
||||||
final int uploadCount = i + 1; // Goddamn Java
|
|
||||||
|
|
||||||
uploadController.startUpload(up, contribution -> {
|
|
||||||
dialog.setProgress(uploadCount);
|
|
||||||
if (uploadCount == photosList.size()) {
|
|
||||||
dialog.dismiss();
|
|
||||||
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
|
|
||||||
startingToast.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadsList.setImageOnlyMode(true);
|
|
||||||
|
|
||||||
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
|
|
||||||
if (categorizationFragment == null) {
|
|
||||||
categorizationFragment = new CategorizationFragment();
|
|
||||||
}
|
|
||||||
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
|
|
||||||
View target = getCurrentFocus();
|
|
||||||
if (target != null) {
|
|
||||||
InputMethodManager imm = (InputMethodManager) target.getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
||||||
if (imm != null)
|
|
||||||
imm.hideSoftInputFromWindow(target.getWindowToken(), 0);
|
|
||||||
}
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.add(R.id.uploadsFragmentContainer, categorizationFragment, "categorization")
|
|
||||||
.commitAllowingStateLoss();
|
|
||||||
isMultipleUploadsFinalised = true;
|
|
||||||
//See http://stackoverflow.com/questions/7469082/getting-exception-illegalstateexception-can-not-perform-this-action-after-onsa
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCategoriesSave(List<String> categories) {
|
|
||||||
if (categories.size() > 0) {
|
|
||||||
for (Contribution contribution : photosList) {
|
|
||||||
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
|
|
||||||
|
|
||||||
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
|
||||||
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
|
||||||
|
|
||||||
modifierSequenceDao.save(categoriesSequence);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// FIXME: Make sure that the content provider is up
|
|
||||||
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
|
||||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home:
|
|
||||||
if (mediaDetails.isVisible()) {
|
|
||||||
getSupportFragmentManager().popBackStack();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setContentView(R.layout.activity_multiple_uploads);
|
|
||||||
ButterKnife.bind(this);
|
|
||||||
initDrawer();
|
|
||||||
initPermissionsRationaleDialog();
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
photosList = savedInstanceState.getParcelableArrayList("uploadsList");
|
|
||||||
}
|
|
||||||
|
|
||||||
getSupportFragmentManager().addOnBackStackChangedListener(this);
|
|
||||||
requestAuthToken();
|
|
||||||
|
|
||||||
//TODO: 15/10/17 should location permission be explicitly requested if not provided?
|
|
||||||
//check if location permission is enabled
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && ContextCompat.checkSelfPermission(this,Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
{
|
|
||||||
locationPermitted = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* We have agreed to show a dialog showing why we need a particular permission.
|
|
||||||
* This method is used to initialise the dialog which is going to show the permission's rationale.
|
|
||||||
* The dialog is initialised along with a callback for positive and negative user actions.
|
|
||||||
*/
|
|
||||||
private void initPermissionsRationaleDialog() {
|
|
||||||
if (storagePermissionInfoDialog == null) {
|
|
||||||
storagePermissionInfoDialog = DialogUtil
|
|
||||||
.getAlertDialogWithPositiveAndNegativeCallbacks(
|
|
||||||
MultipleShareActivity.this,
|
|
||||||
getString(R.string.storage_permission), getString(
|
|
||||||
R.string.write_storage_permission_rationale_for_image_share),
|
|
||||||
R.drawable.ic_launcher, new Callback() {
|
|
||||||
@Override
|
|
||||||
public void onPositiveButtonClicked() {
|
|
||||||
//If the user is willing to give us the permission
|
|
||||||
//But had somehow previously choose never ask again, we take him to app settings to manually enable permission
|
|
||||||
if (null== permissionDeniedResponse){
|
|
||||||
//Dexter returned null, lets see if this ever happens
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
else if (permissionDeniedResponse.isPermanentlyDenied()) {
|
|
||||||
PermissionUtils.askUserToManuallyEnablePermissionFromSettings(MultipleShareActivity.this);
|
|
||||||
} else {
|
|
||||||
//or if we still have chance to show runtime permission dialog, we show him that.
|
|
||||||
askDexterToHandleExternalStoragePermission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNegativeButtonClicked() {
|
|
||||||
//This was the behaviour as of now, I was planning to maybe snack him with some message
|
|
||||||
//and then call finish after some time, or may be it could be associated with some action on the snack
|
|
||||||
//If the user does not want us to give the permission, even after showing rationale dialog, lets not trouble him anymore
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
getSupportFragmentManager().removeOnBackStackChangedListener(this);
|
|
||||||
uploadController.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showDetail(int i) {
|
|
||||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
|
||||||
mediaDetails = new MediaDetailPagerFragment(true, false);
|
|
||||||
getSupportFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.replace(R.id.uploadsFragmentContainer, mediaDetails)
|
|
||||||
.addToBackStack(null)
|
|
||||||
.commit();
|
|
||||||
getSupportFragmentManager().executePendingTransactions();
|
|
||||||
}
|
|
||||||
mediaDetails.showImage(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
/* This will be true if permission request is granted before we request. Otherwise we will
|
|
||||||
* explicitly call operations under this method again.
|
|
||||||
*/
|
|
||||||
if (isMultipleUploadsPrepared) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
Timber.d("onSaveInstanceState multiple uploads is prepared, permission granted");
|
|
||||||
outState.putParcelableArrayList("uploadsList", photosList);
|
|
||||||
} else {
|
|
||||||
Timber.d("onSaveInstanceState multiple uploads is not prepared, permission not granted");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
|
||||||
// Multiple uploads prepared boolean is used to decide when to call multipleUploadsBegin()
|
|
||||||
isMultipleUploadsFinalised = false;
|
|
||||||
isMultipleUploadsPrepared = false;
|
|
||||||
mwApi.setAuthCookie(authCookie);
|
|
||||||
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
|
|
||||||
//If permission is not there, handle the negative cases
|
|
||||||
askDexterToHandleExternalStoragePermission();
|
|
||||||
isMultipleUploadsPrepared = false;
|
|
||||||
return; // Postpone operation to do after gettion permission
|
|
||||||
} else {
|
|
||||||
isMultipleUploadsPrepared = true;
|
|
||||||
prepareMultipleUploadList();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method initialised the Dexter's permission builder (if not already initialised). Also makes sure that the builder is initialised
|
|
||||||
* only once, otherwise we would'nt know on which instance of it, the user is working on. And after the builder is initialised, it checks for the required
|
|
||||||
* permission and then handles the permission status, thanks to Dexter's appropriate callbacks.
|
|
||||||
*/
|
|
||||||
private void askDexterToHandleExternalStoragePermission() {
|
|
||||||
Timber.d(TAG, "External storage permission is being requested");
|
|
||||||
if (null == dexterStoragePermissionBuilder) {
|
|
||||||
dexterStoragePermissionBuilder = Dexter.withActivity(this)
|
|
||||||
.withPermission(permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
.withListener(new BasePermissionListener() {
|
|
||||||
@Override
|
|
||||||
public void onPermissionGranted(PermissionGrantedResponse response) {
|
|
||||||
Timber.d(TAG,"User has granted us the permission for writing the external storage");
|
|
||||||
//If permission is granted, well and good
|
|
||||||
prepareMultipleUploadList();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPermissionDenied(PermissionDeniedResponse response) {
|
|
||||||
Timber.d(TAG,"User has granted us the permission for writing the external storage");
|
|
||||||
//If permission is not granted in whatsoever scenario, we show him a dialog stating why we need the permission
|
|
||||||
permissionDeniedResponse=response;
|
|
||||||
if (null != storagePermissionInfoDialog && !storagePermissionInfoDialog
|
|
||||||
.isShowing()) {
|
|
||||||
storagePermissionInfoDialog.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
dexterStoragePermissionBuilder.check();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data);
|
|
||||||
if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
|
|
||||||
//OnActivity result, no matter what the result is, our function can handle that.
|
|
||||||
askDexterToHandleExternalStoragePermission();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prepares a list from files will be uploaded. Saves these files temporarily to external
|
|
||||||
* storage. Adds them to uploads list
|
|
||||||
*/
|
|
||||||
private void prepareMultipleUploadList() {
|
|
||||||
Intent intent = getIntent();
|
|
||||||
|
|
||||||
if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
|
|
||||||
if (photosList == null) {
|
|
||||||
photosList = new ArrayList<>();
|
|
||||||
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
|
||||||
for (int i = 0; i < urisList.size(); i++) {
|
|
||||||
Contribution up = new Contribution();
|
|
||||||
Uri uri = urisList.get(i);
|
|
||||||
// Use temporarily saved file Uri instead
|
|
||||||
uri = ContributionUtils.saveFileBeingUploadedTemporarily(this, uri);
|
|
||||||
up.setLocalUri(uri);
|
|
||||||
up.setTag("mimeType", intent.getType());
|
|
||||||
up.setTag("sequence", i);
|
|
||||||
up.setSource(Contribution.SOURCE_EXTERNAL);
|
|
||||||
up.setMultiple(true);
|
|
||||||
String imageGpsCoordinates = extractImageGpsData(uri);
|
|
||||||
if (imageGpsCoordinates != null) {
|
|
||||||
Timber.d("GPS data for image found!");
|
|
||||||
up.setDecimalCoords(imageGpsCoordinates);
|
|
||||||
}
|
|
||||||
photosList.add(up);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadsList = (MultipleUploadListFragment) getSupportFragmentManager().findFragmentByTag("uploadsList");
|
|
||||||
if (uploadsList == null) {
|
|
||||||
uploadsList = new MultipleUploadListFragment();
|
|
||||||
getSupportFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.add(R.id.uploadsFragmentContainer, uploadsList, "uploadsList")
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
setTitle(getResources().getQuantityString(R.plurals.multiple_uploads_title, photosList.size(), photosList.size()));
|
|
||||||
uploadController.prepareService();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthFailure() {
|
|
||||||
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
|
|
||||||
failureToast.show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onBackStackChanged() {
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(mediaDetails != null && mediaDetails.isVisible());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Will attempt to extract the gps coordinates using exif data or by using the current
|
|
||||||
* location if available for the image who's imageUri has been provided.
|
|
||||||
* @param imageUri The uri of the image who's GPS coordinates data we wish to extract
|
|
||||||
* @return GPS coordinates as a String as is returned by {@link GPSExtractor}
|
|
||||||
*/
|
|
||||||
@Nullable
|
|
||||||
private String extractImageGpsData(Uri imageUri) {
|
|
||||||
Timber.d("Entering extractImagesGpsData");
|
|
||||||
|
|
||||||
if (imageUri == null) {
|
|
||||||
//now why would you do that???
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
GPSExtractor gpsExtractor = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
||||||
ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r");
|
|
||||||
if (fd != null) {
|
|
||||||
gpsExtractor = new GPSExtractor(fd.getFileDescriptor());
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
String filePath = FileUtils.getPath(this,imageUri);
|
|
||||||
if (filePath != null) {
|
|
||||||
gpsExtractor = new GPSExtractor(filePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gpsExtractor != null) {
|
|
||||||
//get image coordinates from exif data or user location
|
|
||||||
return gpsExtractor.getCoords();
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (FileNotFoundException fnfe) {
|
|
||||||
Timber.w(fnfe);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If on back pressed before sharing
|
|
||||||
@Override
|
|
||||||
public void onBackPressed() {
|
|
||||||
super.onBackPressed();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
// Remove saved files if activity is stopped before upload operation, ie user changed mind
|
|
||||||
if (!isMultipleUploadsFinalised) {
|
|
||||||
if (photosList != null) {
|
|
||||||
for (Contribution contribution : photosList) {
|
|
||||||
Timber.d("User changed mind, didn't click to upload button, deleted file: "+contribution.getLocalUri());
|
|
||||||
ContributionUtils.removeTemporaryFile(contribution.getLocalUri());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onStop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
|
||||||
import android.support.v4.app.Fragment;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.BaseAdapter;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.GridView;
|
|
||||||
import android.widget.RelativeLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
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 {
|
|
||||||
|
|
||||||
public interface OnMultipleUploadInitiatedHandler {
|
|
||||||
void OnMultipleUploadInitiated();
|
|
||||||
}
|
|
||||||
|
|
||||||
@BindView(R.id.multipleShareBackground)
|
|
||||||
GridView photosGrid;
|
|
||||||
|
|
||||||
@BindView(R.id.multipleBaseTitle)
|
|
||||||
EditText baseTitle;
|
|
||||||
|
|
||||||
private PhotoDisplayAdapter photosAdapter;
|
|
||||||
private TitleTextWatcher textWatcher = new TitleTextWatcher();
|
|
||||||
|
|
||||||
private Point photoSize;
|
|
||||||
private MediaDetailPagerFragment.MediaDetailProvider detailProvider;
|
|
||||||
private OnMultipleUploadInitiatedHandler multipleUploadInitiatedHandler;
|
|
||||||
|
|
||||||
private boolean imageOnlyMode;
|
|
||||||
|
|
||||||
private static class UploadHolderView {
|
|
||||||
private Uri imageUri;
|
|
||||||
private SimpleDraweeView image;
|
|
||||||
private TextView title;
|
|
||||||
private RelativeLayout overlay;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttach(Context context) {
|
|
||||||
AndroidSupportInjection.inject(this);
|
|
||||||
super.onAttach(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class PhotoDisplayAdapter extends BaseAdapter {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return detailProvider.getTotalMediaCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getItem(int i) {
|
|
||||||
return detailProvider.getMediaAtPosition(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int i) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int i, View view, ViewGroup viewGroup) {
|
|
||||||
UploadHolderView holder;
|
|
||||||
|
|
||||||
if (view == null) {
|
|
||||||
view = LayoutInflater.from(getContext()).inflate(R.layout.layout_upload_item, viewGroup, false);
|
|
||||||
holder = new UploadHolderView();
|
|
||||||
holder.image = view.findViewById(R.id.uploadImage);
|
|
||||||
holder.title = view.findViewById(R.id.uploadTitle);
|
|
||||||
holder.overlay = view.findViewById(R.id.uploadOverlay);
|
|
||||||
|
|
||||||
holder.image.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, photoSize.y));
|
|
||||||
holder.image.setHierarchy(GenericDraweeHierarchyBuilder
|
|
||||||
.newInstance(getResources())
|
|
||||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_image_black_24dp, getContext().getTheme()))
|
|
||||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_error_outline_black_24dp, getContext().getTheme()))
|
|
||||||
.build());
|
|
||||||
view.setTag(holder);
|
|
||||||
} else {
|
|
||||||
holder = (UploadHolderView) view.getTag();
|
|
||||||
}
|
|
||||||
|
|
||||||
Contribution up = (Contribution) this.getItem(i);
|
|
||||||
|
|
||||||
if (holder.imageUri == null || !holder.imageUri.equals(up.getLocalUri())) {
|
|
||||||
holder.image.setImageURI(up.getLocalUri().toString());
|
|
||||||
holder.imageUri = up.getLocalUri();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imageOnlyMode) {
|
|
||||||
holder.overlay.setVisibility(View.VISIBLE);
|
|
||||||
holder.title.setText(up.getFilename());
|
|
||||||
} else {
|
|
||||||
holder.overlay.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
|
|
||||||
View target = getActivity().getCurrentFocus();
|
|
||||||
ViewUtil.hideKeyboard(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Wrong result type
|
|
||||||
private Point calculatePicDimension(int count) {
|
|
||||||
DisplayMetrics screenMetrics = getResources().getDisplayMetrics();
|
|
||||||
int screenWidth = screenMetrics.widthPixels;
|
|
||||||
int screenHeight = screenMetrics.heightPixels;
|
|
||||||
|
|
||||||
int picWidth = Math.min((int) Math.sqrt(screenWidth * screenHeight / count), screenWidth);
|
|
||||||
picWidth = Math.min((int) (192 * screenMetrics.density), Math.max((int) (120 * screenMetrics.density), picWidth / 48 * 48));
|
|
||||||
int picHeight = Math.min(picWidth, (int) (192 * screenMetrics.density)); // Max Height is same as Contributions list
|
|
||||||
|
|
||||||
return new Point(picWidth, picHeight);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void notifyDatasetChanged() {
|
|
||||||
if (photosAdapter != null) {
|
|
||||||
photosAdapter.notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setImageOnlyMode(boolean mode) {
|
|
||||||
imageOnlyMode = mode;
|
|
||||||
if (imageOnlyMode) {
|
|
||||||
baseTitle.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
baseTitle.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
photosAdapter.notifyDataSetChanged();
|
|
||||||
photosGrid.setEnabled(!mode);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
|
||||||
View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false);
|
|
||||||
ButterKnife.bind(this,view);
|
|
||||||
photosAdapter = new PhotoDisplayAdapter();
|
|
||||||
photosGrid.setAdapter(photosAdapter);
|
|
||||||
photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
|
|
||||||
photoSize = calculatePicDimension(detailProvider.getTotalMediaCount());
|
|
||||||
photosGrid.setColumnWidth(photoSize.x);
|
|
||||||
|
|
||||||
baseTitle.addTextChangedListener(textWatcher);
|
|
||||||
|
|
||||||
baseTitle.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
if (!hasFocus) {
|
|
||||||
ViewUtil.hideKeyboard(v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
baseTitle.removeTextChangedListener(textWatcher);
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
super.onCreateOptionsMenu(menu, inflater);
|
|
||||||
menu.clear();
|
|
||||||
inflater.inflate(R.menu.fragment_multiple_upload_list, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.menu_upload_multiple:
|
|
||||||
if (baseTitle.getText().toString().trim().isEmpty()) {
|
|
||||||
Toast.makeText(getContext(), R.string.add_set_name_toast, Toast.LENGTH_LONG).show();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
multipleUploadInitiatedHandler.OnMultipleUploadInitiated();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
detailProvider = (MediaDetailPagerFragment.MediaDetailProvider) getActivity();
|
|
||||||
multipleUploadInitiatedHandler = (OnMultipleUploadInitiatedHandler) getActivity();
|
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TitleTextWatcher implements TextWatcher {
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence charSequence, int i1, int i2, int i3) {
|
|
||||||
for (int i = 0; i < detailProvider.getTotalMediaCount(); i++) {
|
|
||||||
Contribution up = (Contribution) detailProvider.getMediaAtPosition(i);
|
|
||||||
Boolean isDirty = (Boolean) up.getTag("isDirty");
|
|
||||||
if (isDirty == null || !isDirty) {
|
|
||||||
if (!TextUtils.isEmpty(charSequence)) {
|
|
||||||
up.setFilename(charSequence.toString() + " - " + ((Integer) up.getTag("sequence") + 1));
|
|
||||||
} else {
|
|
||||||
up.setFilename("");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
detailProvider.notifyDatasetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable editable) {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,674 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.Manifest;
|
|
||||||
import android.animation.Animator;
|
|
||||||
import android.animation.AnimatorListenerAdapter;
|
|
||||||
import android.animation.AnimatorSet;
|
|
||||||
import android.animation.ObjectAnimator;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.Point;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.annotation.RequiresApi;
|
|
||||||
import android.support.design.widget.FloatingActionButton;
|
|
||||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
|
||||||
import android.support.v4.app.ActivityCompat;
|
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.animation.DecelerateInterpolator;
|
|
||||||
import android.widget.FrameLayout;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder;
|
|
||||||
import com.facebook.drawee.view.SimpleDraweeView;
|
|
||||||
import com.github.chrisbanes.photoview.PhotoView;
|
|
||||||
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.lang.ref.WeakReference;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
import fr.free.nrw.commons.BuildConfig;
|
|
||||||
import fr.free.nrw.commons.R;
|
|
||||||
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
|
||||||
import fr.free.nrw.commons.auth.LoginActivity;
|
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
|
||||||
import fr.free.nrw.commons.caching.CacheController;
|
|
||||||
import fr.free.nrw.commons.category.CategorizationFragment;
|
|
||||||
import fr.free.nrw.commons.category.OnCategoriesSaveHandler;
|
|
||||||
import fr.free.nrw.commons.contributions.Contribution;
|
|
||||||
import fr.free.nrw.commons.modifications.CategoryModifier;
|
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
|
||||||
import fr.free.nrw.commons.modifications.ModifierSequenceDao;
|
|
||||||
import fr.free.nrw.commons.modifications.TemplateRemoveModifier;
|
|
||||||
import fr.free.nrw.commons.mwapi.CategoryApi;
|
|
||||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
|
||||||
import fr.free.nrw.commons.utils.ContributionUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ExternalStorageUtils;
|
|
||||||
import fr.free.nrw.commons.utils.ViewUtil;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED;
|
|
||||||
import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE;
|
|
||||||
import static fr.free.nrw.commons.upload.FileUtils.getSHA1;
|
|
||||||
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity for the title/desc screen after image is selected. Also starts processing image
|
|
||||||
* GPS coordinates or user location (if enabled in Settings) for category suggestions.
|
|
||||||
*/
|
|
||||||
public class ShareActivity
|
|
||||||
extends AuthenticatedActivity
|
|
||||||
implements SingleUploadFragment.OnUploadActionInitiated,
|
|
||||||
OnCategoriesSaveHandler,
|
|
||||||
ActivityCompat.OnRequestPermissionsResultCallback {
|
|
||||||
|
|
||||||
private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4;
|
|
||||||
//Had to make them class variables, to extract out the click listeners, also I see no harm in this
|
|
||||||
final Rect startBounds = new Rect();
|
|
||||||
final Rect finalBounds = new Rect();
|
|
||||||
final Point globalOffset = new Point();
|
|
||||||
@Inject
|
|
||||||
MediaWikiApi mwApi;
|
|
||||||
@Inject
|
|
||||||
CacheController cacheController;
|
|
||||||
@Inject
|
|
||||||
SessionManager sessionManager;
|
|
||||||
@Inject
|
|
||||||
UploadController uploadController;
|
|
||||||
@Inject
|
|
||||||
ModifierSequenceDao modifierSequenceDao;
|
|
||||||
@Inject
|
|
||||||
CategoryApi apiCall;
|
|
||||||
@Inject @Named("application_preferences") SharedPreferences applicationPrefs;
|
|
||||||
@Inject
|
|
||||||
@Named("default_preferences")
|
|
||||||
SharedPreferences prefs;
|
|
||||||
@Inject
|
|
||||||
GpsCategoryModel gpsCategoryModel;
|
|
||||||
|
|
||||||
@BindView(R.id.container)
|
|
||||||
FrameLayout flContainer;
|
|
||||||
@BindView(R.id.backgroundImage)
|
|
||||||
SimpleDraweeView backgroundImageView;
|
|
||||||
@BindView(R.id.media_map)
|
|
||||||
FloatingActionButton mapButton;
|
|
||||||
@BindView(R.id.media_upload_zoom_in)
|
|
||||||
FloatingActionButton zoomInButton;
|
|
||||||
@BindView(R.id.media_upload_zoom_out)
|
|
||||||
FloatingActionButton zoomOutButton;
|
|
||||||
@BindView(R.id.main_fab)
|
|
||||||
FloatingActionButton mainFab;
|
|
||||||
@BindView(R.id.expanded_image)
|
|
||||||
PhotoView expandedImageView;
|
|
||||||
|
|
||||||
private String source;
|
|
||||||
private String mimeType;
|
|
||||||
private CategorizationFragment categorizationFragment;
|
|
||||||
private Uri mediaUri;
|
|
||||||
private Uri contentProviderUri;
|
|
||||||
private Contribution contribution;
|
|
||||||
private GPSExtractor gpsObj;
|
|
||||||
private String decimalCoords;
|
|
||||||
private FileProcessor fileObj;
|
|
||||||
private boolean useNewPermissions = false;
|
|
||||||
private boolean storagePermitted = false;
|
|
||||||
private boolean locationPermitted = false;
|
|
||||||
private String title;
|
|
||||||
private String description;
|
|
||||||
private String wikiDataEntityId;
|
|
||||||
private boolean duplicateCheckPassed = false;
|
|
||||||
private boolean isNearbyUpload = false;
|
|
||||||
private Animator CurrentAnimator;
|
|
||||||
private long ShortAnimationDuration;
|
|
||||||
private boolean isFABOpen = false;
|
|
||||||
private float startScaleFinal;
|
|
||||||
private Bundle savedInstanceState;
|
|
||||||
private boolean isUploadFinalised = false; // Checks is user clicked to upload button or regret before this phase
|
|
||||||
private boolean isZoom = false;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when user taps the submit button.
|
|
||||||
* Requests Storage permission, if needed.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void uploadActionInitiated(String title, String description) {
|
|
||||||
|
|
||||||
this.title = title;
|
|
||||||
this.description = description;
|
|
||||||
|
|
||||||
|
|
||||||
if (sessionManager.getCurrentAccount() != null) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
||||||
// Check for Storage permission that is required for upload.
|
|
||||||
// Do not allow user to proceed without permission, otherwise will crash
|
|
||||||
if (needsToRequestStoragePermission()) {
|
|
||||||
requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
|
|
||||||
REQUEST_PERM_ON_SUBMIT_STORAGE);
|
|
||||||
} else {
|
|
||||||
uploadBegins();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uploadBegins();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else //Send user to login activity
|
|
||||||
{
|
|
||||||
Toast.makeText(this, "You need to login first!", Toast.LENGTH_SHORT).show();
|
|
||||||
Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
|
|
||||||
startActivity(loginIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether storage permissions need to be requested.
|
|
||||||
* Permissions are needed if the file is not owned by this application, (e.g. shared from the Gallery)
|
|
||||||
*
|
|
||||||
* @return true if file is not owned by this application and permission hasn't been granted beforehand
|
|
||||||
*/
|
|
||||||
@RequiresApi(16)
|
|
||||||
private boolean needsToRequestStoragePermission() {
|
|
||||||
return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri)
|
|
||||||
&& (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
|
||||||
!= PackageManager.PERMISSION_GRANTED);
|
|
||||||
//return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called after permission checks are done.
|
|
||||||
* Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController
|
|
||||||
*/
|
|
||||||
|
|
||||||
private void uploadBegins() {
|
|
||||||
fileObj.processFileCoordinates(locationPermitted);
|
|
||||||
|
|
||||||
Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG);
|
|
||||||
startingToast.show();
|
|
||||||
|
|
||||||
if (!fileObj.isCacheFound()) {
|
|
||||||
//Has to be called after apiCall.request()
|
|
||||||
cacheController.cacheCategory();
|
|
||||||
Timber.d("Cache the categories found");
|
|
||||||
}
|
|
||||||
|
|
||||||
uploadController.startUpload(title, contentProviderUri, mediaUri, description, mimeType, source, decimalCoords, wikiDataEntityId, c -> {
|
|
||||||
ShareActivity.this.contribution = c;
|
|
||||||
showPostUpload();
|
|
||||||
});
|
|
||||||
isUploadFinalised = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Starts CategorizationFragment after uploadBegins.
|
|
||||||
*/
|
|
||||||
|
|
||||||
private void showPostUpload() {
|
|
||||||
if (categorizationFragment == null) {
|
|
||||||
categorizationFragment = new CategorizationFragment();
|
|
||||||
}
|
|
||||||
getSupportFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.single_upload_fragment_container, categorizationFragment, "categorization")
|
|
||||||
.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send categories to modifications queue after they are selected
|
|
||||||
*
|
|
||||||
* @param categories categories selected
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onCategoriesSave(List<String> categories) {
|
|
||||||
if (categories.size() > 0) {
|
|
||||||
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
|
|
||||||
|
|
||||||
categoriesSequence.queueModifier(new CategoryModifier(categories.toArray(new String[]{})));
|
|
||||||
categoriesSequence.queueModifier(new TemplateRemoveModifier("Uncategorized"));
|
|
||||||
modifierSequenceDao.save(categoriesSequence);
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Make sure that the content provider is up
|
|
||||||
// This is the wrong place for it, but bleh - better than not having it turned on by default for people who don't go throughl ogin
|
|
||||||
ContentResolver.setSyncAutomatically(sessionManager.getCurrentAccount(), BuildConfig.MODIFICATION_AUTHORITY, true); // Enable sync by default!
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
if (contribution != null) {
|
|
||||||
outState.putParcelable("contribution", contribution);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthCookieAcquired(String authCookie) {
|
|
||||||
mwApi.setAuthCookie(authCookie);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onAuthFailure() {
|
|
||||||
Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG);
|
|
||||||
failureToast.show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
isUploadFinalised = false;
|
|
||||||
setContentView(R.layout.activity_share);
|
|
||||||
ButterKnife.bind(this);
|
|
||||||
initBack();
|
|
||||||
backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder
|
|
||||||
.newInstance(getResources())
|
|
||||||
.setPlaceholderImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_image_black_24dp, getTheme()))
|
|
||||||
.setFailureImage(VectorDrawableCompat.create(getResources(),
|
|
||||||
R.drawable.ic_error_outline_black_24dp, getTheme()))
|
|
||||||
.build());
|
|
||||||
if (!ExternalStorageUtils.isStoragePermissionGranted(this)) {
|
|
||||||
this.savedInstanceState = savedInstanceState;
|
|
||||||
ExternalStorageUtils.requestExternalStoragePermission(this);
|
|
||||||
return; // Postpone operation to do after getting permission
|
|
||||||
} else {
|
|
||||||
receiveImageIntent();
|
|
||||||
createContributionWithReceivedIntent(savedInstanceState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onStop() {
|
|
||||||
// If upload is not finalised with failure or success, but contribution is created,
|
|
||||||
// we have to remove temp file, to prevent using unnecessary memory
|
|
||||||
if (!isUploadFinalised) {
|
|
||||||
if (mediaUri != null) {
|
|
||||||
ContributionUtils.removeTemporaryFile(mediaUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void createContributionWithReceivedIntent(Bundle savedInstanceState) {
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
contribution = savedInstanceState.getParcelable("contribution");
|
|
||||||
}
|
|
||||||
|
|
||||||
requestAuthToken();
|
|
||||||
Timber.d("Uri: %s", mediaUri.toString());
|
|
||||||
Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory());
|
|
||||||
|
|
||||||
SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView");
|
|
||||||
categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization");
|
|
||||||
if (shareView == null && categorizationFragment == null) {
|
|
||||||
shareView = new SingleUploadFragment();
|
|
||||||
getSupportFragmentManager()
|
|
||||||
.beginTransaction()
|
|
||||||
.add(R.id.single_upload_fragment_container, shareView, "shareView")
|
|
||||||
.commitAllowingStateLoss();
|
|
||||||
}
|
|
||||||
uploadController.prepareService();
|
|
||||||
|
|
||||||
ContentResolver contentResolver = this.getContentResolver();
|
|
||||||
fileObj = new FileProcessor(mediaUri, contentResolver, this);
|
|
||||||
checkIfFileExists();
|
|
||||||
gpsObj = fileObj.processFileCoordinates(locationPermitted);
|
|
||||||
decimalCoords = fileObj.getDecimalCoords();
|
|
||||||
if (sessionManager.getCurrentAccount() == null) {
|
|
||||||
Toast.makeText(this, getString(R.string.login_alert_message), Toast.LENGTH_SHORT).show();
|
|
||||||
applicationPrefs.edit().putBoolean("login_skipped", false).apply();
|
|
||||||
Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class);
|
|
||||||
startActivity(loginIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receive intent from ContributionController.java when user selects picture to upload
|
|
||||||
*/
|
|
||||||
private void receiveImageIntent() {
|
|
||||||
Intent intent = getIntent();
|
|
||||||
|
|
||||||
if (Intent.ACTION_SEND.equals(intent.getAction())) {
|
|
||||||
mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
|
||||||
contentProviderUri = mediaUri;
|
|
||||||
mediaUri = ContributionUtils.saveFileBeingUploadedTemporarily(this, mediaUri);
|
|
||||||
|
|
||||||
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
|
|
||||||
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
|
|
||||||
} else {
|
|
||||||
source = Contribution.SOURCE_EXTERNAL;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isDirectUpload = intent.getBooleanExtra("isDirectUpload", false);
|
|
||||||
|
|
||||||
if (isDirectUpload) {
|
|
||||||
Timber.d("This was initiated by a direct upload from Nearby");
|
|
||||||
isNearbyUpload = true;
|
|
||||||
wikiDataEntityId = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
|
|
||||||
Timber.d("Received wikiDataEntityId from contribution controller %s", wikiDataEntityId);
|
|
||||||
}
|
|
||||||
mimeType = intent.getType();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaUri != null) {
|
|
||||||
backgroundImageView.setImageURI(mediaUri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Function to display the zoom and map FAB
|
|
||||||
*/
|
|
||||||
private void showFABMenu() {
|
|
||||||
isFABOpen = true;
|
|
||||||
|
|
||||||
if (gpsObj != null && gpsObj.imageCoordsExists)
|
|
||||||
mapButton.setVisibility(View.VISIBLE);
|
|
||||||
zoomInButton.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
mainFab.animate().rotationBy(180);
|
|
||||||
mapButton.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);
|
|
||||||
mapButton.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) {
|
|
||||||
mapButton.setVisibility(View.GONE);
|
|
||||||
zoomInButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animator) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationRepeat(Animator animator) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if upload was initiated via Nearby
|
|
||||||
*
|
|
||||||
* @return true if upload was initiated via Nearby
|
|
||||||
*/
|
|
||||||
protected boolean isNearbyUpload() {
|
|
||||||
return isNearbyUpload;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles submit button permission request (for storage)
|
|
||||||
*
|
|
||||||
* @param requestCode type of request
|
|
||||||
* @param permissions permissions requested
|
|
||||||
* @param grantResults grant results
|
|
||||||
*/
|
|
||||||
@Override
|
|
||||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
|
||||||
if (requestCode == 1 && grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
Timber.d("onRequestPermissionsResult external storage permission granted");
|
|
||||||
// You can receive image intent and save image to a temp file only if ext storage permission is granted
|
|
||||||
receiveImageIntent();
|
|
||||||
createContributionWithReceivedIntent(savedInstanceState);
|
|
||||||
|
|
||||||
if (requestCode == REQUEST_PERM_ON_SUBMIT_STORAGE) {
|
|
||||||
checkIfFileExists();
|
|
||||||
//Uploading only begins if storage permission granted from arrow icon
|
|
||||||
uploadBegins();
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if file user wants to upload already exists on Commons
|
|
||||||
*/
|
|
||||||
private void checkIfFileExists() {
|
|
||||||
if (!useNewPermissions || storagePermitted) {
|
|
||||||
if (!duplicateCheckPassed) {
|
|
||||||
//Test SHA1 of image to see if it matches SHA1 of a file on Commons
|
|
||||||
try {
|
|
||||||
InputStream inputStream = getContentResolver().openInputStream(mediaUri);
|
|
||||||
String fileSHA1 = getSHA1(inputStream);
|
|
||||||
Timber.d("Input stream created from %s", mediaUri.toString());
|
|
||||||
Timber.d("File SHA1 is: %s", fileSHA1);
|
|
||||||
|
|
||||||
ExistingFileAsync fileAsyncTask =
|
|
||||||
new ExistingFileAsync(new WeakReference<Activity>(this), fileSHA1, new WeakReference<Context>(this), result -> {
|
|
||||||
Timber.d("%s duplicate check: %s", mediaUri.toString(), result);
|
|
||||||
duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE);
|
|
||||||
if (duplicateCheckPassed) {
|
|
||||||
//image is not a duplicate, so now check if its a unwanted picture or not
|
|
||||||
fileObj.detectUnwantedPictures();
|
|
||||||
}
|
|
||||||
}, mwApi);
|
|
||||||
fileAsyncTask.execute();
|
|
||||||
} catch (IOException e) {
|
|
||||||
Timber.e(e, "IO Exception: ");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s",
|
|
||||||
useNewPermissions, storagePermitted, locationPermitted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
uploadController.cleanup();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case android.R.id.home:
|
|
||||||
if (categorizationFragment != null && categorizationFragment.isVisible()) {
|
|
||||||
categorizationFragment.showBackButtonDialog();
|
|
||||||
} else {
|
|
||||||
onBackPressed();
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped
|
|
||||||
*/
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
isZoom = true;
|
|
||||||
ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit));
|
|
||||||
closeFABMenu();
|
|
||||||
mainFab.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
InputStream input = null;
|
|
||||||
try {
|
|
||||||
input = this.getContentResolver().openInputStream(imageuri);
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver());
|
|
||||||
Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri);
|
|
||||||
|
|
||||||
// Load the high-resolution "zoomed-in" image.
|
|
||||||
expandedImageView.setImageBitmap(scaledImage);
|
|
||||||
float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset);
|
|
||||||
|
|
||||||
// Hide the thumbnail and show the zoomed-in view. When the animation
|
|
||||||
// begins, it will position the zoomed-in view in the place of the
|
|
||||||
// thumbnail.
|
|
||||||
thumbView.setAlpha(0f);
|
|
||||||
expandedImageView.setVisibility(View.VISIBLE);
|
|
||||||
zoomOutButton.setVisibility(View.VISIBLE);
|
|
||||||
zoomInButton.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
// Set the pivot point for SCALE_X and SCALE_Y transformations
|
|
||||||
// to the top-left corner of the zoomed-in view (the default
|
|
||||||
// is the center of the view).
|
|
||||||
expandedImageView.setPivotX(0f);
|
|
||||||
expandedImageView.setPivotY(0f);
|
|
||||||
|
|
||||||
// Construct and run the parallel animation of the four translation and
|
|
||||||
// scale properties (X, Y, SCALE_X, and SCALE_Y).
|
|
||||||
AnimatorSet set = new AnimatorSet();
|
|
||||||
set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left))
|
|
||||||
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top))
|
|
||||||
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f))
|
|
||||||
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f));
|
|
||||||
set.setDuration(ShortAnimationDuration);
|
|
||||||
set.setInterpolator(new DecelerateInterpolator());
|
|
||||||
set.addListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
CurrentAnimator = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animation) {
|
|
||||||
CurrentAnimator = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
set.start();
|
|
||||||
CurrentAnimator = set;
|
|
||||||
|
|
||||||
// Upon clicking the zoomed-in image, it should zoom back down
|
|
||||||
// to the original bounds and show the thumbnail instead of
|
|
||||||
// the expanded image.
|
|
||||||
startScaleFinal = startScale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when user taps the ^ FAB button, expands to show Zoom and Map
|
|
||||||
*/
|
|
||||||
@OnClick(R.id.main_fab)
|
|
||||||
public void onMainFabClicked() {
|
|
||||||
if (!isFABOpen) {
|
|
||||||
showFABMenu();
|
|
||||||
} else {
|
|
||||||
closeFABMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.media_upload_zoom_in)
|
|
||||||
public void onZoomInFabClicked() {
|
|
||||||
try {
|
|
||||||
zoomImageFromThumb(backgroundImageView, mediaUri);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Timber.e(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.media_upload_zoom_out)
|
|
||||||
public void onZoomOutFabClicked() {
|
|
||||||
if (CurrentAnimator != null) {
|
|
||||||
CurrentAnimator.cancel();
|
|
||||||
}
|
|
||||||
isZoom = false;
|
|
||||||
zoomOutButton.setVisibility(View.GONE);
|
|
||||||
mainFab.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
// Animate the four positioning/sizing properties in parallel,
|
|
||||||
// back to their original values.
|
|
||||||
AnimatorSet set = new AnimatorSet();
|
|
||||||
set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left))
|
|
||||||
.with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top))
|
|
||||||
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal))
|
|
||||||
.with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal));
|
|
||||||
|
|
||||||
set.setDuration(ShortAnimationDuration);
|
|
||||||
set.setInterpolator(new DecelerateInterpolator());
|
|
||||||
set.addListener(new AnimatorListenerAdapter() {
|
|
||||||
@Override
|
|
||||||
public void onAnimationEnd(Animator animation) {
|
|
||||||
//background image view is thumbView
|
|
||||||
backgroundImageView.setAlpha(1f);
|
|
||||||
expandedImageView.setVisibility(View.GONE);
|
|
||||||
CurrentAnimator = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAnimationCancel(Animator animation) {
|
|
||||||
//background image view is thumbView
|
|
||||||
backgroundImageView.setAlpha(1f);
|
|
||||||
expandedImageView.setVisibility(View.GONE);
|
|
||||||
CurrentAnimator = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
set.start();
|
|
||||||
CurrentAnimator = set;
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.media_map)
|
|
||||||
public void onFabShowMapsClicked() {
|
|
||||||
if (gpsObj != null && gpsObj.imageCoordsExists) {
|
|
||||||
Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
|
|
||||||
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
|
||||||
mapIntent.setPackage("com.google.android.apps.maps");
|
|
||||||
startActivity(mapIntent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
||||||
switch (keyCode) {
|
|
||||||
case KeyEvent.KEYCODE_BACK:
|
|
||||||
if (isZoom) {
|
|
||||||
onZoomOutFabClicked();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode,event);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
public interface SimilarImageInterface {
|
||||||
|
void showSimilarImageFragment(String originalFilePath, String possibleFilePath);
|
||||||
|
}
|
||||||
|
|
@ -1,389 +0,0 @@
|
||||||
package fr.free.nrw.commons.upload;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.preference.PreferenceManager;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.v4.view.ViewCompat;
|
|
||||||
import android.support.v7.app.AlertDialog;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
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;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.google.gson.Gson;
|
|
||||||
import com.google.gson.reflect.TypeToken;
|
|
||||||
|
|
||||||
import java.lang.reflect.Type;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Locale;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.inject.Named;
|
|
||||||
|
|
||||||
import butterknife.BindView;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import butterknife.OnClick;
|
|
||||||
import butterknife.OnItemSelected;
|
|
||||||
import butterknife.OnTouch;
|
|
||||||
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_UP;
|
|
||||||
|
|
||||||
public class SingleUploadFragment extends CommonsDaggerSupportFragment {
|
|
||||||
|
|
||||||
@BindView(R.id.titleEdit) EditText titleEdit;
|
|
||||||
@BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
|
|
||||||
@BindView(R.id.titleDescButton) Button titleDescButton;
|
|
||||||
@BindView(R.id.share_license_summary) TextView licenseSummaryView;
|
|
||||||
@BindView(R.id.licenseSpinner) Spinner licenseSpinner;
|
|
||||||
|
|
||||||
|
|
||||||
@Inject @Named("default_preferences") SharedPreferences prefs;
|
|
||||||
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
|
||||||
|
|
||||||
private String license;
|
|
||||||
private OnUploadActionInitiated uploadActionInitiatedHandler;
|
|
||||||
private TitleTextWatcher textWatcher = new TitleTextWatcher();
|
|
||||||
private DescriptionsAdapter descriptionsAdapter;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
|
||||||
inflater.inflate(R.menu.activity_share, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
//What happens when the 'submit' icon is tapped
|
|
||||||
case R.id.menu_upload_single:
|
|
||||||
|
|
||||||
if (titleEdit.getText().toString().trim().isEmpty()) {
|
|
||||||
Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String title = titleEdit.getText().toString();
|
|
||||||
String descriptionsInVariousLanguages = getDescriptionsInAppropriateFormat();
|
|
||||||
|
|
||||||
//Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these
|
|
||||||
prefs.edit()
|
|
||||||
.putString("Title", title)
|
|
||||||
.putString("Desc", new Gson().toJson(descriptionsAdapter
|
|
||||||
.getDescriptions()))//Description, now is not just a string, its a list of description objects
|
|
||||||
.apply();
|
|
||||||
|
|
||||||
uploadActionInitiatedHandler
|
|
||||||
.uploadActionInitiated(title, descriptionsInVariousLanguages);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getDescriptionsInAppropriateFormat() {
|
|
||||||
List<Description> descriptions = descriptionsAdapter.getDescriptions();
|
|
||||||
StringBuilder descriptionsInAppropriateFormat = new StringBuilder();
|
|
||||||
for (Description description : descriptions) {
|
|
||||||
String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageId(),
|
|
||||||
description.getDescriptionText());
|
|
||||||
descriptionsInAppropriateFormat.append(individualDescription);
|
|
||||||
}
|
|
||||||
return descriptionsInAppropriateFormat.toString();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Description> getDescriptions() {
|
|
||||||
List<Description> descriptions = descriptionsAdapter.getDescriptions();
|
|
||||||
return descriptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
|
||||||
Bundle savedInstanceState) {
|
|
||||||
View rootView = inflater.inflate(R.layout.fragment_single_upload, container, false);
|
|
||||||
ButterKnife.bind(this, rootView);
|
|
||||||
|
|
||||||
initRecyclerView();
|
|
||||||
|
|
||||||
Intent activityIntent = getActivity().getIntent();
|
|
||||||
if (activityIntent.hasExtra("title")) {
|
|
||||||
titleEdit.setText(activityIntent.getStringExtra("title"));
|
|
||||||
}
|
|
||||||
if (activityIntent.hasExtra("description") && descriptionsAdapter.getDescriptions() != null
|
|
||||||
&& descriptionsAdapter.getDescriptions().size() > 0) {
|
|
||||||
descriptionsAdapter.getDescriptions().get(0)
|
|
||||||
.setDescriptionText(activityIntent.getStringExtra("description"));
|
|
||||||
descriptionsAdapter.notifyItemChanged(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
ArrayList<String> licenseItems = new ArrayList<>();
|
|
||||||
licenseItems.add(getString(R.string.license_name_cc0));
|
|
||||||
licenseItems.add(getString(R.string.license_name_cc_by));
|
|
||||||
licenseItems.add(getString(R.string.license_name_cc_by_sa));
|
|
||||||
licenseItems.add(getString(R.string.license_name_cc_by_four));
|
|
||||||
licenseItems.add(getString(R.string.license_name_cc_by_sa_four));
|
|
||||||
|
|
||||||
license = prefs.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
|
||||||
|
|
||||||
// If this is a direct upload from Nearby, autofill title and desc fields with the Place's values
|
|
||||||
boolean isNearbyUpload = ((ShareActivity) getActivity()).isNearbyUpload();
|
|
||||||
|
|
||||||
if (isNearbyUpload) {
|
|
||||||
String imageTitle = directPrefs.getString("Title", "");
|
|
||||||
String imageDesc = directPrefs.getString("Desc", "");
|
|
||||||
String imageCats = directPrefs.getString("Category", "");
|
|
||||||
Timber.d("Image title: " + imageTitle + ", image desc: " + imageDesc + ", image categories: " + imageCats);
|
|
||||||
titleEdit.setText(imageTitle);
|
|
||||||
if (descriptionsAdapter.getDescriptions() != null
|
|
||||||
&& descriptionsAdapter.getDescriptions().size() > 0) {
|
|
||||||
descriptionsAdapter.getDescriptions().get(0).setDescriptionText(imageDesc);
|
|
||||||
descriptionsAdapter.notifyItemChanged(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if this is the first time we have uploaded
|
|
||||||
if (prefs.getString("Title", "").trim().length() == 0
|
|
||||||
&& prefs.getString("Desc", "").trim().length() == 0) {
|
|
||||||
titleDescButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d(license);
|
|
||||||
|
|
||||||
ArrayAdapter<String> adapter;
|
|
||||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme", false)) {
|
|
||||||
// dark theme
|
|
||||||
adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, licenseItems);
|
|
||||||
} else {
|
|
||||||
// light theme
|
|
||||||
adapter = new ArrayAdapter<>(getActivity(), R.layout.light_simple_spinner_dropdown_item, licenseItems);
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseSpinner.setAdapter(adapter);
|
|
||||||
|
|
||||||
int position = licenseItems.indexOf(getString(Utils.licenseNameFor(license)));
|
|
||||||
|
|
||||||
// Check position is valid
|
|
||||||
if (position < 0) {
|
|
||||||
Timber.d("Invalid position: %d. Using default license", position);
|
|
||||||
position = 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(license)));
|
|
||||||
licenseSpinner.setSelection(position);
|
|
||||||
|
|
||||||
titleEdit.addTextChangedListener(textWatcher);
|
|
||||||
|
|
||||||
titleEdit.setOnFocusChangeListener((v, hasFocus) -> {
|
|
||||||
if (!hasFocus) {
|
|
||||||
ViewUtil.hideKeyboard(v);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
setLicenseSummary(license);
|
|
||||||
|
|
||||||
return rootView;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initRecyclerView() {
|
|
||||||
descriptionsAdapter = new DescriptionsAdapter();
|
|
||||||
descriptionsAdapter.setCallback(this::showInfoAlert);
|
|
||||||
descriptionsAdapter.setLanguages(getLocaleSupportedByDevice());
|
|
||||||
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
|
||||||
rvDescriptions.setAdapter(descriptionsAdapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
private List<Language> getLocaleSupportedByDevice() {
|
|
||||||
List<Language> languages = new ArrayList<>();
|
|
||||||
Locale[] localesArray = Locale.getAvailableLocales();
|
|
||||||
List<Locale> locales = Arrays.asList(localesArray);
|
|
||||||
for (Locale locale : locales) {
|
|
||||||
languages.add(new Language(locale));
|
|
||||||
}
|
|
||||||
return languages;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
titleEdit.removeTextChangedListener(textWatcher);
|
|
||||||
super.onDestroyView();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnItemSelected(R.id.licenseSpinner)
|
|
||||||
void onLicenseSelected(AdapterView<?> parent, View view, int position, long id) {
|
|
||||||
String licenseName = parent.getItemAtPosition(position).toString();
|
|
||||||
|
|
||||||
// Set selected color to white because it should be readable on random images.
|
|
||||||
TextView selectedText = (TextView) licenseSpinner.getChildAt(0);
|
|
||||||
if (selectedText != null) {
|
|
||||||
selectedText.setTextColor(Color.WHITE);
|
|
||||||
selectedText.setBackgroundColor(Color.TRANSPARENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
String license;
|
|
||||||
if (getString(R.string.license_name_cc0).equals(licenseName)) {
|
|
||||||
license = Prefs.Licenses.CC0;
|
|
||||||
} else if (getString(R.string.license_name_cc_by).equals(licenseName)) {
|
|
||||||
license = Prefs.Licenses.CC_BY_3;
|
|
||||||
} else if (getString(R.string.license_name_cc_by_sa).equals(licenseName)) {
|
|
||||||
license = Prefs.Licenses.CC_BY_SA_3;
|
|
||||||
} else if (getString(R.string.license_name_cc_by_four).equals(licenseName)) {
|
|
||||||
license = Prefs.Licenses.CC_BY_4;
|
|
||||||
} else if (getString(R.string.license_name_cc_by_sa_four).equals(licenseName)) {
|
|
||||||
license = Prefs.Licenses.CC_BY_SA_4;
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("Unknown licenseName: " + licenseName);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLicenseSummary(license);
|
|
||||||
prefs.edit()
|
|
||||||
.putString(Prefs.DEFAULT_LICENSE, license)
|
|
||||||
.apply();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@OnClick(R.id.titleDescButton)
|
|
||||||
void setTitleDescButton() {
|
|
||||||
//Retrieve last title and desc entered
|
|
||||||
String title = prefs.getString("Title", "");
|
|
||||||
String descriptionJson = prefs.getString("Desc", "");
|
|
||||||
Timber.d("Title: %s, Desc: %s", title, descriptionJson);
|
|
||||||
|
|
||||||
titleEdit.setText(title);
|
|
||||||
Type typeOfDest = new TypeToken<List<Description>>() {
|
|
||||||
}.getType();
|
|
||||||
|
|
||||||
List<Description> descriptions = new Gson().fromJson(descriptionJson, typeOfDest);
|
|
||||||
descriptionsAdapter.setDescriptions(descriptions);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Copied from https://stackoverflow.com/a/26269435/8065933
|
|
||||||
*/
|
|
||||||
@OnTouch(R.id.titleEdit)
|
|
||||||
boolean titleInfo(View view, MotionEvent motionEvent) {
|
|
||||||
final int value;
|
|
||||||
if (ViewCompat.getLayoutDirection(getView()) == ViewCompat.LAYOUT_DIRECTION_LTR) {
|
|
||||||
value = titleEdit.getRight() - titleEdit.getCompoundDrawables()[2].getBounds().width();
|
|
||||||
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() >= value) {
|
|
||||||
showInfoAlert(R.string.media_detail_title, R.string.title_info);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
value = titleEdit.getLeft() + titleEdit.getCompoundDrawables()[0].getBounds().width();
|
|
||||||
if (motionEvent.getAction() == ACTION_UP && motionEvent.getRawX() <= value) {
|
|
||||||
showInfoAlert(R.string.media_detail_title, R.string.title_info);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
|
||||||
private void setLicenseSummary(String license) {
|
|
||||||
String licenseHyperLink = "<a href='" + licenseUrlFor(license)+"'>"+ getString(Utils.licenseNameFor(license)) + "</a><br>";
|
|
||||||
licenseSummaryView.setMovementMethod(LinkMovementMethod.getInstance());
|
|
||||||
licenseSummaryView.setText(Html.fromHtml(getString(R.string.share_license_summary, licenseHyperLink)));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityCreated(Bundle savedInstanceState) {
|
|
||||||
super.onActivityCreated(savedInstanceState);
|
|
||||||
setHasOptionsMenu(true);
|
|
||||||
uploadActionInitiatedHandler = (OnUploadActionInitiated) getActivity();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
super.onStop();
|
|
||||||
|
|
||||||
// FIXME: Stops the keyboard from being shown 'stale' while moving out of this fragment into the next
|
|
||||||
View target = getActivity().getCurrentFocus();
|
|
||||||
ViewUtil.hideKeyboard(target);
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
private String licenseUrlFor(String license) {
|
|
||||||
switch (license) {
|
|
||||||
case Prefs.Licenses.CC_BY_3:
|
|
||||||
return "https://creativecommons.org/licenses/by/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_4:
|
|
||||||
return "https://creativecommons.org/licenses/by/4.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_3:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/3.0/";
|
|
||||||
case Prefs.Licenses.CC_BY_SA_4:
|
|
||||||
return "https://creativecommons.org/licenses/by-sa/4.0/";
|
|
||||||
case Prefs.Licenses.CC0:
|
|
||||||
return "https://creativecommons.org/publicdomain/zero/1.0/";
|
|
||||||
}
|
|
||||||
throw new RuntimeException("Unrecognized license value: " + license);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface OnUploadActionInitiated {
|
|
||||||
|
|
||||||
void uploadActionInitiated(String title, String description);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TitleTextWatcher implements TextWatcher {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void beforeTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onTextChanged(CharSequence charSequence, int i, int i2, int i3) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void afterTextChanged(Editable editable) {
|
|
||||||
if (getActivity() != null) {
|
|
||||||
getActivity().invalidateOptionsMenu();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void showInfoAlert (int titleStringID, int messageStringID){
|
|
||||||
new AlertDialog.Builder(getContext())
|
|
||||||
.setTitle(titleStringID)
|
|
||||||
.setMessage(messageStringID)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
@OnClick(R.id.ll_add_description)
|
|
||||||
public void onLLAddDescriptionClicked() {
|
|
||||||
descriptionsAdapter.addDescription(new Description());
|
|
||||||
rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +1,83 @@
|
||||||
package fr.free.nrw.commons.upload;
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.os.Build;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
import android.widget.ArrayAdapter;
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
import butterknife.BindView;
|
import butterknife.BindView;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.utils.BiMap;
|
||||||
|
|
||||||
public class SpinnerLanguagesAdapter extends ArrayAdapter {
|
public class SpinnerLanguagesAdapter extends ArrayAdapter {
|
||||||
|
|
||||||
private final int resource;
|
private final int resource;
|
||||||
private final LayoutInflater layoutInflater;
|
private final LayoutInflater layoutInflater;
|
||||||
List<Language> languages;
|
private List<String> languageNamesList;
|
||||||
|
private List<String> languageCodesList;
|
||||||
|
private final BiMap<AdapterView, String> selectedLanguages;
|
||||||
|
public String selectedLangCode="";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public SpinnerLanguagesAdapter(@NonNull Context context,
|
public SpinnerLanguagesAdapter(@NonNull Context context,
|
||||||
int resource) {
|
int resource, BiMap<AdapterView, String> selectedLanguages) {
|
||||||
super(context, resource);
|
super(context, resource);
|
||||||
this.resource = resource;
|
this.resource = resource;
|
||||||
this.layoutInflater = LayoutInflater.from(context);
|
this.layoutInflater = LayoutInflater.from(context);
|
||||||
languages = new ArrayList<>();
|
languageNamesList = new ArrayList<>();
|
||||||
|
languageCodesList = new ArrayList<>();
|
||||||
|
prepareLanguages();
|
||||||
|
this.selectedLanguages = selectedLanguages;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLanguages(List<Language> languages) {
|
private void prepareLanguages() {
|
||||||
this.languages = languages;
|
List<Language> languages = getLocaleSupportedByDevice();
|
||||||
|
|
||||||
|
for(Language language: languages) {
|
||||||
|
if(!languageCodesList.contains(language.getLocale().getLanguage())) {
|
||||||
|
languageNamesList.add(language.getLocale().getDisplayName());
|
||||||
|
languageCodesList.add(language.getLocale().getLanguage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Language> getLocaleSupportedByDevice() {
|
||||||
|
List<Language> languages = new ArrayList<>();
|
||||||
|
Locale[] localesArray = Locale.getAvailableLocales();
|
||||||
|
for (Locale locale : localesArray) {
|
||||||
|
languages.add(new Language(locale));
|
||||||
|
}
|
||||||
|
|
||||||
|
Collections.sort(languages, (language, t1) -> language.getLocale().getDisplayName()
|
||||||
|
.compareTo(t1.getLocale().getDisplayName()));
|
||||||
|
return languages;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled(int position) {
|
||||||
|
return !languageCodesList.get(position).isEmpty()&&
|
||||||
|
(!selectedLanguages.containsKey(languageCodesList.get(position)) ||
|
||||||
|
languageCodesList.get(position).equals(selectedLangCode));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return languages.size();
|
return languageNamesList.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -75,19 +115,40 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(int position, boolean isDropDownView) {
|
public void init(int position, boolean isDropDownView) {
|
||||||
Language language = languages.get(position);
|
|
||||||
if (!isDropDownView) {
|
if (!isDropDownView) {
|
||||||
view.setVisibility(View.GONE);
|
view.setVisibility(View.GONE);
|
||||||
tvLanguage.setText(
|
if(languageCodesList.get(position).length()>2)
|
||||||
language.getLocale().getLanguage());
|
tvLanguage.setText(languageCodesList.get(position).subSequence(0,2));
|
||||||
|
else
|
||||||
|
tvLanguage.setText(languageCodesList.get(position));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
view.setVisibility(View.VISIBLE);
|
view.setVisibility(View.VISIBLE);
|
||||||
|
if (languageCodesList.get(position).isEmpty()) {
|
||||||
|
tvLanguage.setText(languageNamesList.get(position));
|
||||||
|
tvLanguage.setTextColor(Color.GRAY);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
tvLanguage.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
tvLanguage.setText(
|
tvLanguage.setText(
|
||||||
String.format("%s [%s]", language.getLocale().getDisplayName(),
|
String.format("%s [%s]", languageNamesList.get(position), languageCodesList.get(position)));
|
||||||
language.getLocale().getLanguage()));
|
if(selectedLanguages.containsKey(languageCodesList.get(position))&&
|
||||||
|
!languageCodesList.get(position).equals(selectedLangCode))
|
||||||
|
tvLanguage.setTextColor(Color.GRAY);
|
||||||
|
else
|
||||||
|
tvLanguage.setTextColor(Color.BLACK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String getLanguageCode(int position) {
|
||||||
|
return languageCodesList.get(position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int getIndexOfUserDefaultLocale(Context context) {
|
||||||
|
return languageCodesList.indexOf(context.getResources().getConfiguration().locale.getLanguage());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
public interface ThumbnailClickedListener {
|
||||||
|
void thumbnailClicked(UploadModel.UploadItem content);
|
||||||
|
}
|
||||||
37
app/src/main/java/fr/free/nrw/commons/upload/Title.java
Normal file
37
app/src/main/java/fr/free/nrw/commons/upload/Title.java
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import io.reactivex.subjects.BehaviorSubject;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
class Title{
|
||||||
|
|
||||||
|
private String titleText;
|
||||||
|
private boolean set;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return titleText;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTitleText(String titleText) {
|
||||||
|
this.titleText = titleText;
|
||||||
|
|
||||||
|
if (!TextUtils.isEmpty(titleText)) {
|
||||||
|
set = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSet() {
|
||||||
|
return set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSet(boolean set) {
|
||||||
|
this.set = set;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return titleText==null || titleText.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
607
app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
Normal file
607
app/src/main/java/fr/free/nrw/commons/upload/UploadActivity.java
Normal file
|
|
@ -0,0 +1,607 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.animation.LayoutTransition;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.constraint.ConstraintLayout;
|
||||||
|
import android.support.design.widget.TextInputLayout;
|
||||||
|
import android.support.v7.app.AlertDialog;
|
||||||
|
import android.support.v7.widget.CardView;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.Html;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.method.LinkMovementMethod;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
|
import android.widget.Button;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ProgressBar;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
import android.widget.ViewFlipper;
|
||||||
|
|
||||||
|
import com.github.chrisbanes.photoview.PhotoView;
|
||||||
|
import com.jakewharton.rxbinding2.view.RxView;
|
||||||
|
import com.jakewharton.rxbinding2.widget.RxTextView;
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.Utils;
|
||||||
|
import fr.free.nrw.commons.auth.AuthenticatedActivity;
|
||||||
|
import fr.free.nrw.commons.auth.LoginActivity;
|
||||||
|
import fr.free.nrw.commons.category.CategoriesModel;
|
||||||
|
import fr.free.nrw.commons.category.CategoryItem;
|
||||||
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.utils.DialogUtil;
|
||||||
|
import fr.free.nrw.commons.utils.StringUtils;
|
||||||
|
import fr.free.nrw.commons.utils.ViewUtil;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.utils.ImageUtils.Result;
|
||||||
|
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
|
||||||
|
import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF;
|
||||||
|
|
||||||
|
public class UploadActivity extends AuthenticatedActivity implements UploadView, SimilarImageInterface {
|
||||||
|
@Inject InputMethodManager inputMethodManager;
|
||||||
|
@Inject MediaWikiApi mwApi;
|
||||||
|
@Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs;
|
||||||
|
@Inject UploadPresenter presenter;
|
||||||
|
@Inject CategoriesModel categoriesModel;
|
||||||
|
|
||||||
|
// Main GUI
|
||||||
|
@BindView(R.id.backgroundImage) PhotoView background;
|
||||||
|
@BindView(R.id.activity_upload_cards) ConstraintLayout cardLayout;
|
||||||
|
@BindView(R.id.view_flipper) ViewFlipper viewFlipper;
|
||||||
|
|
||||||
|
// Top Card
|
||||||
|
@BindView(R.id.top_card) CardView topCard;
|
||||||
|
@BindView(R.id.top_card_expand_button) ImageView topCardExpandButton;
|
||||||
|
@BindView(R.id.top_card_title) TextView topCardTitle;
|
||||||
|
@BindView(R.id.top_card_thumbnails) RecyclerView topCardThumbnails;
|
||||||
|
|
||||||
|
// Bottom Card
|
||||||
|
@BindView(R.id.bottom_card) CardView bottomCard;
|
||||||
|
@BindView(R.id.bottom_card_expand_button) ImageView bottomCardExpandButton;
|
||||||
|
@BindView(R.id.bottom_card_title) TextView bottomCardTitle;
|
||||||
|
@BindView(R.id.bottom_card_subtitle) TextView bottomCardSubtitle;
|
||||||
|
@BindView(R.id.bottom_card_next) Button next;
|
||||||
|
@BindView(R.id.bottom_card_previous) Button previous;
|
||||||
|
@BindView(R.id.bottom_card_add_desc) Button bottomCardAddDescription;
|
||||||
|
|
||||||
|
//Right Card
|
||||||
|
@BindView(R.id.right_card) CardView rightCard;
|
||||||
|
@BindView(R.id.right_card_expand_button) ImageView rightCardExpandButton;
|
||||||
|
@BindView(R.id.right_card_map_button) View rightCardMapButton;
|
||||||
|
|
||||||
|
// Category Search
|
||||||
|
@BindView(R.id.categories_title) TextView categoryTitle;
|
||||||
|
@BindView(R.id.category_next) Button categoryNext;
|
||||||
|
@BindView(R.id.category_previous) Button categoryPrevious;
|
||||||
|
@BindView(R.id.categoriesSearchInProgress) ProgressBar categoriesSearchInProgress;
|
||||||
|
@BindView(R.id.category_search) EditText categoriesSearch;
|
||||||
|
@BindView(R.id.category_search_container) TextInputLayout categoriesSearchContainer;
|
||||||
|
@BindView(R.id.categories) RecyclerView categoriesList;
|
||||||
|
|
||||||
|
// Final Submission
|
||||||
|
@BindView(R.id.license_title) TextView licenseTitle;
|
||||||
|
@BindView(R.id.share_license_summary) TextView licenseSummary;
|
||||||
|
@BindView(R.id.media_upload_policy) TextView licensePolicy;
|
||||||
|
@BindView(R.id.license_list) Spinner licenseSpinner;
|
||||||
|
@BindView(R.id.submit) Button submit;
|
||||||
|
@BindView(R.id.license_previous) Button licensePrevious;
|
||||||
|
@BindView(R.id.rv_descriptions) RecyclerView rvDescriptions;
|
||||||
|
|
||||||
|
private DescriptionsAdapter descriptionsAdapter;
|
||||||
|
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||||
|
private CompositeDisposable compositeDisposable;
|
||||||
|
|
||||||
|
DexterPermissionObtainer dexterPermissionObtainer;
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_upload);
|
||||||
|
ButterKnife.bind(this);
|
||||||
|
compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
|
configureLayout();
|
||||||
|
configureTopCard();
|
||||||
|
configureBottomCard();
|
||||||
|
initRecyclerView();
|
||||||
|
configureRightCard();
|
||||||
|
configureNavigationButtons();
|
||||||
|
configureCategories();
|
||||||
|
configureLicenses();
|
||||||
|
|
||||||
|
presenter.init();
|
||||||
|
|
||||||
|
dexterPermissionObtainer = new DexterPermissionObtainer(this,
|
||||||
|
Manifest.permission.WRITE_EXTERNAL_STORAGE,
|
||||||
|
getString(R.string.storage_permission),
|
||||||
|
getString(R.string.write_storage_permission_rationale_for_image_share));
|
||||||
|
|
||||||
|
dexterPermissionObtainer.confirmStoragePermissions().subscribe(this::receiveSharedItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean checkIfLoggedIn() {
|
||||||
|
if (!sessionManager.isUserLoggedIn()) {
|
||||||
|
Timber.d("Current account is null");
|
||||||
|
ViewUtil.showLongToast(this, getString(R.string.user_not_logged_in));
|
||||||
|
Intent loginIntent = new Intent(UploadActivity.this, LoginActivity.class);
|
||||||
|
startActivity(loginIntent);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
presenter.cleanup();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onResume() {
|
||||||
|
super.onResume();
|
||||||
|
checkIfLoggedIn();
|
||||||
|
compositeDisposable.add(
|
||||||
|
dexterPermissionObtainer.confirmStoragePermissions()
|
||||||
|
.subscribe(() -> presenter.addView(this)));
|
||||||
|
compositeDisposable.add(
|
||||||
|
RxTextView.textChanges(categoriesSearch)
|
||||||
|
.doOnEach(v -> categoriesSearchContainer.setError(null))
|
||||||
|
.takeUntil(RxView.detaches(categoriesSearch))
|
||||||
|
.debounce(500, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(filter -> updateCategoryList(filter.toString()), Timber::e)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPause() {
|
||||||
|
presenter.removeView();
|
||||||
|
compositeDisposable.dispose();
|
||||||
|
compositeDisposable = new CompositeDisposable();
|
||||||
|
super.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateThumbnails(List<UploadModel.UploadItem> uploads) {
|
||||||
|
int uploadCount = uploads.size();
|
||||||
|
topCardThumbnails.setAdapter(new UploadThumbnailsAdapterFactory(presenter::thumbnailClicked).create(uploads));
|
||||||
|
topCardTitle.setText(getResources().getQuantityString(R.plurals.upload_count_title, uploadCount, uploadCount));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateRightCardContent(boolean gpsPresent) {
|
||||||
|
if(gpsPresent){
|
||||||
|
rightCardMapButton.setVisibility(View.VISIBLE);
|
||||||
|
}else{
|
||||||
|
rightCardMapButton.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
//The card should be disabled if it has no buttons.
|
||||||
|
setRightCardVisibility(gpsPresent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateBottomCardContent(int currentStep,
|
||||||
|
int stepCount,
|
||||||
|
UploadModel.UploadItem uploadItem,
|
||||||
|
boolean isShowingItem) {
|
||||||
|
String cardTitle = getResources().getString(R.string.step_count, currentStep, stepCount);
|
||||||
|
String cardSubTitle = getResources().getString(R.string.image_in_set_label, currentStep);
|
||||||
|
bottomCardTitle.setText(cardTitle);
|
||||||
|
bottomCardSubtitle.setText(cardSubTitle);
|
||||||
|
categoryTitle.setText(cardTitle);
|
||||||
|
licenseTitle.setText(cardTitle);
|
||||||
|
if(isShowingItem) {
|
||||||
|
descriptionsAdapter.setItems(uploadItem.title, uploadItem.descriptions);
|
||||||
|
rvDescriptions.setAdapter(descriptionsAdapter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateLicenses(List<String> licenses, String selectedLicense) {
|
||||||
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, licenses);
|
||||||
|
licenseSpinner.setAdapter(adapter);
|
||||||
|
|
||||||
|
int position = licenses.indexOf(getString(Utils.licenseNameFor(selectedLicense)));
|
||||||
|
|
||||||
|
// Check position is valid
|
||||||
|
if (position < 0) {
|
||||||
|
Timber.d("Invalid position: %d. Using default license", position);
|
||||||
|
position = licenses.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.d("Position: %d %s", position, getString(Utils.licenseNameFor(selectedLicense)));
|
||||||
|
licenseSpinner.setSelection(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("StringFormatInvalid")
|
||||||
|
@Override
|
||||||
|
public void updateLicenseSummary(String selectedLicense) {
|
||||||
|
String licenseHyperLink = "<a href='" + Utils.licenseUrlFor(selectedLicense)+"'>" +
|
||||||
|
getString(Utils.licenseNameFor(selectedLicense)) + "</a><br>";
|
||||||
|
licenseSummary.setMovementMethod(LinkMovementMethod.getInstance());
|
||||||
|
licenseSummary.setText(
|
||||||
|
Html.fromHtml(
|
||||||
|
getString(R.string.share_license_summary, licenseHyperLink)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateTopCardContent() {
|
||||||
|
RecyclerView.Adapter adapter = topCardThumbnails.getAdapter();
|
||||||
|
if (adapter != null) {
|
||||||
|
adapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setNextEnabled(boolean available) {
|
||||||
|
next.setEnabled(available);
|
||||||
|
categoryNext.setEnabled(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setSubmitEnabled(boolean available) {
|
||||||
|
submit.setEnabled(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setPreviousEnabled(boolean available) {
|
||||||
|
previous.setEnabled(available);
|
||||||
|
categoryPrevious.setEnabled(available);
|
||||||
|
licensePrevious.setEnabled(available);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTopCardState(boolean state) {
|
||||||
|
updateCardState(state, topCardExpandButton, topCardThumbnails);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setTopCardVisibility(boolean visible) {
|
||||||
|
topCard.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBottomCardVisibility(boolean visible) {
|
||||||
|
bottomCard.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRightCardVisibility(boolean visible) {
|
||||||
|
rightCard.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBottomCardVisibility(@UploadPage int page) {
|
||||||
|
if (page == TITLE_CARD) {
|
||||||
|
viewFlipper.setDisplayedChild(0);
|
||||||
|
} else if (page == CATEGORIES) {
|
||||||
|
viewFlipper.setDisplayedChild(1);
|
||||||
|
} else if (page == LICENSE) {
|
||||||
|
viewFlipper.setDisplayedChild(2);
|
||||||
|
dismissKeyboard();
|
||||||
|
} else if (page == PLEASE_WAIT) {
|
||||||
|
viewFlipper.setDisplayedChild(3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBottomCardState(boolean state) {
|
||||||
|
updateCardState(state, bottomCardExpandButton, rvDescriptions, previous, next, bottomCardAddDescription);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setRightCardState(boolean state) {
|
||||||
|
rightCardExpandButton.animate().rotation(rightCardExpandButton.getRotation() + (state ? -180 : 180)).start();
|
||||||
|
//Add all items in rightCard here
|
||||||
|
rightCardMapButton.setVisibility(state ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBackground(Uri mediaUri) {
|
||||||
|
background.setImageURI(mediaUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void dismissKeyboard() {
|
||||||
|
InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||||
|
|
||||||
|
// verify if the soft keyboard is open
|
||||||
|
if (imm != null && imm.isAcceptingText() && getCurrentFocus() != null) {
|
||||||
|
imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showBadPicturePopup(@Result int result) {
|
||||||
|
String errorMessageForResult = getErrorMessageForResult(this, result);
|
||||||
|
if (StringUtils.isNullOrWhiteSpace(errorMessageForResult)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DialogUtil.showAlertDialog(this,
|
||||||
|
getString(R.string.warning),
|
||||||
|
errorMessageForResult,
|
||||||
|
() -> presenter.deletePicture(),
|
||||||
|
() -> presenter.keepPicture());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showDuplicatePicturePopup() {
|
||||||
|
DialogUtil.showAlertDialog(this,
|
||||||
|
getString(R.string.warning),
|
||||||
|
String.format(getString(R.string.upload_title_duplicate), presenter.getCurrentImageFileName()),
|
||||||
|
null,
|
||||||
|
() -> {
|
||||||
|
presenter.keepPicture();
|
||||||
|
presenter.handleNext(descriptionsAdapter.getTitle(), getDescriptions());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void showNoCategorySelectedWarning() {
|
||||||
|
DialogUtil.showAlertDialog(this,
|
||||||
|
getString(R.string.no_categories_selected),
|
||||||
|
getString(R.string.no_categories_selected_warning_desc),
|
||||||
|
getString(R.string.no_go_back),
|
||||||
|
getString(R.string.yes_submit),
|
||||||
|
null,
|
||||||
|
() -> presenter.handleCategoryNext(categoriesModel, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void launchMapActivity(String decCoords) {
|
||||||
|
Utils.handleGeoCoordinates(this, decCoords);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showErrorMessage(int resourceId) {
|
||||||
|
ViewUtil.showShortToast(this, resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initDefaultCategories() {
|
||||||
|
updateCategoryList("");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAuthCookieAcquired(String authCookie) {
|
||||||
|
mwApi.setAuthCookie(authCookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == CommonsApplication.OPEN_APPLICATION_DETAIL_SETTINGS) {
|
||||||
|
dexterPermissionObtainer.onManualPermissionReturned();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onAuthFailure() {
|
||||||
|
Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG).show();
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureLicenses() {
|
||||||
|
licenseSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
String licenseName = parent.getItemAtPosition(position).toString();
|
||||||
|
presenter.selectLicense(licenseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(AdapterView<?> parent) {
|
||||||
|
presenter.selectLicense(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureLayout() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
cardLayout.getLayoutTransition().enableTransitionType(LayoutTransition.CHANGING);
|
||||||
|
}
|
||||||
|
background.setScaleType(ImageView.ScaleType.CENTER_CROP);
|
||||||
|
background.setOnScaleChangeListener((scaleFactor, x, y) -> presenter.closeAllCards());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureTopCard() {
|
||||||
|
topCardExpandButton.setOnClickListener(v -> presenter.toggleTopCardState());
|
||||||
|
topCardThumbnails.setLayoutManager(new LinearLayoutManager(this,
|
||||||
|
LinearLayoutManager.HORIZONTAL, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureBottomCard() {
|
||||||
|
bottomCardExpandButton.setOnClickListener(v -> presenter.toggleBottomCardState());
|
||||||
|
bottomCardAddDescription.setOnClickListener(v -> addNewDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addNewDescription() {
|
||||||
|
descriptionsAdapter.addDescription(new Description());
|
||||||
|
rvDescriptions.scrollToPosition(descriptionsAdapter.getItemCount() - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureRightCard() {
|
||||||
|
rightCardExpandButton.setOnClickListener(v -> presenter.toggleRightCardState());
|
||||||
|
rightCardMapButton.setOnClickListener(v -> presenter.openCoordinateMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureNavigationButtons() {
|
||||||
|
// Navigation next / previous for each image as we're collecting title + description
|
||||||
|
next.setOnClickListener(v -> {
|
||||||
|
setTitleAndDescriptions();
|
||||||
|
presenter.handleNext(descriptionsAdapter.getTitle(),
|
||||||
|
descriptionsAdapter.getDescriptions());
|
||||||
|
});
|
||||||
|
previous.setOnClickListener(v -> presenter.handlePrevious());
|
||||||
|
|
||||||
|
// Next / previous for the category selection currentPage
|
||||||
|
categoryNext.setOnClickListener(v -> presenter.handleCategoryNext(categoriesModel, false));
|
||||||
|
categoryPrevious.setOnClickListener(v -> presenter.handlePrevious());
|
||||||
|
|
||||||
|
// Finally, the previous / submit buttons on the final currentPage of the wizard
|
||||||
|
licensePrevious.setOnClickListener(v -> presenter.handlePrevious());
|
||||||
|
submit.setOnClickListener(v -> {
|
||||||
|
Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG).show();
|
||||||
|
presenter.handleSubmit(categoriesModel);
|
||||||
|
finish();
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTitleAndDescriptions() {
|
||||||
|
List<Description> descriptions = descriptionsAdapter.getDescriptions();
|
||||||
|
Timber.d("Descriptions size is %d are %s", descriptions.size(), descriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configureCategories() {
|
||||||
|
categoriesAdapter = new UploadCategoriesAdapterFactory(categoriesModel).create(new ArrayList<>());
|
||||||
|
categoriesList.setLayoutManager(new LinearLayoutManager(this));
|
||||||
|
categoriesList.setAdapter(categoriesAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private void updateCategoryList(String filter) {
|
||||||
|
List<String> imageTitleList = presenter.getImageTitleList();
|
||||||
|
Observable.fromIterable(categoriesModel.getSelectedCategories())
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.doOnSubscribe(disposable -> {
|
||||||
|
categoriesSearchInProgress.setVisibility(View.VISIBLE);
|
||||||
|
categoriesSearchContainer.setError(null);
|
||||||
|
categoriesAdapter.clear();
|
||||||
|
})
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.concatWith(
|
||||||
|
categoriesModel.searchAll(filter, imageTitleList)
|
||||||
|
.mergeWith(categoriesModel.searchCategories(filter, imageTitleList))
|
||||||
|
.concatWith(TextUtils.isEmpty(filter)
|
||||||
|
? categoriesModel.defaultCategories(imageTitleList) : Observable.empty())
|
||||||
|
)
|
||||||
|
.filter(categoryItem -> !categoriesModel.containsYear(categoryItem.getName()))
|
||||||
|
.distinct()
|
||||||
|
.sorted(categoriesModel.sortBySimilarity(filter))
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(
|
||||||
|
s -> categoriesAdapter.add(s),
|
||||||
|
Timber::e,
|
||||||
|
() -> {
|
||||||
|
categoriesAdapter.notifyDataSetChanged();
|
||||||
|
categoriesSearchInProgress.setVisibility(View.GONE);
|
||||||
|
|
||||||
|
if (categoriesAdapter.getItemCount() == categoriesModel.selectedCategoriesCount()
|
||||||
|
&& !categoriesSearch.getText().toString().isEmpty()) {
|
||||||
|
categoriesSearchContainer.setError("No categories found");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receiveSharedItems() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
String mimeType = intent.getType();
|
||||||
|
String source;
|
||||||
|
|
||||||
|
if (intent.hasExtra(UploadService.EXTRA_SOURCE)) {
|
||||||
|
source = intent.getStringExtra(UploadService.EXTRA_SOURCE);
|
||||||
|
} else {
|
||||||
|
source = Contribution.SOURCE_EXTERNAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Intent.ACTION_SEND.equals(intent.getAction())) {
|
||||||
|
Uri mediaUri = intent.getParcelableExtra(Intent.EXTRA_STREAM);
|
||||||
|
if (intent.getBooleanExtra("isDirectUpload", false)) {
|
||||||
|
String imageTitle = directPrefs.getString("Title", "");
|
||||||
|
String imageDesc = directPrefs.getString("Desc", "");
|
||||||
|
Timber.i("Received direct upload with title %s and description %s", imageTitle, imageDesc);
|
||||||
|
String wikidataEntityIdPref = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF);
|
||||||
|
presenter.receiveDirect(mediaUri, mimeType, source, wikidataEntityIdPref, imageTitle, imageDesc);
|
||||||
|
} else {
|
||||||
|
Timber.i("Received single upload");
|
||||||
|
presenter.receive(mediaUri, mimeType, source);
|
||||||
|
}
|
||||||
|
} else if (Intent.ACTION_SEND_MULTIPLE.equals(intent.getAction())) {
|
||||||
|
ArrayList<Uri> urisList = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
|
||||||
|
Timber.i("Received multiple upload %s", urisList.size());
|
||||||
|
presenter.receive(urisList, mimeType, source);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateCardState(boolean state, ImageView button, View... content) {
|
||||||
|
button.animate().rotation(button.getRotation() + (state ? 180 : -180)).start();
|
||||||
|
if (content != null) {
|
||||||
|
for (View view : content) {
|
||||||
|
view.setVisibility(state ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Description> getDescriptions() {
|
||||||
|
return descriptionsAdapter.getDescriptions();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initRecyclerView() {
|
||||||
|
descriptionsAdapter = new DescriptionsAdapter(this);
|
||||||
|
descriptionsAdapter.setCallback(this::showInfoAlert);
|
||||||
|
rvDescriptions.setLayoutManager(new LinearLayoutManager(getApplicationContext()));
|
||||||
|
rvDescriptions.setAdapter(descriptionsAdapter);
|
||||||
|
addNewDescription();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void showInfoAlert(int titleStringID, int messageStringId, String... formatArgs) {
|
||||||
|
new AlertDialog.Builder(this)
|
||||||
|
.setTitle(titleStringID)
|
||||||
|
.setMessage(getString(messageStringId, (Object[]) formatArgs))
|
||||||
|
.setCancelable(true)
|
||||||
|
.setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
|
||||||
|
.create()
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void showSimilarImageFragment(String originalFilePath, String possibleFilePath) {
|
||||||
|
SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment();
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putString("originalImagePath", originalFilePath);
|
||||||
|
args.putString("possibleImagePath", possibleFilePath);
|
||||||
|
newFragment.setArguments(args);
|
||||||
|
newFragment.show(getSupportFragmentManager(), "dialog");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,27 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
import com.pedrogomez.renderers.RendererBuilder;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.category.CategoryClickedListener;
|
||||||
|
import fr.free.nrw.commons.category.CategoryItem;
|
||||||
|
|
||||||
|
public class UploadCategoriesAdapterFactory {
|
||||||
|
private final CategoryClickedListener listener;
|
||||||
|
|
||||||
|
public UploadCategoriesAdapterFactory(CategoryClickedListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RVRendererAdapter<CategoryItem> create(List<CategoryItem> placeList) {
|
||||||
|
RendererBuilder<CategoryItem> builder = new RendererBuilder<CategoryItem>()
|
||||||
|
.bind(CategoryItem.class, new UploadCategoriesRenderer(listener));
|
||||||
|
ListAdapteeCollection<CategoryItem> collection = new ListAdapteeCollection<>(
|
||||||
|
placeList != null ? placeList : Collections.emptyList());
|
||||||
|
return new RVRendererAdapter<>(builder, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.CheckBox;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.Renderer;
|
||||||
|
|
||||||
|
import butterknife.BindView;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.category.CategoryClickedListener;
|
||||||
|
import fr.free.nrw.commons.category.CategoryItem;
|
||||||
|
|
||||||
|
public class UploadCategoriesRenderer extends Renderer<CategoryItem> {
|
||||||
|
@BindView(R.id.tvName) CheckBox checkedView;
|
||||||
|
private final CategoryClickedListener listener;
|
||||||
|
|
||||||
|
UploadCategoriesRenderer(CategoryClickedListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) {
|
||||||
|
return layoutInflater.inflate(R.layout.layout_upload_categories_item, viewGroup, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUpView(View view) {
|
||||||
|
ButterKnife.bind(this, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookListeners(View view) {
|
||||||
|
view.setOnClickListener(v -> {
|
||||||
|
CategoryItem item = getContent();
|
||||||
|
item.setSelected(!item.isSelected());
|
||||||
|
checkedView.setChecked(item.isSelected());
|
||||||
|
if (listener != null) {
|
||||||
|
listener.categoryClicked(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
CategoryItem item = getContent();
|
||||||
|
checkedView.setChecked(item.isSelected());
|
||||||
|
checkedView.setText(item.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,7 +23,6 @@ import java.io.InputStream;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
|
||||||
import fr.free.nrw.commons.HandlerService;
|
import fr.free.nrw.commons.HandlerService;
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import fr.free.nrw.commons.auth.SessionManager;
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
|
@ -87,49 +86,11 @@ public class UploadController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Starts a new upload task.
|
* Starts a new upload task.
|
||||||
* @param title the title of the contribution
|
*
|
||||||
* @param mediaUri the media URI of the contribution
|
* @param contribution the contribution object
|
||||||
* @param description the description of the contribution
|
|
||||||
* @param mimeType the MIME type of the contribution
|
|
||||||
* @param source the source of the contribution
|
|
||||||
* @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615")
|
|
||||||
* @param wikiDataEntityId
|
|
||||||
* @param onComplete the progress tracker
|
|
||||||
*/
|
*/
|
||||||
public void startUpload(String title, Uri contentProviderUri, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) {
|
public void startUpload(Contribution contribution) {
|
||||||
Contribution contribution;
|
startUpload(contribution, c -> {});
|
||||||
|
|
||||||
|
|
||||||
//TODO: Modify this to include coords
|
|
||||||
contribution = new Contribution(mediaUri, null, title, description, -1,
|
|
||||||
null, null, sessionManager.getCurrentAccount().name,
|
|
||||||
CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
|
|
||||||
|
|
||||||
|
|
||||||
contribution.setTag("mimeType", mimeType);
|
|
||||||
contribution.setSource(source);
|
|
||||||
|
|
||||||
Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId);
|
|
||||||
//TODO: Modify this to include coords
|
|
||||||
Account currentAccount = sessionManager.getCurrentAccount();
|
|
||||||
if (currentAccount == null) {
|
|
||||||
Timber.d("Current account is null");
|
|
||||||
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
|
|
||||||
sessionManager.forceLogin(context);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
contribution = new Contribution(mediaUri, null, title, description, -1,
|
|
||||||
null, null, currentAccount.name,
|
|
||||||
CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords);
|
|
||||||
|
|
||||||
|
|
||||||
contribution.setTag("mimeType", mimeType);
|
|
||||||
contribution.setSource(source);
|
|
||||||
contribution.setWikiDataEntityId(wikiDataEntityId);
|
|
||||||
contribution.setContentProviderUri(contentProviderUri);
|
|
||||||
|
|
||||||
//Calls the next overloaded method
|
|
||||||
startUpload(contribution, onComplete);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -142,7 +103,14 @@ public class UploadController {
|
||||||
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
|
public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) {
|
||||||
//Set creator, desc, and license
|
//Set creator, desc, and license
|
||||||
if (TextUtils.isEmpty(contribution.getCreator())) {
|
if (TextUtils.isEmpty(contribution.getCreator())) {
|
||||||
contribution.setCreator(sessionManager.getCurrentAccount().name);
|
Account currentAccount = sessionManager.getCurrentAccount();
|
||||||
|
if (currentAccount == null) {
|
||||||
|
Timber.d("Current account is null");
|
||||||
|
ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in));
|
||||||
|
sessionManager.forceLogin(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
contribution.setCreator(currentAccount.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contribution.getDescription() == null) {
|
if (contribution.getDescription() == null) {
|
||||||
|
|
@ -163,8 +131,6 @@ public class UploadController {
|
||||||
long length;
|
long length;
|
||||||
ContentResolver contentResolver = context.getContentResolver();
|
ContentResolver contentResolver = context.getContentResolver();
|
||||||
try {
|
try {
|
||||||
|
|
||||||
//TODO: understand do we really need this code
|
|
||||||
if (contribution.getDataLength() <= 0) {
|
if (contribution.getDataLength() <= 0) {
|
||||||
Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri());
|
Timber.d("UploadController/doInBackground, contribution.getLocalUri():" + contribution.getLocalUri());
|
||||||
AssetFileDescriptor assetFileDescriptor = contentResolver
|
AssetFileDescriptor assetFileDescriptor = contentResolver
|
||||||
|
|
|
||||||
400
app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
Normal file
400
app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java
Normal file
|
|
@ -0,0 +1,400 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.graphics.BitmapRegionDecoder;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Named;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
|
import fr.free.nrw.commons.auth.SessionManager;
|
||||||
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.settings.Prefs;
|
||||||
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.disposables.Disposable;
|
||||||
|
import io.reactivex.functions.Consumer;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import io.reactivex.subjects.BehaviorSubject;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
public class UploadModel {
|
||||||
|
|
||||||
|
private MediaWikiApi mwApi;
|
||||||
|
private static UploadItem DUMMY = new UploadItem(Uri.EMPTY, "", "", GPSExtractor.DUMMY, "", null,-1l) {
|
||||||
|
};
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
private final List<String> licenses;
|
||||||
|
private String license;
|
||||||
|
private final Map<String, String> licensesByName;
|
||||||
|
private List<UploadItem> items = new ArrayList<>();
|
||||||
|
private boolean topCardState = true;
|
||||||
|
private boolean bottomCardState = true;
|
||||||
|
private boolean rightCardState = true;
|
||||||
|
private int currentStepIndex = 0;
|
||||||
|
private Context context;
|
||||||
|
private ContentResolver contentResolver;
|
||||||
|
private boolean useExtStorage;
|
||||||
|
private Disposable badImageSubscription;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
SessionManager sessionManager;
|
||||||
|
private Uri currentMediaUri;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UploadModel(@Named("licenses") List<String> licenses,
|
||||||
|
@Named("default_preferences") SharedPreferences prefs,
|
||||||
|
@Named("licenses_by_name") Map<String, String> licensesByName,
|
||||||
|
Context context,
|
||||||
|
MediaWikiApi mwApi) {
|
||||||
|
this.licenses = licenses;
|
||||||
|
this.prefs = prefs;
|
||||||
|
this.license = Prefs.Licenses.CC_BY_SA_3;
|
||||||
|
this.licensesByName = licensesByName;
|
||||||
|
this.context = context;
|
||||||
|
this.mwApi = mwApi;
|
||||||
|
this.contentResolver = context.getContentResolver();
|
||||||
|
useExtStorage = this.prefs.getBoolean("useExternalStorage", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
void receive(List<Uri> mediaUri, String mimeType, String source, SimilarImageInterface similarImageInterface) {
|
||||||
|
initDefaultValues();
|
||||||
|
Observable<UploadItem> itemObservable = Observable.fromIterable(mediaUri)
|
||||||
|
.map(media -> {
|
||||||
|
currentMediaUri=media;
|
||||||
|
return cacheFileUpload(media);
|
||||||
|
})
|
||||||
|
.map(filePath -> {
|
||||||
|
long fileCreatedDate = getFileCreatedDate(currentMediaUri);
|
||||||
|
Uri uri = Uri.fromFile(new File(filePath));
|
||||||
|
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
|
||||||
|
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
|
||||||
|
FileUtils.getFileExt(filePath), null,fileCreatedDate);
|
||||||
|
Single.zip(
|
||||||
|
Single.fromCallable(() ->
|
||||||
|
new FileInputStream(filePath))
|
||||||
|
.map(FileUtils::getSHA1)
|
||||||
|
.map(mwApi::existingFile)
|
||||||
|
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
|
||||||
|
Single.fromCallable(() ->
|
||||||
|
new FileInputStream(filePath))
|
||||||
|
.map(file -> BitmapRegionDecoder.newInstance(file, false))
|
||||||
|
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
|
||||||
|
(dupe, dark) -> dupe | dark)
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.subscribe(item.imageQuality::onNext, Timber::e);
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
items = itemObservable.toList().blockingGet();
|
||||||
|
items.get(0).selected = true;
|
||||||
|
items.get(0).first = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
void receiveDirect(Uri media, String mimeType, String source, String wikidataEntityIdPref, String title, String desc, SimilarImageInterface similarImageInterface) {
|
||||||
|
initDefaultValues();
|
||||||
|
long fileCreatedDate = getFileCreatedDate(media);
|
||||||
|
String filePath = this.cacheFileUpload(media);
|
||||||
|
Uri uri = Uri.fromFile(new File(filePath));
|
||||||
|
FileProcessor fp = new FileProcessor(filePath, context.getContentResolver(), context);
|
||||||
|
UploadItem item = new UploadItem(uri, mimeType, source, fp.processFileCoordinates(similarImageInterface),
|
||||||
|
FileUtils.getFileExt(filePath), wikidataEntityIdPref,fileCreatedDate);
|
||||||
|
item.title.setTitleText(title);
|
||||||
|
item.descriptions.get(0).setDescriptionText(desc);
|
||||||
|
//TODO figure out if default descriptions in other languages exist
|
||||||
|
item.descriptions.get(0).setLanguageCode("en");
|
||||||
|
Single.zip(
|
||||||
|
Single.fromCallable(() ->
|
||||||
|
new FileInputStream(filePath))
|
||||||
|
.map(FileUtils::getSHA1)
|
||||||
|
.map(mwApi::existingFile)
|
||||||
|
.map(b -> b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK),
|
||||||
|
Single.fromCallable(() ->
|
||||||
|
new FileInputStream(filePath))
|
||||||
|
.map(file -> BitmapRegionDecoder.newInstance(file, false))
|
||||||
|
.map(ImageUtils::checkIfImageIsTooDark), //Returns IMAGE_DARK or IMAGE_OK
|
||||||
|
(dupe, dark) -> dupe | dark).subscribe(item.imageQuality::onNext);
|
||||||
|
items.add(item);
|
||||||
|
items.get(0).selected = true;
|
||||||
|
items.get(0).first = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initDefaultValues() {
|
||||||
|
currentStepIndex = 0;
|
||||||
|
topCardState = true;
|
||||||
|
bottomCardState = true;
|
||||||
|
rightCardState = true;
|
||||||
|
items = new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get file creation date from uri from all possible content providers
|
||||||
|
* @param media
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private long getFileCreatedDate(Uri media) {
|
||||||
|
try {
|
||||||
|
Cursor cursor = contentResolver.query(media, null, null, null, null);
|
||||||
|
if (cursor == null) {
|
||||||
|
return -1;//Could not fetch last_modified
|
||||||
|
}
|
||||||
|
//Content provider contracts for opening gallery from the app and that by sharing from gallery from outside are different and we need to handle both the cases
|
||||||
|
int lastModifiedColumnIndex = cursor.getColumnIndex("last_modified");//If gallery is opened from in app
|
||||||
|
if(lastModifiedColumnIndex==-1){
|
||||||
|
lastModifiedColumnIndex=cursor.getColumnIndex("datetaken");
|
||||||
|
}
|
||||||
|
//If both the content providers do not give the data, lets leave it to Jesus
|
||||||
|
if(lastModifiedColumnIndex==-1){
|
||||||
|
return -1l;
|
||||||
|
}
|
||||||
|
cursor.moveToFirst();
|
||||||
|
return cursor.getLong(lastModifiedColumnIndex);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return -1;////Could not fetch last_modified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isPreviousAvailable() {
|
||||||
|
return currentStepIndex > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isNextAvailable() {
|
||||||
|
return currentStepIndex < (items.size() + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isSubmitAvailable() {
|
||||||
|
int count = items.size();
|
||||||
|
boolean hasError = license == null;
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
UploadItem item = items.get(i);
|
||||||
|
hasError |= item.error;
|
||||||
|
}
|
||||||
|
return !hasError;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getCurrentStep() {
|
||||||
|
return currentStepIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getStepCount() {
|
||||||
|
return items.size() + 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCount() {
|
||||||
|
return items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<UploadItem> getUploads() {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isTopCardState() {
|
||||||
|
return topCardState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setTopCardState(boolean topCardState) {
|
||||||
|
this.topCardState = topCardState;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isBottomCardState() {
|
||||||
|
return bottomCardState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setRightCardState(boolean rightCardState) {
|
||||||
|
this.rightCardState = rightCardState;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isRightCardState() {
|
||||||
|
return rightCardState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setBottomCardState(boolean bottomCardState) {
|
||||||
|
this.bottomCardState = bottomCardState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void next() {
|
||||||
|
if (badImageSubscription != null)
|
||||||
|
badImageSubscription.dispose();
|
||||||
|
markCurrentUploadVisited();
|
||||||
|
if (currentStepIndex < items.size() + 1) {
|
||||||
|
currentStepIndex++;
|
||||||
|
}
|
||||||
|
updateItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentTitleAndDescriptions(Title title, List<Description> descriptions) {
|
||||||
|
setCurrentUploadTitle(title);
|
||||||
|
setCurrentUploadDescriptions(descriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCurrentUploadTitle(Title title) {
|
||||||
|
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
|
||||||
|
items.get(currentStepIndex).title = title;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCurrentUploadDescriptions(List<Description> descriptions) {
|
||||||
|
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
|
||||||
|
items.get(currentStepIndex).descriptions = descriptions;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void previous() {
|
||||||
|
if (badImageSubscription != null)
|
||||||
|
badImageSubscription.dispose();
|
||||||
|
markCurrentUploadVisited();
|
||||||
|
if (currentStepIndex > 0) {
|
||||||
|
currentStepIndex--;
|
||||||
|
}
|
||||||
|
updateItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void jumpTo(UploadItem item) {
|
||||||
|
currentStepIndex = items.indexOf(item);
|
||||||
|
item.visited = true;
|
||||||
|
updateItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
UploadItem getCurrentItem() {
|
||||||
|
return isShowingItem() ? items.get(currentStepIndex) : DUMMY;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isShowingItem() {
|
||||||
|
return currentStepIndex < items.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateItemState() {
|
||||||
|
int count = items.size();
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
UploadItem item = items.get(i);
|
||||||
|
item.selected = (currentStepIndex >= count || i == currentStepIndex);
|
||||||
|
item.error = item.title == null || item.title.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void markCurrentUploadVisited() {
|
||||||
|
if (currentStepIndex < items.size() && currentStepIndex >= 0) {
|
||||||
|
items.get(currentStepIndex).visited = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getLicenses() {
|
||||||
|
return licenses;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getSelectedLicense() {
|
||||||
|
return license;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelectedLicense(String licenseName) {
|
||||||
|
this.license = licensesByName.get(licenseName);
|
||||||
|
}
|
||||||
|
|
||||||
|
Observable<Contribution> buildContributions(List<String> categoryStringList) {
|
||||||
|
return Observable.fromIterable(items).map(item ->
|
||||||
|
{
|
||||||
|
Contribution contribution = new Contribution(item.mediaUri, null, item.title + "." + item.fileExt,
|
||||||
|
Description.formatList(item.descriptions), -1,
|
||||||
|
null, null, sessionManager.getUserName(),
|
||||||
|
CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
|
||||||
|
contribution.setWikiDataEntityId(item.wikidataEntityId);
|
||||||
|
contribution.setCategories(categoryStringList);
|
||||||
|
contribution.setTag("mimeType", item.mimeType);
|
||||||
|
contribution.setSource(item.source);
|
||||||
|
contribution.setContentProviderUri(item.mediaUri);
|
||||||
|
if (item.createdTimestamp != -1l) {
|
||||||
|
contribution.setDateCreated(new Date(item.createdTimestamp));
|
||||||
|
//Set the date only if you have it, else the upload service is gonna try it the other way
|
||||||
|
}
|
||||||
|
return contribution;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy files into local storage and return file path
|
||||||
|
*
|
||||||
|
* @param media Uri of the file
|
||||||
|
* @return path of the enw file
|
||||||
|
*/
|
||||||
|
private String cacheFileUpload(Uri media) {
|
||||||
|
try {
|
||||||
|
String copyPath;
|
||||||
|
if (useExtStorage)
|
||||||
|
copyPath = FileUtils.createExternalCopyPathAndCopy(media, contentResolver);
|
||||||
|
else
|
||||||
|
copyPath = FileUtils.createCopyPathAndCopy(media, context);
|
||||||
|
Timber.i("File path is " + copyPath);
|
||||||
|
return copyPath;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Timber.w(e, "Error in copying URI " + media.getPath());
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void keepPicture() {
|
||||||
|
items.get(currentStepIndex).imageQuality.onNext(ImageUtils.IMAGE_KEEP);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deletePicture() {
|
||||||
|
badImageSubscription.dispose();
|
||||||
|
items.remove(currentStepIndex).imageQuality.onComplete();
|
||||||
|
updateItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
void subscribeBadPicture(Consumer<Integer> consumer) {
|
||||||
|
badImageSubscription = getCurrentItem().imageQuality.subscribe(consumer, Timber::e);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings("WeakerAccess")
|
||||||
|
static class UploadItem {
|
||||||
|
public final Uri mediaUri;
|
||||||
|
public final String mimeType;
|
||||||
|
public final String source;
|
||||||
|
public final GPSExtractor gpsCoords;
|
||||||
|
|
||||||
|
public boolean selected = false;
|
||||||
|
public boolean first = false;
|
||||||
|
public String fileExt;
|
||||||
|
public BehaviorSubject<Integer> imageQuality;
|
||||||
|
Title title;
|
||||||
|
List<Description> descriptions;
|
||||||
|
public String wikidataEntityId;
|
||||||
|
public boolean visited;
|
||||||
|
public boolean error;
|
||||||
|
public long createdTimestamp;
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
UploadItem(Uri mediaUri, String mimeType, String source, GPSExtractor gpsCoords, String fileExt, @Nullable String wikidataEntityId, long createdTimestamp) {
|
||||||
|
title = new Title();
|
||||||
|
descriptions = new ArrayList<>();
|
||||||
|
descriptions.add(new Description());
|
||||||
|
this.wikidataEntityId = wikidataEntityId;
|
||||||
|
this.mediaUri = mediaUri;
|
||||||
|
this.mimeType = mimeType;
|
||||||
|
this.source = source;
|
||||||
|
this.gpsCoords = gpsCoords;
|
||||||
|
this.fileExt = fileExt;
|
||||||
|
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
|
||||||
|
this.createdTimestamp=createdTimestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,430 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import java.lang.reflect.Proxy;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.inject.Singleton;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
import fr.free.nrw.commons.category.CategoriesModel;
|
||||||
|
import fr.free.nrw.commons.contributions.Contribution;
|
||||||
|
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||||
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
|
import io.reactivex.Completable;
|
||||||
|
import io.reactivex.Observable;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import timber.log.Timber;
|
||||||
|
|
||||||
|
import static fr.free.nrw.commons.upload.UploadModel.UploadItem;
|
||||||
|
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
|
||||||
|
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
|
||||||
|
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
|
||||||
|
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The MVP pattern presenter of Upload GUI
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
public class UploadPresenter {
|
||||||
|
|
||||||
|
private final UploadModel uploadModel;
|
||||||
|
private final UploadController uploadController;
|
||||||
|
private final MediaWikiApi mediaWikiApi;
|
||||||
|
|
||||||
|
private static final UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
|
||||||
|
new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
|
||||||
|
private UploadView view = DUMMY;
|
||||||
|
|
||||||
|
private static final SimilarImageInterface SIMILAR_IMAGE = (SimilarImageInterface) Proxy.newProxyInstance(SimilarImageInterface.class.getClassLoader(),
|
||||||
|
new Class[]{SimilarImageInterface.class}, (proxy, method, methodArgs) -> null);
|
||||||
|
private SimilarImageInterface similarImageInterface = SIMILAR_IMAGE;
|
||||||
|
|
||||||
|
@UploadView.UploadPage
|
||||||
|
private int currentPage = UploadView.PLEASE_WAIT;
|
||||||
|
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
UploadPresenter(UploadModel uploadModel,
|
||||||
|
UploadController uploadController,
|
||||||
|
MediaWikiApi mediaWikiApi) {
|
||||||
|
this.uploadModel = uploadModel;
|
||||||
|
this.uploadController = uploadController;
|
||||||
|
this.mediaWikiApi = mediaWikiApi;
|
||||||
|
}
|
||||||
|
|
||||||
|
void receive(Uri mediaUri, String mimeType, String source) {
|
||||||
|
receive(Collections.singletonList(mediaUri), mimeType, source);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes the items received to {@link #uploadModel} and displays the items.
|
||||||
|
*
|
||||||
|
* @param media The Uri's of the media being uploaded.
|
||||||
|
* @param mimeType the mimeType of the files.
|
||||||
|
* @param source File source from {@link Contribution.FileSource}
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
void receive(List<Uri> media, String mimeType, @Contribution.FileSource String source) {
|
||||||
|
Completable.fromRunnable(() -> uploadModel.receive(media, mimeType, source, similarImageInterface))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
updateCards();
|
||||||
|
updateLicenses();
|
||||||
|
updateContent();
|
||||||
|
if (uploadModel.isShowingItem())
|
||||||
|
uploadModel.subscribeBadPicture(this::handleBadPicture);
|
||||||
|
}, Timber::e);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Passes the direct upload item received to {@link #uploadModel} and displays the items.
|
||||||
|
*
|
||||||
|
* @param media The Uri's of the media being uploaded.
|
||||||
|
* @param mimeType the mimeType of the files.
|
||||||
|
* @param source File source from {@link Contribution.FileSource}
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
void receiveDirect(Uri media, String mimeType, @Contribution.FileSource String source, String wikidataEntityIdPref, String title, String desc) {
|
||||||
|
Completable.fromRunnable(() -> uploadModel.receiveDirect(media, mimeType, source, wikidataEntityIdPref, title, desc, similarImageInterface))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(() -> {
|
||||||
|
updateCards();
|
||||||
|
updateLicenses();
|
||||||
|
updateContent();
|
||||||
|
if (uploadModel.isShowingItem())
|
||||||
|
uploadModel.subscribeBadPicture(this::handleBadPicture);
|
||||||
|
}, Timber::e);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sets the license to parameter and updates {@link UploadActivity}
|
||||||
|
*
|
||||||
|
* @param licenseName license name
|
||||||
|
*/
|
||||||
|
void selectLicense(String licenseName) {
|
||||||
|
uploadModel.setSelectedLicense(licenseName);
|
||||||
|
view.updateLicenseSummary(uploadModel.getSelectedLicense());
|
||||||
|
}
|
||||||
|
|
||||||
|
//region Wizard step management
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the next button in {@link UploadActivity}
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
void handleNext(Title title,
|
||||||
|
List<Description> descriptions) {
|
||||||
|
validateCurrentItemTitle()
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(errorCode -> handleImage(errorCode, title, descriptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the next button in {@link UploadActivity}
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
void handleCategoryNext(CategoriesModel categoriesModel,
|
||||||
|
boolean noCategoryWarningShown) {
|
||||||
|
if (categoriesModel.selectedCategoriesCount() < 1 && !noCategoryWarningShown) {
|
||||||
|
view.showNoCategorySelectedWarning();
|
||||||
|
} else {
|
||||||
|
nextUploadedItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleImage(Integer errorCode, Title title, List<Description> descriptions) {
|
||||||
|
switch (errorCode) {
|
||||||
|
case EMPTY_TITLE:
|
||||||
|
view.showErrorMessage(R.string.add_title_toast);
|
||||||
|
break;
|
||||||
|
case FILE_NAME_EXISTS:
|
||||||
|
if(getCurrentItem().imageQuality.getValue().equals(IMAGE_KEEP)) {
|
||||||
|
setTitleAndDescription(title, descriptions);
|
||||||
|
nextUploadedItem();
|
||||||
|
} else {
|
||||||
|
view.showDuplicatePicturePopup();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case IMAGE_OK:
|
||||||
|
default:
|
||||||
|
setTitleAndDescription(title, descriptions);
|
||||||
|
nextUploadedItem();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void nextUploadedItem() {
|
||||||
|
uploadModel.next();
|
||||||
|
updateContent();
|
||||||
|
if (uploadModel.isShowingItem()) {
|
||||||
|
uploadModel.subscribeBadPicture(this::handleBadPicture);
|
||||||
|
}
|
||||||
|
view.dismissKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTitleAndDescription(Title title, List<Description> descriptions) {
|
||||||
|
uploadModel.setCurrentTitleAndDescriptions(title, descriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Title getCurrentImageTitle() {
|
||||||
|
return getCurrentItem().title;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getCurrentImageFileName() {
|
||||||
|
UploadItem currentItem = getCurrentItem();
|
||||||
|
return currentItem.title + "." + uploadModel.getCurrentItem().fileExt;
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
private Observable<Integer> validateCurrentItemTitle() {
|
||||||
|
Title title = getCurrentImageTitle();
|
||||||
|
if (title.isEmpty()) {
|
||||||
|
view.showErrorMessage(R.string.add_title_toast);
|
||||||
|
return Observable.just(EMPTY_TITLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.fromCallable(() -> mediaWikiApi.fileExistsWithName(getCurrentImageFileName()))
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.map(doesFileExist -> {
|
||||||
|
if (doesFileExist) {
|
||||||
|
return FILE_NAME_EXISTS;
|
||||||
|
}
|
||||||
|
return IMAGE_OK;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the previous button in {@link UploadActivity}
|
||||||
|
*/
|
||||||
|
void handlePrevious() {
|
||||||
|
uploadModel.previous();
|
||||||
|
updateContent();
|
||||||
|
if (uploadModel.isShowingItem()) {
|
||||||
|
uploadModel.subscribeBadPicture(this::handleBadPicture);
|
||||||
|
}
|
||||||
|
view.dismissKeyboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when one of the pictures on the top card is clicked on in {@link UploadActivity}
|
||||||
|
*/
|
||||||
|
void thumbnailClicked(UploadItem item) {
|
||||||
|
uploadModel.jumpTo(item);
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the submit button in {@link UploadActivity}
|
||||||
|
*/
|
||||||
|
@SuppressLint("CheckResult")
|
||||||
|
void handleSubmit(CategoriesModel categoriesModel) {
|
||||||
|
if (view.checkIfLoggedIn())
|
||||||
|
uploadModel.buildContributions(categoriesModel.getCategoryStringList())
|
||||||
|
.observeOn(Schedulers.io())
|
||||||
|
.subscribe(uploadController::startUpload);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the map button on the right card in {@link UploadActivity}
|
||||||
|
*/
|
||||||
|
void openCoordinateMap() {
|
||||||
|
GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
|
||||||
|
if (gpsObj != null && gpsObj.imageCoordsExists) {
|
||||||
|
view.launchMapActivity(gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called by the image processors when a result is obtained.
|
||||||
|
*
|
||||||
|
* @param result the result returned by the image procesors.
|
||||||
|
*/
|
||||||
|
private void handleBadPicture(@ImageUtils.Result int result) {
|
||||||
|
view.showBadPicturePopup(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
void keepPicture() {
|
||||||
|
uploadModel.keepPicture();
|
||||||
|
}
|
||||||
|
|
||||||
|
void deletePicture() {
|
||||||
|
if (uploadModel.getCount() == 1)
|
||||||
|
view.finish();
|
||||||
|
else {
|
||||||
|
uploadModel.deletePicture();
|
||||||
|
updateCards();
|
||||||
|
updateContent();
|
||||||
|
if (uploadModel.isShowingItem())
|
||||||
|
uploadModel.subscribeBadPicture(this::handleBadPicture);
|
||||||
|
view.dismissKeyboard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region Top Bottom and Right card state management
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the top card's state between open and closed.
|
||||||
|
*/
|
||||||
|
void toggleTopCardState() {
|
||||||
|
uploadModel.setTopCardState(!uploadModel.isTopCardState());
|
||||||
|
view.setTopCardState(uploadModel.isTopCardState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the bottom card's state between open and closed.
|
||||||
|
*/
|
||||||
|
void toggleBottomCardState() {
|
||||||
|
uploadModel.setBottomCardState(!uploadModel.isBottomCardState());
|
||||||
|
view.setBottomCardState(uploadModel.isBottomCardState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the right card's state between open and closed.
|
||||||
|
*/
|
||||||
|
void toggleRightCardState() {
|
||||||
|
uploadModel.setRightCardState(!uploadModel.isRightCardState());
|
||||||
|
view.setRightCardState(uploadModel.isRightCardState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets all the cards' states to closed.
|
||||||
|
*/
|
||||||
|
void closeAllCards() {
|
||||||
|
if (uploadModel.isTopCardState()) {
|
||||||
|
uploadModel.setTopCardState(false);
|
||||||
|
view.setTopCardState(false);
|
||||||
|
}
|
||||||
|
if (uploadModel.isRightCardState()) {
|
||||||
|
uploadModel.setRightCardState(false);
|
||||||
|
view.setRightCardState(false);
|
||||||
|
}
|
||||||
|
if (uploadModel.isBottomCardState()) {
|
||||||
|
uploadModel.setBottomCardState(false);
|
||||||
|
view.setBottomCardState(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
//region View / Lifecycle management
|
||||||
|
public void init() {
|
||||||
|
uploadController.prepareService();
|
||||||
|
}
|
||||||
|
|
||||||
|
void cleanup() {
|
||||||
|
uploadController.cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeView() {
|
||||||
|
this.view = DUMMY;
|
||||||
|
}
|
||||||
|
|
||||||
|
void addView(UploadView view) {
|
||||||
|
this.view = view;
|
||||||
|
|
||||||
|
updateCards();
|
||||||
|
updateLicenses();
|
||||||
|
updateContent();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the cards for when there is a change to the amount of items being uploaded.
|
||||||
|
*/
|
||||||
|
private void updateCards() {
|
||||||
|
Timber.i("uploadModel.getCount():" + uploadModel.getCount());
|
||||||
|
view.updateThumbnails(uploadModel.getUploads());
|
||||||
|
view.setTopCardVisibility(uploadModel.getCount() > 1);
|
||||||
|
view.setBottomCardVisibility(uploadModel.getCount() > 0);
|
||||||
|
view.setTopCardState(uploadModel.isTopCardState());
|
||||||
|
view.setBottomCardState(uploadModel.isBottomCardState());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the list of licences and the default license.
|
||||||
|
*/
|
||||||
|
private void updateLicenses() {
|
||||||
|
String selectedLicense = uploadModel.getSelectedLicense();
|
||||||
|
view.updateLicenses(uploadModel.getLicenses(), selectedLicense);
|
||||||
|
view.updateLicenseSummary(selectedLicense);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the cards and the background when a new currentPage is selected.
|
||||||
|
*/
|
||||||
|
private void updateContent() {
|
||||||
|
Timber.i("Updating content for currentPage" + uploadModel.getCurrentStep());
|
||||||
|
view.setNextEnabled(uploadModel.isNextAvailable());
|
||||||
|
view.setPreviousEnabled(uploadModel.isPreviousAvailable());
|
||||||
|
view.setSubmitEnabled(uploadModel.isSubmitAvailable());
|
||||||
|
|
||||||
|
view.setBackground(uploadModel.getCurrentItem().mediaUri);
|
||||||
|
|
||||||
|
view.updateBottomCardContent(uploadModel.getCurrentStep(),
|
||||||
|
uploadModel.getStepCount(),
|
||||||
|
uploadModel.getCurrentItem(),
|
||||||
|
uploadModel.isShowingItem());
|
||||||
|
|
||||||
|
view.updateTopCardContent();
|
||||||
|
|
||||||
|
GPSExtractor gpsObj = uploadModel.getCurrentItem().gpsCoords;
|
||||||
|
view.updateRightCardContent(gpsObj != null && gpsObj.imageCoordsExists);
|
||||||
|
|
||||||
|
showCorrectCards(uploadModel.getCurrentStep(), uploadModel.getCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the layout to show the correct bottom card.
|
||||||
|
*
|
||||||
|
* @param currentStep the current step
|
||||||
|
* @param uploadCount how many items are being uploaded
|
||||||
|
*/
|
||||||
|
private void showCorrectCards(int currentStep, int uploadCount) {
|
||||||
|
if (uploadCount == 0) {
|
||||||
|
currentPage = UploadView.PLEASE_WAIT;
|
||||||
|
} else if (currentStep <= uploadCount) {
|
||||||
|
currentPage = UploadView.TITLE_CARD;
|
||||||
|
view.setTopCardVisibility(uploadModel.getCount() > 1);
|
||||||
|
} else if (currentStep == uploadCount + 1) {
|
||||||
|
currentPage = UploadView.CATEGORIES;
|
||||||
|
view.setTopCardVisibility(false);
|
||||||
|
view.setRightCardVisibility(false);
|
||||||
|
view.initDefaultCategories();
|
||||||
|
} else {
|
||||||
|
currentPage = UploadView.LICENSE;
|
||||||
|
view.setTopCardVisibility(false);
|
||||||
|
view.setRightCardVisibility(false);
|
||||||
|
}
|
||||||
|
view.setBottomCardVisibility(currentPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
//endregion
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the item currently being displayed
|
||||||
|
*/
|
||||||
|
private UploadItem getCurrentItem() {
|
||||||
|
return uploadModel.getCurrentItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> getImageTitleList() {
|
||||||
|
List<String> titleList = new ArrayList<>();
|
||||||
|
for (UploadItem item : uploadModel.getUploads()) {
|
||||||
|
if (item.title.isSet()) {
|
||||||
|
titleList.add(item.title.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return titleList;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -62,7 +62,9 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
private NotificationCompat.Builder curProgressNotification;
|
private NotificationCompat.Builder curProgressNotification;
|
||||||
private int toUpload;
|
private int toUpload;
|
||||||
|
|
||||||
// The file names of unfinished uploads, used to prevent overwriting
|
/**
|
||||||
|
* The file names of unfinished uploads, used to prevent overwriting
|
||||||
|
*/
|
||||||
private Set<String> unfinishedUploads = new HashSet<>();
|
private Set<String> unfinishedUploads = new HashSet<>();
|
||||||
|
|
||||||
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
|
// DO NOT HAVE NOTIFICATION ID OF 0 FOR ANYTHING
|
||||||
|
|
@ -314,6 +316,7 @@ public class UploadService extends HandlerService<Contribution> {
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StringFormatInvalid")
|
@SuppressLint("StringFormatInvalid")
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
private void showFailedNotification(Contribution contribution) {
|
private void showFailedNotification(Contribution contribution) {
|
||||||
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
|
Notification failureNotification = new NotificationCompat.Builder(this).setAutoCancel(true)
|
||||||
.setSmallIcon(R.drawable.ic_launcher)
|
.setSmallIcon(R.drawable.ic_launcher)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import com.facebook.drawee.view.SimpleDraweeView;
|
||||||
|
import com.pedrogomez.renderers.Renderer;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.R;
|
||||||
|
|
||||||
|
class UploadThumbnailRenderer extends Renderer<UploadModel.UploadItem> {
|
||||||
|
private ThumbnailClickedListener listener;
|
||||||
|
private SimpleDraweeView background;
|
||||||
|
private View space;
|
||||||
|
private ImageView error;
|
||||||
|
|
||||||
|
public UploadThumbnailRenderer(ThumbnailClickedListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
|
||||||
|
return inflater.inflate(R.layout.item_upload_thumbnail, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setUpView(View rootView) {
|
||||||
|
error = rootView.findViewById(R.id.error);
|
||||||
|
space = rootView.findViewById(R.id.left_space);
|
||||||
|
background = rootView.findViewById(R.id.thumbnail);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void hookListeners(View rootView) {
|
||||||
|
background.setOnClickListener(v -> listener.thumbnailClicked(getContent()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render() {
|
||||||
|
UploadModel.UploadItem content = getContent();
|
||||||
|
background.setImageURI(content.mediaUri);
|
||||||
|
background.setAlpha(content.selected ? 1.0f : 0.5f);
|
||||||
|
space.setVisibility(content.first ? View.VISIBLE : View.GONE);
|
||||||
|
error.setVisibility(content.visited && content.error ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||||
|
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||||
|
import com.pedrogomez.renderers.RendererBuilder;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
public class UploadThumbnailsAdapterFactory {
|
||||||
|
private ThumbnailClickedListener listener;
|
||||||
|
|
||||||
|
UploadThumbnailsAdapterFactory(ThumbnailClickedListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RVRendererAdapter<UploadModel.UploadItem> create(List<UploadModel.UploadItem> placeList) {
|
||||||
|
RendererBuilder<UploadModel.UploadItem> builder = new RendererBuilder<UploadModel.UploadItem>()
|
||||||
|
.bind(UploadModel.UploadItem.class, new UploadThumbnailRenderer(listener));
|
||||||
|
ListAdapteeCollection<UploadModel.UploadItem> collection = new ListAdapteeCollection<>(
|
||||||
|
placeList != null ? placeList : Collections.emptyList());
|
||||||
|
return new RVRendererAdapter<>(builder, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
82
app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
Normal file
82
app/src/main/java/fr/free/nrw/commons/upload/UploadView.java
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
package fr.free.nrw.commons.upload;
|
||||||
|
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
|
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import fr.free.nrw.commons.utils.ImageUtils;
|
||||||
|
|
||||||
|
import static java.lang.annotation.RetentionPolicy.SOURCE;
|
||||||
|
|
||||||
|
public interface UploadView {
|
||||||
|
// Dummy implementation of the view interface to allow us to have a 'null object pattern'
|
||||||
|
// in the presenter and avoid constant NULL checking.
|
||||||
|
// UploadView DUMMY = (UploadView) Proxy.newProxyInstance(UploadView.class.getClassLoader(),
|
||||||
|
// new Class[]{UploadView.class}, (proxy, method, methodArgs) -> null);
|
||||||
|
|
||||||
|
List<Description> getDescriptions();
|
||||||
|
|
||||||
|
@Retention(SOURCE)
|
||||||
|
@IntDef({PLEASE_WAIT, TITLE_CARD, CATEGORIES, LICENSE})
|
||||||
|
@interface UploadPage {}
|
||||||
|
|
||||||
|
int PLEASE_WAIT = 0;
|
||||||
|
|
||||||
|
int TITLE_CARD = 1;
|
||||||
|
int CATEGORIES = 2;
|
||||||
|
int LICENSE = 3;
|
||||||
|
|
||||||
|
boolean checkIfLoggedIn();
|
||||||
|
|
||||||
|
void updateThumbnails(List<UploadModel.UploadItem> uploads);
|
||||||
|
|
||||||
|
void setNextEnabled(boolean available);
|
||||||
|
|
||||||
|
void setSubmitEnabled(boolean available);
|
||||||
|
|
||||||
|
void setPreviousEnabled(boolean available);
|
||||||
|
|
||||||
|
void setTopCardState(boolean state);
|
||||||
|
|
||||||
|
void setRightCardVisibility(boolean visible);
|
||||||
|
|
||||||
|
void setBottomCardState(boolean state);
|
||||||
|
|
||||||
|
void setRightCardState(boolean bottomCardState);
|
||||||
|
|
||||||
|
void setBackground(Uri mediaUri);
|
||||||
|
|
||||||
|
void setTopCardVisibility(boolean visible);
|
||||||
|
|
||||||
|
void setBottomCardVisibility(boolean visible);
|
||||||
|
|
||||||
|
void setBottomCardVisibility(@UploadPage int page);
|
||||||
|
|
||||||
|
void updateRightCardContent(boolean gpsPresent);
|
||||||
|
|
||||||
|
void updateBottomCardContent(int currentStep, int stepCount, UploadModel.UploadItem uploadItem, boolean isShowingItem);
|
||||||
|
|
||||||
|
void updateLicenses(List<String> licenses, String selectedLicense);
|
||||||
|
|
||||||
|
void updateLicenseSummary(String selectedLicense);
|
||||||
|
|
||||||
|
void updateTopCardContent();
|
||||||
|
|
||||||
|
void dismissKeyboard();
|
||||||
|
|
||||||
|
void showBadPicturePopup(@ImageUtils.Result int errorMessage);
|
||||||
|
|
||||||
|
void showDuplicatePicturePopup();
|
||||||
|
|
||||||
|
void finish();
|
||||||
|
|
||||||
|
void launchMapActivity(String decCoords);
|
||||||
|
|
||||||
|
void showErrorMessage(int resourceId);
|
||||||
|
|
||||||
|
void initDefaultCategories();
|
||||||
|
|
||||||
|
void showNoCategorySelectedWarning();
|
||||||
|
}
|
||||||
|
|
@ -7,61 +7,61 @@ import java.util.HashMap;
|
||||||
* info in the user language
|
* info in the user language
|
||||||
*/
|
*/
|
||||||
public class UrlLicense {
|
public class UrlLicense {
|
||||||
HashMap<String,String> urlLicense = new HashMap<String, String>();
|
public static HashMap<String,String> urlLicense = new HashMap<>();
|
||||||
public void initialize(){
|
static {
|
||||||
urlLicense.put("en","https://commons.wikimedia.org/wiki/Commons:Licensing");
|
urlLicense.put("en", "https://commons.wikimedia.org/wiki/Commons:Licensing");
|
||||||
urlLicense.put("ar","https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
|
urlLicense.put("ar", "https://commons.wikimedia.org/wiki/Commons:Licensing/ar");
|
||||||
urlLicense.put("ast","https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
|
urlLicense.put("ast", "https://commons.wikimedia.org/wiki/Commons:Licensing/ast");
|
||||||
urlLicense.put("az","https://commons.wikimedia.org/wiki/Commons:Licensing/az");
|
urlLicense.put("az", "https://commons.wikimedia.org/wiki/Commons:Licensing/az");
|
||||||
urlLicense.put("be","https://commons.wikimedia.org/wiki/Commons:Licensing/be");
|
urlLicense.put("be", "https://commons.wikimedia.org/wiki/Commons:Licensing/be");
|
||||||
urlLicense.put("bg","https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
|
urlLicense.put("bg", "https://commons.wikimedia.org/wiki/Commons:Licensing/bg");
|
||||||
urlLicense.put("bn","https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
|
urlLicense.put("bn", "https://commons.wikimedia.org/wiki/Commons:Licensing/bn");
|
||||||
urlLicense.put("ca","https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
|
urlLicense.put("ca", "https://commons.wikimedia.org/wiki/Commons:Licensing/ca");
|
||||||
urlLicense.put("cs","https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
|
urlLicense.put("cs", "https://commons.wikimedia.org/wiki/Commons:Licensing/cs");
|
||||||
urlLicense.put("da","https://commons.wikimedia.org/wiki/Commons:Licensing/da");
|
urlLicense.put("da", "https://commons.wikimedia.org/wiki/Commons:Licensing/da");
|
||||||
urlLicense.put("de","https://commons.wikimedia.org/wiki/Commons:Licensing/de");
|
urlLicense.put("de", "https://commons.wikimedia.org/wiki/Commons:Licensing/de");
|
||||||
urlLicense.put("el","https://commons.wikimedia.org/wiki/Commons:Licensing/el");
|
urlLicense.put("el", "https://commons.wikimedia.org/wiki/Commons:Licensing/el");
|
||||||
urlLicense.put("eo","https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
|
urlLicense.put("eo", "https://commons.wikimedia.org/wiki/Commons:Licensing/eo");
|
||||||
urlLicense.put("es","https://commons.wikimedia.org/wiki/Commons:Licensing/es");
|
urlLicense.put("es", "https://commons.wikimedia.org/wiki/Commons:Licensing/es");
|
||||||
urlLicense.put("eu","https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
|
urlLicense.put("eu", "https://commons.wikimedia.org/wiki/Commons:Licensing/eu");
|
||||||
urlLicense.put("fa","https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
|
urlLicense.put("fa", "https://commons.wikimedia.org/wiki/Commons:Licensing/fa");
|
||||||
urlLicense.put("fi","https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
|
urlLicense.put("fi", "https://commons.wikimedia.org/wiki/Commons:Licensing/fi");
|
||||||
urlLicense.put("fr","https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
|
urlLicense.put("fr", "https://commons.wikimedia.org/wiki/Commons:Licensing/fr");
|
||||||
urlLicense.put("gl","https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
|
urlLicense.put("gl", "https://commons.wikimedia.org/wiki/Commons:Licensing/gl");
|
||||||
urlLicense.put("gsw","https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
|
urlLicense.put("gsw", "https://commons.wikimedia.org/wiki/Commons:Licensing/gsw");
|
||||||
urlLicense.put("he","https://commons.wikimedia.org/wiki/Commons:Licensing/he");
|
urlLicense.put("he", "https://commons.wikimedia.org/wiki/Commons:Licensing/he");
|
||||||
urlLicense.put("hi","https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
|
urlLicense.put("hi", "https://commons.wikimedia.org/wiki/Commons:Licensing/hi");
|
||||||
urlLicense.put("hu","https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
|
urlLicense.put("hu", "https://commons.wikimedia.org/wiki/Commons:Licensing/hu");
|
||||||
urlLicense.put("id","https://commons.wikimedia.org/wiki/Commons:Licensing/id");
|
urlLicense.put("id", "https://commons.wikimedia.org/wiki/Commons:Licensing/id");
|
||||||
urlLicense.put("is","https://commons.wikimedia.org/wiki/Commons:Licensing/is");
|
urlLicense.put("is", "https://commons.wikimedia.org/wiki/Commons:Licensing/is");
|
||||||
urlLicense.put("it","https://commons.wikimedia.org/wiki/Commons:Licensing/it");
|
urlLicense.put("it", "https://commons.wikimedia.org/wiki/Commons:Licensing/it");
|
||||||
urlLicense.put("ja","https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
|
urlLicense.put("ja", "https://commons.wikimedia.org/wiki/Commons:Licensing/ja");
|
||||||
urlLicense.put("ka","https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
|
urlLicense.put("ka", "https://commons.wikimedia.org/wiki/Commons:Licensing/ka");
|
||||||
urlLicense.put("km","https://commons.wikimedia.org/wiki/Commons:Licensing/km");
|
urlLicense.put("km", "https://commons.wikimedia.org/wiki/Commons:Licensing/km");
|
||||||
urlLicense.put("ko","https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
|
urlLicense.put("ko", "https://commons.wikimedia.org/wiki/Commons:Licensing/ko");
|
||||||
urlLicense.put("ku","https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
|
urlLicense.put("ku", "https://commons.wikimedia.org/wiki/Commons:Licensing/ku");
|
||||||
urlLicense.put("mk","https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
|
urlLicense.put("mk", "https://commons.wikimedia.org/wiki/Commons:Licensing/mk");
|
||||||
urlLicense.put("mr","https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
|
urlLicense.put("mr", "https://commons.wikimedia.org/wiki/Commons:Licensing/mr");
|
||||||
urlLicense.put("ms","https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
|
urlLicense.put("ms", "https://commons.wikimedia.org/wiki/Commons:Licensing/ms");
|
||||||
urlLicense.put("my","https://commons.wikimedia.org/wiki/Commons:Licensing/my");
|
urlLicense.put("my", "https://commons.wikimedia.org/wiki/Commons:Licensing/my");
|
||||||
urlLicense.put("nl","https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
|
urlLicense.put("nl", "https://commons.wikimedia.org/wiki/Commons:Licensing/nl");
|
||||||
urlLicense.put("oc","https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
|
urlLicense.put("oc", "https://commons.wikimedia.org/wiki/Commons:Licensing/oc");
|
||||||
urlLicense.put("pl","https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
|
urlLicense.put("pl", "https://commons.wikimedia.org/wiki/Commons:Licensing/pl");
|
||||||
urlLicense.put("pt","https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
|
urlLicense.put("pt", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt");
|
||||||
urlLicense.put("pt-br","https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
|
urlLicense.put("pt-br", "https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br");
|
||||||
urlLicense.put("ro","https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
|
urlLicense.put("ro", "https://commons.wikimedia.org/wiki/Commons:Licensing/ro");
|
||||||
urlLicense.put("ru","https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
|
urlLicense.put("ru", "https://commons.wikimedia.org/wiki/Commons:Licensing/ru");
|
||||||
urlLicense.put("scn","https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
|
urlLicense.put("scn", "https://commons.wikimedia.org/wiki/Commons:Licensing/scn");
|
||||||
urlLicense.put("sk","https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
|
urlLicense.put("sk", "https://commons.wikimedia.org/wiki/Commons:Licensing/sk");
|
||||||
urlLicense.put("sl","https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
|
urlLicense.put("sl", "https://commons.wikimedia.org/wiki/Commons:Licensing/sl");
|
||||||
urlLicense.put("sv","https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
|
urlLicense.put("sv", "https://commons.wikimedia.org/wiki/Commons:Licensing/sv");
|
||||||
urlLicense.put("tr","https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
|
urlLicense.put("tr", "https://commons.wikimedia.org/wiki/Commons:Licensing/tr");
|
||||||
urlLicense.put("uk","https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
|
urlLicense.put("uk", "https://commons.wikimedia.org/wiki/Commons:Licensing/uk");
|
||||||
urlLicense.put("ur","https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
|
urlLicense.put("ur", "https://commons.wikimedia.org/wiki/Commons:Licensing/ur");
|
||||||
urlLicense.put("vi","https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
|
urlLicense.put("vi", "https://commons.wikimedia.org/wiki/Commons:Licensing/vi");
|
||||||
urlLicense.put("zh","https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
|
urlLicense.put("zh", "https://commons.wikimedia.org/wiki/Commons:Licensing/zh");
|
||||||
}
|
}
|
||||||
public String getLicenseUrl ( String language){
|
public static String getLicenseUrl ( String language){
|
||||||
if (urlLicense.containsKey(language)) {
|
if (urlLicense.containsKey(language)) {
|
||||||
return urlLicense.get(language);
|
return urlLicense.get(language);
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
|
||||||
|
public class AbstractTextWatcher implements TextWatcher {
|
||||||
|
private final TextChange textChange;
|
||||||
|
|
||||||
|
public AbstractTextWatcher(@NonNull TextChange textChange) {
|
||||||
|
this.textChange = textChange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
|
textChange.onTextChanged(s.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(Editable s) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface TextChange {
|
||||||
|
void onTextChanged(String value);
|
||||||
|
}
|
||||||
|
}
|
||||||
41
app/src/main/java/fr/free/nrw/commons/utils/BiMap.java
Normal file
41
app/src/main/java/fr/free/nrw/commons/utils/BiMap.java
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HashMap that can be searched in both the forward and reverse directions.
|
||||||
|
*/
|
||||||
|
public class BiMap<K, V> {
|
||||||
|
|
||||||
|
private HashMap<K, V> map = new HashMap<K, V>();
|
||||||
|
private HashMap<V, K> inversedMap = new HashMap<V, K>();
|
||||||
|
|
||||||
|
public void put(K k, V v) {
|
||||||
|
map.put(k, v);
|
||||||
|
inversedMap.put(v, k);
|
||||||
|
}
|
||||||
|
|
||||||
|
public V get(K k) {
|
||||||
|
return map.get(k);
|
||||||
|
}
|
||||||
|
|
||||||
|
public K getKey(V v) {
|
||||||
|
return inversedMap.get(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<V> getEntrySet(){
|
||||||
|
return inversedMap.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void remove(K k){
|
||||||
|
inversedMap.remove(map.remove(k));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean containsKey(V v){
|
||||||
|
return inversedMap.containsKey(v);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -5,6 +5,7 @@ import android.app.AlertDialog;
|
||||||
import android.app.AlertDialog.Builder;
|
import android.app.AlertDialog.Builder;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.DialogInterface;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
|
|
@ -114,7 +115,49 @@ public class DialogUtil {
|
||||||
.setIcon(iconResourceId).create();
|
.setIcon(iconResourceId).create();
|
||||||
|
|
||||||
return alertDialog;
|
return alertDialog;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showAlertDialog(Activity activity,
|
||||||
|
String title,
|
||||||
|
String message,
|
||||||
|
final Runnable onPositiveBtnClick,
|
||||||
|
final Runnable onNegativeBtnClick) {
|
||||||
|
showAlertDialog(activity,
|
||||||
|
title,
|
||||||
|
message,
|
||||||
|
activity.getString(R.string.no),
|
||||||
|
activity.getString(R.string.yes),
|
||||||
|
onPositiveBtnClick,
|
||||||
|
onNegativeBtnClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showAlertDialog(Activity activity,
|
||||||
|
String title,
|
||||||
|
String message,
|
||||||
|
String positiveButtonText,
|
||||||
|
String negativeButtonText,
|
||||||
|
final Runnable onPositiveBtnClick,
|
||||||
|
final Runnable onNegativeBtnClick) {
|
||||||
|
AlertDialog.Builder builder = new AlertDialog.Builder(activity);
|
||||||
|
builder.setTitle(title);
|
||||||
|
builder.setMessage(message);
|
||||||
|
|
||||||
|
builder.setPositiveButton(positiveButtonText, (dialogInterface, i) -> {
|
||||||
|
dialogInterface.dismiss();
|
||||||
|
if (onPositiveBtnClick != null) {
|
||||||
|
onPositiveBtnClick.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.setNegativeButton(negativeButtonText, (DialogInterface dialogInterface, int i) -> {
|
||||||
|
dialogInterface.dismiss();
|
||||||
|
if (onNegativeBtnClick != null) {
|
||||||
|
onNegativeBtnClick.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
AlertDialog dialog = builder.create();
|
||||||
|
showSafely(activity, dialog);
|
||||||
}
|
}
|
||||||
|
|
||||||
public interface Callback {
|
public interface Callback {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import android.graphics.BitmapRegionDecoder;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.support.annotation.IntDef;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
|
|
||||||
import com.facebook.common.executors.CallerThreadExecutor;
|
import com.facebook.common.executors.CallerThreadExecutor;
|
||||||
|
|
@ -20,6 +21,8 @@ import com.facebook.imagepipeline.request.ImageRequest;
|
||||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.lang.annotation.Retention;
|
||||||
|
import java.lang.annotation.RetentionPolicy;
|
||||||
|
|
||||||
import fr.free.nrw.commons.R;
|
import fr.free.nrw.commons.R;
|
||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
@ -30,20 +33,44 @@ import timber.log.Timber;
|
||||||
|
|
||||||
public class ImageUtils {
|
public class ImageUtils {
|
||||||
|
|
||||||
public enum Result {
|
public static final int IMAGE_DARK = 1;
|
||||||
|
public static final int IMAGE_BLURRY = 1 << 1;
|
||||||
|
public static final int IMAGE_DUPLICATE = 1 << 2;
|
||||||
|
public static final int IMAGE_OK = 0;
|
||||||
|
public static final int IMAGE_KEEP = -1;
|
||||||
|
public static final int IMAGE_WAIT = -2;
|
||||||
|
public static final int EMPTY_TITLE = -3;
|
||||||
|
public static final int FILE_NAME_EXISTS = -4;
|
||||||
|
public static final int NO_CATEGORY_SELECTED = -5;
|
||||||
|
|
||||||
|
@IntDef(
|
||||||
|
flag = true,
|
||||||
|
value = {
|
||||||
IMAGE_DARK,
|
IMAGE_DARK,
|
||||||
IMAGE_OK
|
IMAGE_BLURRY,
|
||||||
|
IMAGE_DUPLICATE,
|
||||||
|
IMAGE_OK,
|
||||||
|
IMAGE_KEEP,
|
||||||
|
IMAGE_WAIT,
|
||||||
|
EMPTY_TITLE,
|
||||||
|
FILE_NAME_EXISTS,
|
||||||
|
NO_CATEGORY_SELECTED
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@Retention(RetentionPolicy.SOURCE)
|
||||||
|
public @interface Result {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
|
* @param bitmapRegionDecoder BitmapRegionDecoder for the image we wish to process
|
||||||
* @return Result.IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
|
* @return IMAGE_OK if image is neither dark nor blurry or if the input bitmapRegionDecoder provided is null
|
||||||
* Result.IMAGE_DARK if image is too dark
|
* IMAGE_DARK if image is too dark
|
||||||
*/
|
*/
|
||||||
public static Result checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
|
public static @Result
|
||||||
|
int checkIfImageIsTooDark(BitmapRegionDecoder bitmapRegionDecoder) {
|
||||||
if (bitmapRegionDecoder == null) {
|
if (bitmapRegionDecoder == null) {
|
||||||
Timber.e("Expected bitmapRegionDecoder was null");
|
Timber.e("Expected bitmapRegionDecoder was null");
|
||||||
return Result.IMAGE_OK;
|
return IMAGE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
int loadImageHeight = bitmapRegionDecoder.getHeight();
|
int loadImageHeight = bitmapRegionDecoder.getHeight();
|
||||||
|
|
@ -59,10 +86,10 @@ public class ImageUtils {
|
||||||
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
|
Bitmap processBitmap = bitmapRegionDecoder.decodeRegion(rect,null);
|
||||||
|
|
||||||
if (checkIfImageIsDark(processBitmap)) {
|
if (checkIfImageIsDark(processBitmap)) {
|
||||||
return Result.IMAGE_DARK;
|
return IMAGE_DARK;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result.IMAGE_OK;
|
return IMAGE_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -132,8 +159,9 @@ public class ImageUtils {
|
||||||
/**
|
/**
|
||||||
* Downloads the image from the URL and sets it as the phone's wallpaper
|
* Downloads the image from the URL and sets it as the phone's wallpaper
|
||||||
* Fails silently if download or setting wallpaper fails.
|
* Fails silently if download or setting wallpaper fails.
|
||||||
* @param context
|
*
|
||||||
* @param imageUrl
|
* @param context context
|
||||||
|
* @param imageUrl Url of the image
|
||||||
*/
|
*/
|
||||||
public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) {
|
public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) {
|
||||||
Timber.d("Trying to set wallpaper from url %s", imageUrl.toString());
|
Timber.d("Trying to set wallpaper from url %s", imageUrl.toString());
|
||||||
|
|
@ -150,7 +178,7 @@ public class ImageUtils {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||||
if (dataSource.isFinished() && bitmap != null){
|
if (dataSource.isFinished() && bitmap != null) {
|
||||||
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
|
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
|
||||||
setWallpaper(context, Bitmap.createBitmap(bitmap));
|
setWallpaper(context, Bitmap.createBitmap(bitmap));
|
||||||
dataSource.close();
|
dataSource.close();
|
||||||
|
|
@ -173,7 +201,29 @@ public class ImageUtils {
|
||||||
wallpaperManager.setBitmap(bitmap);
|
wallpaperManager.setBitmap(bitmap);
|
||||||
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
|
ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully));
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Timber.e(e,"Error setting wallpaper");
|
Timber.e(e, "Error setting wallpaper");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String getErrorMessageForResult(Context context, @Result int result) {
|
||||||
|
String errorMessage;
|
||||||
|
if (result == ImageUtils.IMAGE_DARK)
|
||||||
|
errorMessage = context.getString(R.string.upload_image_problem_dark);
|
||||||
|
else if (result == ImageUtils.IMAGE_BLURRY)
|
||||||
|
errorMessage = context.getString(R.string.upload_image_problem_blurry);
|
||||||
|
else if (result == ImageUtils.IMAGE_DUPLICATE)
|
||||||
|
errorMessage = context.getString(R.string.upload_image_problem_duplicate);
|
||||||
|
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY))
|
||||||
|
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry);
|
||||||
|
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_DUPLICATE))
|
||||||
|
errorMessage = context.getString(R.string.upload_image_problem_dark_duplicate);
|
||||||
|
else if (result == (ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
|
||||||
|
errorMessage = context.getString(R.string.upload_image_problem_blurry_duplicate);
|
||||||
|
else if (result == (ImageUtils.IMAGE_DARK|ImageUtils.IMAGE_BLURRY|ImageUtils.IMAGE_DUPLICATE))
|
||||||
|
errorMessage = context.getString(R.string.upload_image_problem_dark_blurry_duplicate);
|
||||||
|
else
|
||||||
|
return "";
|
||||||
|
|
||||||
|
return errorMessage;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import android.support.v4.content.ContextCompat;
|
||||||
|
|
||||||
import fr.free.nrw.commons.CommonsApplication;
|
import fr.free.nrw.commons.CommonsApplication;
|
||||||
|
|
||||||
|
|
||||||
public class PermissionUtils {
|
public class PermissionUtils {
|
||||||
|
|
||||||
public static final int CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST = 100;
|
public static final int CAMERA_PERMISSION_FROM_CONTRIBUTION_LIST = 100;
|
||||||
|
|
|
||||||
|
|
@ -12,4 +12,8 @@ public class StringUtils {
|
||||||
return Html.fromHtml(source).toString();
|
return Html.fromHtml(source).toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean isNullOrWhiteSpace(String value) {
|
||||||
|
return value == null || value.trim().isEmpty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.utils;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.support.annotation.StringRes;
|
||||||
import android.support.design.widget.Snackbar;
|
import android.support.design.widget.Snackbar;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
@ -32,6 +33,30 @@ public class ViewUtil {
|
||||||
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show());
|
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void showLongToast(Context context, @StringRes int stringResourceId) {
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_LONG).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showShortToast(Context context, String text) {
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void showShortToast(Context context, @StringRes int stringResourceId) {
|
||||||
|
if (context == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, context.getString(stringResourceId), Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
public static boolean isPortrait(Context context) {
|
public static boolean isPortrait(Context context) {
|
||||||
Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay();
|
Display orientation = ((Activity)context).getWindowManager().getDefaultDisplay();
|
||||||
if (orientation.getWidth() < orientation.getHeight()){
|
if (orientation.getWidth() < orientation.getHeight()){
|
||||||
|
|
|
||||||
5
app/src/main/res/drawable/ic_error_red_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_error_red_24dp.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#FF0000"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_expand_less_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_expand_less_black_24dp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_expand_less_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_expand_less_white_24dp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M12,8l-6,6 1.41,1.41L12,10.83l4.59,4.58L18,14z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_expand_more_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_expand_more_black_24dp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/ic_expand_more_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_expand_more_white_24dp.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
|
||||||
|
</vector>
|
||||||
74
app/src/main/res/layout/activity_upload.xml
Normal file
74
app/src/main/res/layout/activity_upload.xml
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.v4.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/drawer_layout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/toolbar"
|
||||||
|
layout="@layout/toolbar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<com.github.chrisbanes.photoview.PhotoView
|
||||||
|
android:id="@+id/backgroundImage"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_below="@id/toolbar"
|
||||||
|
android:background="@color/commons_app_blue_dark"
|
||||||
|
app:actualImageScaleType="centerCrop" />
|
||||||
|
|
||||||
|
<android.support.constraint.ConstraintLayout
|
||||||
|
android:id="@+id/activity_upload_cards"
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_below="@id/toolbar"
|
||||||
|
android:gravity="bottom">
|
||||||
|
|
||||||
|
<include layout="@layout/activity_upload_top_card"/>
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/activity_upload_right_card" />
|
||||||
|
|
||||||
|
<ViewFlipper
|
||||||
|
android:id="@+id/view_flipper"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:measureAllChildren="false"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/activity_upload_bottom_card"
|
||||||
|
android:visibility="visible" />
|
||||||
|
|
||||||
|
<include layout="@layout/activity_upload_categories" />
|
||||||
|
|
||||||
|
<include layout="@layout/activity_upload_license" />
|
||||||
|
|
||||||
|
<include layout="@layout/activity_upload_please_wait" />
|
||||||
|
|
||||||
|
</ViewFlipper>
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<android.support.design.widget.NavigationView
|
||||||
|
android:id="@+id/navigation_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_gravity="start"
|
||||||
|
app:headerLayout="@layout/drawer_header"
|
||||||
|
app:menu="@menu/drawer" />
|
||||||
|
|
||||||
|
</android.support.v4.widget.DrawerLayout>
|
||||||
99
app/src/main/res/layout/activity_upload_bottom_card.xml
Normal file
99
app/src/main/res/layout/activity_upload_bottom_card.xml
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/bottom_card"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:elevation="@dimen/cardview_default_elevation"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
tools:showIn="@layout/activity_upload">
|
||||||
|
|
||||||
|
<android.support.constraint.ConstraintLayout
|
||||||
|
android:id="@+id/relativeLayout"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:padding="@dimen/small_gap">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_card_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="@dimen/normal_text"
|
||||||
|
android:textStyle="bold"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:text="Step 1 of 15" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/bottom_card_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="@dimen/subtitle_text"
|
||||||
|
app:layout_constraintTop_toBottomOf="@id/bottom_card_title"
|
||||||
|
tools:text="1st image" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/bottom_card_expand_button"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:rotation="180"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
|
||||||
|
|
||||||
|
<fr.free.nrw.commons.upload.HeightLimitedRecyclerView
|
||||||
|
android:id="@+id/rv_descriptions"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginTop="8dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/bottom_card_previous"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/bottom_card_subtitle"
|
||||||
|
tools:visibility="gone"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/bottom_card_next"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/next"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintRight_toRightOf="parent" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/bottom_card_previous"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:text="@string/previous"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toStartOf="@id/bottom_card_next"
|
||||||
|
app:layout_constraintRight_toLeftOf="@id/bottom_card_next" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/bottom_card_add_desc"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="48dp"
|
||||||
|
android:text="+"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
|
</android.support.constraint.ConstraintLayout>
|
||||||
|
</android.support.v7.widget.CardView>
|
||||||
|
|
||||||
|
|
||||||
127
app/src/main/res/layout/activity_upload_categories.xml
Normal file
127
app/src/main/res/layout/activity_upload_categories.xml
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:showIn="@layout/activity_upload">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categories_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="@dimen/normal_text"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="Step 1 of 15" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/categories_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/tiny_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="@dimen/subtitle_text"
|
||||||
|
android:text="@string/upload_flow_all_images_in_set"
|
||||||
|
android:layout_below="@+id/categories_title"
|
||||||
|
tools:text="(For all images in set)" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" android:id="@+id/category_search_layout"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_below="@id/categories_subtitle">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
android:id="@+id/category_search_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputEditText
|
||||||
|
android:id="@+id/category_search"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/categories_search_text_hint"
|
||||||
|
android:imeOptions="actionSearch"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1" />
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/categoriesSearchInProgress"
|
||||||
|
style="?android:progressBarStyleSmall"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/tiny_gap"
|
||||||
|
android:layout_marginRight="@dimen/tiny_gap"
|
||||||
|
android:layout_gravity="center_vertical|end"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:indeterminateOnly="true"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/categories"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_above="@+id/button_divider"
|
||||||
|
android:layout_below="@id/category_search_layout" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/button_divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_above="@+id/category_next"
|
||||||
|
android:background="@color/divider_grey" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/category_next"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:text="@string/next" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/category_previous"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:layout_toStartOf="@id/category_next"
|
||||||
|
android:layout_toLeftOf="@id/category_next"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:text="@string/previous" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
|
||||||
116
app/src/main/res/layout/activity_upload_license.xml
Normal file
116
app/src/main/res/layout/activity_upload_license.xml
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
tools:showIn="@layout/activity_upload">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/license_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="@dimen/normal_text"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="Step 1 of 15" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/license_subtitle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/tiny_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="@dimen/subtitle_text"
|
||||||
|
android:text="@string/upload_flow_all_images_in_set"
|
||||||
|
android:layout_below="@+id/license_title"
|
||||||
|
tools:text="(For all images in set)" />
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/license_list"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_below="@id/license_subtitle"
|
||||||
|
tools:visibility="gone"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/share_license_summary"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_below="@id/license_list"
|
||||||
|
android:text="@string/share_license_summary" />
|
||||||
|
|
||||||
|
<fr.free.nrw.commons.ui.widget.HtmlTextView
|
||||||
|
android:id="@+id/media_upload_policy"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_marginBottom="@dimen/standard_gap"
|
||||||
|
android:layout_above="@+id/button_divider"
|
||||||
|
android:gravity="start"
|
||||||
|
android:text="@string/media_upload_policy" />
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:id="@+id/button_divider"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:layout_above="@+id/submit"
|
||||||
|
android:background="@color/divider_grey" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/submit"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="24dp"
|
||||||
|
android:layout_marginRight="24dp"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:text="@string/submit" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/license_previous"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:layout_marginBottom="24dp"
|
||||||
|
android:layout_toStartOf="@id/submit"
|
||||||
|
android:layout_toLeftOf="@id/submit"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:text="@string/previous" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
||||||
|
|
||||||
31
app/src/main/res/layout/activity_upload_please_wait.xml
Normal file
31
app/src/main/res/layout/activity_upload_please_wait.xml
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:gravity="center"
|
||||||
|
android:orientation="vertical"
|
||||||
|
tools:showIn="@layout/activity_upload">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/shareInProgress"
|
||||||
|
style="?android:progressBarStyleLarge"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:indeterminate="true"
|
||||||
|
android:indeterminateOnly="true" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/standard_gap"
|
||||||
|
android:layout_marginLeft="@dimen/standard_gap"
|
||||||
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
|
android:layout_marginEnd="@dimen/standard_gap"
|
||||||
|
android:layout_marginRight="@dimen/standard_gap"
|
||||||
|
android:gravity="center"
|
||||||
|
android:text="Receiving shared content,\nthis may take a moment or two." />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
45
app/src/main/res/layout/activity_upload_right_card.xml
Normal file
45
app/src/main/res/layout/activity_upload_right_card.xml
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/right_card"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/view_flipper"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/top_card"
|
||||||
|
tools:showIn="@layout/activity_upload"
|
||||||
|
tools:ignore="UnusedAttribute">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/right_card_content"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/right_card_expand_button"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="16dp"
|
||||||
|
android:layout_height="16dp"
|
||||||
|
android:layout_margin="8dp"
|
||||||
|
android:rotation="90"
|
||||||
|
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/right_card_map_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:visibility="visible"
|
||||||
|
app:srcCompat="@drawable/ic_map_white_24dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
</android.support.v7.widget.CardView>
|
||||||
|
|
||||||
66
app/src/main/res/layout/activity_upload_top_card.xml
Normal file
66
app/src/main/res/layout/activity_upload_top_card.xml
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.v7.widget.CardView xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/top_card"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="16dp"
|
||||||
|
android:layout_marginLeft="8dp"
|
||||||
|
android:layout_marginRight="8dp"
|
||||||
|
android:layout_marginBottom="8dp"
|
||||||
|
android:layout_marginEnd="8dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
android:elevation="@dimen/cardview_default_elevation"
|
||||||
|
tools:ignore="UnusedAttribute"
|
||||||
|
tools:showIn="@layout/activity_upload">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/top_card_title"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/small_gap"
|
||||||
|
android:layout_marginTop="@dimen/small_gap"
|
||||||
|
android:layout_marginEnd="@dimen/small_gap"
|
||||||
|
android:layout_marginBottom="@dimen/small_gap"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:textSize="@dimen/normal_text"
|
||||||
|
android:textStyle="bold"
|
||||||
|
tools:text="4 Uploads" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/top_card_expand_button"
|
||||||
|
style="@style/Widget.AppCompat.Button.Borderless"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_marginStart="@dimen/small_gap"
|
||||||
|
android:layout_marginTop="@dimen/small_gap"
|
||||||
|
android:layout_marginEnd="@dimen/small_gap"
|
||||||
|
android:layout_marginBottom="@dimen/small_gap"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:padding="0dp"
|
||||||
|
app:srcCompat="@drawable/ic_expand_less_black_24dp" />
|
||||||
|
|
||||||
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:id="@+id/top_card_thumbnails"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="90dp"
|
||||||
|
android:layout_marginBottom="@dimen/small_gap"
|
||||||
|
android:layout_below="@id/top_card_title" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
</android.support.v7.widget.CardView>
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:background="?attr/fragmentCategorisationBackground"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusableInTouchMode="true"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingBottom="@dimen/small_gap"
|
|
||||||
android:paddingEnd="@dimen/standard_gap"
|
|
||||||
android:paddingLeft="@dimen/standard_gap"
|
|
||||||
android:paddingRight="@dimen/standard_gap"
|
|
||||||
android:paddingStart="@dimen/standard_gap"
|
|
||||||
android:paddingTop="@dimen/small_gap"
|
|
||||||
android:theme="@style/DarkAppTheme">
|
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clickable="true"
|
|
||||||
android:focusableInTouchMode="true">
|
|
||||||
|
|
||||||
<EditText
|
|
||||||
android:id="@+id/categoriesSearchBox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:hint="@string/categories_search_text_hint"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:gravity="left"
|
|
||||||
android:inputType="textCapWords"
|
|
||||||
android:imeOptions="flagNoExtractUi"/>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/categoriesSearchInProgress"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:indeterminateOnly="true"
|
|
||||||
android:layout_marginRight="@dimen/tiny_gap"
|
|
||||||
android:layout_marginEnd="@dimen/tiny_gap"
|
|
||||||
android:layout_gravity="center_vertical|right"
|
|
||||||
style="?android:progressBarStyleSmall"
|
|
||||||
android:visibility="gone"
|
|
||||||
/>
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/categoriesNotFound"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:gravity="center"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/categoriesExplanation"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="@dimen/huge_gap"
|
|
||||||
android:focusable="true"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="@string/categories_skip_explanation"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
|
||||||
android:id="@+id/categoriesListBox"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:fadingEdge="none" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
@ -6,13 +6,13 @@
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:focusableInTouchMode="true"
|
android:focusableInTouchMode="true"
|
||||||
|
android:nestedScrollingEnabled="false"
|
||||||
android:paddingBottom="@dimen/small_gap"
|
android:paddingBottom="@dimen/small_gap"
|
||||||
android:paddingEnd="@dimen/standard_gap"
|
android:paddingEnd="@dimen/standard_gap"
|
||||||
android:paddingLeft="@dimen/standard_gap"
|
android:paddingLeft="@dimen/standard_gap"
|
||||||
android:paddingRight="@dimen/standard_gap"
|
android:paddingRight="@dimen/standard_gap"
|
||||||
android:paddingStart="@dimen/standard_gap"
|
android:paddingStart="@dimen/standard_gap"
|
||||||
android:paddingTop="@dimen/small_gap"
|
android:paddingTop="@dimen/small_gap"
|
||||||
android:nestedScrollingEnabled="false"
|
|
||||||
android:theme="@style/DarkAppTheme">
|
android:theme="@style/DarkAppTheme">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
@ -31,11 +31,11 @@
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:drawableEnd="@drawable/mapbox_info_icon_default"
|
android:drawableEnd="@drawable/mapbox_info_icon_default"
|
||||||
android:drawableRight="@drawable/mapbox_info_icon_default"
|
android:drawableRight="@drawable/mapbox_info_icon_default"
|
||||||
android:maxLines="1"
|
|
||||||
android:maxLength="80"
|
|
||||||
android:hint="@string/share_title_hint"
|
android:hint="@string/share_title_hint"
|
||||||
android:imeOptions="flagNoExtractUi"
|
android:imeOptions="flagNoExtractUi"
|
||||||
android:inputType="text"
|
android:inputType="text"
|
||||||
|
android:maxLength="80"
|
||||||
|
android:maxLines="1"
|
||||||
android:scrollHorizontally="false" />
|
android:scrollHorizontally="false" />
|
||||||
</android.support.design.widget.TextInputLayout>
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
|
@ -43,27 +43,30 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<android.support.v7.widget.RecyclerView
|
<android.support.v7.widget.RecyclerView
|
||||||
android:id="@+id/rv_descriptions"
|
android:id="@+id/rv_descriptions"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_marginTop="4dp"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:id="@+id/ll_add_description"
|
android:id="@+id/ll_add_description"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_gravity="right"
|
|
||||||
android:gravity="right"
|
|
||||||
android:padding="10dp"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
>
|
android:layout_gravity="right"
|
||||||
|
android:layout_marginTop="4dp"
|
||||||
|
android:gravity="right"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:padding="10dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
style="@style/TextAppearance.AppCompat.Body1"
|
style="@style/TextAppearance.AppCompat.Body1"
|
||||||
android:text="@string/add_description"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"
|
||||||
|
android:text="@string/add_description" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<Spinner
|
<Spinner
|
||||||
android:id="@+id/licenseSpinner"
|
android:id="@+id/licenseSpinner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
@ -82,10 +85,10 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginTop="@dimen/standard_gap"
|
android:layout_marginTop="@dimen/standard_gap"
|
||||||
android:gravity="center"
|
|
||||||
android:clickable="true"
|
android:clickable="true"
|
||||||
android:textColorLink="@color/button_blue"
|
android:gravity="center"
|
||||||
android:text="@string/share_license_summary" />
|
android:text="@string/share_license_summary"
|
||||||
|
android:textColorLink="@color/button_blue" />
|
||||||
|
|
||||||
<fr.free.nrw.commons.ui.widget.HtmlTextView
|
<fr.free.nrw.commons.ui.widget.HtmlTextView
|
||||||
android:id="@+id/media_upload_policy"
|
android:id="@+id/media_upload_policy"
|
||||||
|
|
|
||||||
37
app/src/main/res/layout/item_upload_thumbnail.xml
Normal file
37
app/src/main/res/layout/item_upload_thumbnail.xml
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<LinearLayout xmlns:fresco="http://schemas.android.com/apk/res-auto"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<android.support.v4.widget.Space
|
||||||
|
android:id="@+id/left_space"
|
||||||
|
android:layout_width="8dp"
|
||||||
|
android:layout_height="90dp" />
|
||||||
|
|
||||||
|
<com.facebook.drawee.view.SimpleDraweeView
|
||||||
|
android:id="@+id/thumbnail"
|
||||||
|
android:layout_width="90dp"
|
||||||
|
android:layout_height="90dp"
|
||||||
|
fresco:actualImageScaleType="fitCenter" />
|
||||||
|
|
||||||
|
<android.support.v4.widget.Space
|
||||||
|
android:id="@+id/right_space"
|
||||||
|
android:layout_width="8dp"
|
||||||
|
android:layout_height="90dp" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/error"
|
||||||
|
android:layout_width="24dp"
|
||||||
|
android:layout_height="24dp"
|
||||||
|
android:layout_gravity="end"
|
||||||
|
android:visibility="gone"
|
||||||
|
app:srcCompat="@drawable/ic_error_red_24dp" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
@ -7,6 +7,6 @@
|
||||||
android:checked="false"
|
android:checked="false"
|
||||||
android:gravity="center_vertical"
|
android:gravity="center_vertical"
|
||||||
android:padding="@dimen/tiny_gap"
|
android:padding="@dimen/tiny_gap"
|
||||||
android:theme="@style/DarkAppTheme">
|
android:textColor="@color/primaryDarkColor">
|
||||||
|
|
||||||
</CheckedTextView>
|
</CheckedTextView>
|
||||||
10
app/src/main/res/layout/layout_upload_categories_item.xml
Normal file
10
app/src/main/res/layout/layout_upload_categories_item.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<CheckBox xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/tvName"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checkMark="?android:attr/textCheckMark"
|
||||||
|
android:checked="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:padding="@dimen/tiny_gap"
|
||||||
|
android:textColor="@color/primaryDarkColor" />
|
||||||
|
|
@ -1,35 +1,34 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:weightSum="10">
|
android:weightSum="8">
|
||||||
|
|
||||||
<android.support.v7.widget.AppCompatSpinner
|
<android.support.v7.widget.AppCompatSpinner
|
||||||
android:id="@+id/spinner_description_languages"
|
android:id="@+id/spinner_description_languages"
|
||||||
android:layout_width="0dp"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
android:layout_weight="0"
|
||||||
android:layout_weight="3"
|
android:minWidth="1dp"
|
||||||
tools:listitem="@layout/row_item_languages_spinner"
|
android:padding="0dp"
|
||||||
android:spinnerMode="dialog"></android.support.v7.widget.AppCompatSpinner>
|
android:spinnerMode="dialog" />
|
||||||
|
|
||||||
<android.support.design.widget.TextInputLayout
|
<android.support.design.widget.TextInputLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="217dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="7">
|
android:layout_weight="8">
|
||||||
|
|
||||||
<EditText
|
<EditText
|
||||||
android:id="@+id/et_description_text"
|
android:id="@+id/description_item_edit_text"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:drawableEnd="@drawable/mapbox_info_icon_default"
|
android:drawableEnd="@drawable/mapbox_info_icon_default"
|
||||||
android:drawableRight="@drawable/mapbox_info_icon_default"
|
android:drawableRight="@drawable/mapbox_info_icon_default"
|
||||||
android:hint="@string/share_description_hint"
|
android:hint="@string/share_description_hint"
|
||||||
android:imeOptions="flagNoExtractUi"
|
android:imeOptions="flagNoExtractUi"
|
||||||
android:inputType="textMultiLine"/>
|
android:inputType="textMultiLine" />
|
||||||
</android.support.design.widget.TextInputLayout>
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
19
app/src/main/res/layout/row_item_title.xml
Normal file
19
app/src/main/res/layout/row_item_title.xml
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<android.support.design.widget.TextInputLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/image_title_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
tools:showIn="@layout/activity_upload">
|
||||||
|
|
||||||
|
<android.support.design.widget.TextInputEditText
|
||||||
|
android:id="@+id/description_item_edit_text"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:hint="@string/share_title_hint"
|
||||||
|
android:imeOptions="actionNext"
|
||||||
|
android:inputType="text"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:nextFocusForward="@+id/image_description" />
|
||||||
|
</android.support.design.widget.TextInputLayout>
|
||||||
|
|
@ -240,8 +240,8 @@
|
||||||
<string name="error_while_cache">خطأ أثناء تخزين الصور</string>
|
<string name="error_while_cache">خطأ أثناء تخزين الصور</string>
|
||||||
<string name="title_info">عنوان وصفي فريد للملف، والذي سيكون بمثابة اسم الملف، يمكنك استخدام لغة واضحة مع مسافات، لا تقم بتضمين امتداد الملف</string>
|
<string name="title_info">عنوان وصفي فريد للملف، والذي سيكون بمثابة اسم الملف، يمكنك استخدام لغة واضحة مع مسافات، لا تقم بتضمين امتداد الملف</string>
|
||||||
<string name="description_info">يُرجَى وصف الوسائط قدر الإمكان: أين تم التقاطها؟ ما تظهر؟ ما هو السياق؟ يُرجَى وصف الأشياء أو الأشخاص، اكشف المعلومات التي لا يمكن تخمينها بسهولة، على سبيل المثال الوقت في اليوم إذا كان منظرا طبيعيا، إذا أظهرت الوسائط شيئا غير عادي، فيُرجَى توضيح ما يجعله غير عادي.</string>
|
<string name="description_info">يُرجَى وصف الوسائط قدر الإمكان: أين تم التقاطها؟ ما تظهر؟ ما هو السياق؟ يُرجَى وصف الأشياء أو الأشخاص، اكشف المعلومات التي لا يمكن تخمينها بسهولة، على سبيل المثال الوقت في اليوم إذا كان منظرا طبيعيا، إذا أظهرت الوسائط شيئا غير عادي، فيُرجَى توضيح ما يجعله غير عادي.</string>
|
||||||
<string name="upload_image_too_dark">هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.</string>
|
<string name="upload_image_problem_dark">هذه الصورة مظلمة للغاية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.</string>
|
||||||
<string name="upload_image_blurry">هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.</string>
|
<string name="upload_image_problem_blurry">هذه الصورة ضبابية، هل أنت متأكد من رغبتك في رفعها؟ ويكيميديا كومنز للصور ذات القيمة الموسوعية فقط.</string>
|
||||||
<string name="give_permission">إعطاء السماح</string>
|
<string name="give_permission">إعطاء السماح</string>
|
||||||
<string name="use_external_storage">استخدم تخزينا خارجيا</string>
|
<string name="use_external_storage">استخدم تخزينا خارجيا</string>
|
||||||
<string name="use_external_storage_summary">احفظ الصور الملتقطة بالكاميرا داخل التطبيق على جهازك</string>
|
<string name="use_external_storage_summary">احفظ الصور الملتقطة بالكاميرا داخل التطبيق على جهازك</string>
|
||||||
|
|
|
||||||
|
|
@ -168,7 +168,7 @@
|
||||||
<string name="title_activity_nearby">Llugares cercanos</string>
|
<string name="title_activity_nearby">Llugares cercanos</string>
|
||||||
<string name="no_nearby">Nun s\'alcontraron llugares cercanos</string>
|
<string name="no_nearby">Nun s\'alcontraron llugares cercanos</string>
|
||||||
<string name="warning">Avisu</string>
|
<string name="warning">Avisu</string>
|
||||||
<string name="file_exists">Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?</string>
|
<string name="upload_image_problem_duplicate">Esti ficheru yá esiste\'n Commons. ¿Confirmes que quies siguir?</string>
|
||||||
<string name="yes">Sí</string>
|
<string name="yes">Sí</string>
|
||||||
<string name="no">Non</string>
|
<string name="no">Non</string>
|
||||||
<string name="media_detail_title">Títulu</string>
|
<string name="media_detail_title">Títulu</string>
|
||||||
|
|
@ -230,8 +230,8 @@
|
||||||
<string name="error_while_cache">Error al poner les fotos na caché</string>
|
<string name="error_while_cache">Error al poner les fotos na caché</string>
|
||||||
<string name="title_info">Un títulu descriptivu únicu pal ficheru, que sirvirá para da-y nome al mesmu. Puede usase llinguaxe normal con espacios. Nun incluyas la estensión del ficheru</string>
|
<string name="title_info">Un títulu descriptivu únicu pal ficheru, que sirvirá para da-y nome al mesmu. Puede usase llinguaxe normal con espacios. Nun incluyas la estensión del ficheru</string>
|
||||||
<string name="description_info">Por favor, describi l\'elementu multimedia tantu como sía posible: ¿ónde se tomó?, ¿qué amuesa?, ¿cuál ye\'l contestu? Por favor, describi los oxetos o persones. Revela la información que nun pueda aldovinase de mou cenciellu, por casu el momentu del día si ye un paisaxe. Si\'l mediu amuesa daqué desacostumao, esplica qué lo fai raro.</string>
|
<string name="description_info">Por favor, describi l\'elementu multimedia tantu como sía posible: ¿ónde se tomó?, ¿qué amuesa?, ¿cuál ye\'l contestu? Por favor, describi los oxetos o persones. Revela la información que nun pueda aldovinase de mou cenciellu, por casu el momentu del día si ye un paisaxe. Si\'l mediu amuesa daqué desacostumao, esplica qué lo fai raro.</string>
|
||||||
<string name="upload_image_too_dark">Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
|
<string name="upload_image_problem_dark">Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
|
||||||
<string name="upload_image_blurry">Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
|
<string name="upload_image_problem_blurry">Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu.</string>
|
||||||
<string name="give_permission">Dar permisu</string>
|
<string name="give_permission">Dar permisu</string>
|
||||||
<string name="use_external_storage">Usar almacenamientu esternu</string>
|
<string name="use_external_storage">Usar almacenamientu esternu</string>
|
||||||
<string name="use_external_storage_summary">Guardar nel preséu les imaxes tomaes cola cámara de la app</string>
|
<string name="use_external_storage_summary">Guardar nel preséu les imaxes tomaes cola cámara de la app</string>
|
||||||
|
|
|
||||||
|
|
@ -169,7 +169,7 @@
|
||||||
<string name="title_activity_nearby">Mesta u blizini</string>
|
<string name="title_activity_nearby">Mesta u blizini</string>
|
||||||
<string name="no_nearby">Nisu pronađena obližnja mesta</string>
|
<string name="no_nearby">Nisu pronađena obližnja mesta</string>
|
||||||
<string name="warning">Upozorenje</string>
|
<string name="warning">Upozorenje</string>
|
||||||
<string name="file_exists">Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?</string>
|
<string name="upload_image_problem_duplicate">Ova datoteka je već dostupna na Ostavi. Da li ste sigurni da želite da nastavite?</string>
|
||||||
<string name="yes">Da</string>
|
<string name="yes">Da</string>
|
||||||
<string name="no">Ne</string>
|
<string name="no">Ne</string>
|
||||||
<string name="media_detail_title">Naslov</string>
|
<string name="media_detail_title">Naslov</string>
|
||||||
|
|
|
||||||
|
|
@ -157,7 +157,7 @@
|
||||||
<string name="location_permission_rationale">Мотлаҡ булмаған рөхсәт: категория тәҡдиме өсөн ошо урынды алыу</string>
|
<string name="location_permission_rationale">Мотлаҡ булмаған рөхсәт: категория тәҡдиме өсөн ошо урынды алыу</string>
|
||||||
<string name="title_activity_nearby">Яҡындағы урындар</string>
|
<string name="title_activity_nearby">Яҡындағы урындар</string>
|
||||||
<string name="no_nearby">Яҡындағы урындар табылманы</string>
|
<string name="no_nearby">Яҡындағы урындар табылманы</string>
|
||||||
<string name="file_exists">Был файл Викискладта бар. Дауам итергә ризаһыңмы?</string>
|
<string name="upload_image_problem_duplicate">Был файл Викискладта бар. Дауам итергә ризаһыңмы?</string>
|
||||||
<string name="media_detail_title">Атама</string>
|
<string name="media_detail_title">Атама</string>
|
||||||
<string name="media_detail_media_title">Мәғлүмәт йөрөтөүсенең атамаһы</string>
|
<string name="media_detail_media_title">Мәғлүмәт йөрөтөүсенең атамаһы</string>
|
||||||
<string name="media_detail_description_explanation">Мәғлүмәт йөрөтөүсене һүрәтләү ошонда яҙыла.Уның ярайһы уҡ оҙон, хатта бер-нисә юлға һуҙылып китеүе лә бар. Шулай булһа ла ул бик матур күренер тип уйлайбыҙ.</string>
|
<string name="media_detail_description_explanation">Мәғлүмәт йөрөтөүсене һүрәтләү ошонда яҙыла.Уның ярайһы уҡ оҙон, хатта бер-нисә юлға һуҙылып китеүе лә бар. Шулай булһа ла ул бик матур күренер тип уйлайбыҙ.</string>
|
||||||
|
|
@ -204,8 +204,8 @@
|
||||||
<string name="error_while_cache">Рәсемде кэшлағандағы хата</string>
|
<string name="error_while_cache">Рәсемде кэшлағандағы хата</string>
|
||||||
<string name="title_info">Файлдың исеме булараҡ һаҡланасаҡ үҙенсәлекле һәртәләү. Тәбиғи телегеҙҙе, һүҙҙәр араһын айырып, ҡулланырға була. Зинһар, файл киңәйтеүҙәрен күрһәтмәгеҙ.</string>
|
<string name="title_info">Файлдың исеме булараҡ һаҡланасаҡ үҙенсәлекле һәртәләү. Тәбиғи телегеҙҙе, һүҙҙәр араһын айырып, ҡулланырға була. Зинһар, файл киңәйтеүҙәрен күрһәтмәгеҙ.</string>
|
||||||
<string name="description_info">Зинһар, тейәләсәк файлды тәфсирләп һүрәтлә:ҡайҙа төшөрөлгән? нимә һәрәтләнә? һүрәт нимәне аңлата? Рәсемдәге кешеләр йәки объекттарҙы ла һүрәтлә. Һүрәткә ҡарап ҡына белеп булмаған мәғлүмәттәрҙе өҫтә: мәҫәлән, тәүлектең ниндәй мәлендә, ҡасан төшөрөлгән был файл. Әгәр ғәҙәти булмаған әйбер төшөрөлһә, уның нимәһе шаҡ ҡатырғанын аңлат.</string>
|
<string name="description_info">Зинһар, тейәләсәк файлды тәфсирләп һүрәтлә:ҡайҙа төшөрөлгән? нимә һәрәтләнә? һүрәт нимәне аңлата? Рәсемдәге кешеләр йәки объекттарҙы ла һүрәтлә. Һүрәткә ҡарап ҡына белеп булмаған мәғлүмәттәрҙе өҫтә: мәҫәлән, тәүлектең ниндәй мәлендә, ҡасан төшөрөлгән был файл. Әгәр ғәҙәти булмаған әйбер төшөрөлһә, уның нимәһе шаҡ ҡатырғанын аңлат.</string>
|
||||||
<string name="upload_image_too_dark">Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
|
<string name="upload_image_problem_dark">Был рәсем бик ҡараңғы күренә. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
|
||||||
<string name="upload_image_blurry">Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
|
<string name="upload_image_problem_blurry">Был рәсем асыҡ түгел. Тейәргәме? Викискладта энциклопедик йәһәттән ҡиммәте булған фоторәсемдәр генә ҡәҙерле.</string>
|
||||||
<string name="give_permission">Рөхсәт бирәм</string>
|
<string name="give_permission">Рөхсәт бирәм</string>
|
||||||
<string name="use_external_storage">Тышҡы һаҡлағысты ҡуллан</string>
|
<string name="use_external_storage">Тышҡы һаҡлағысты ҡуллан</string>
|
||||||
<string name="use_external_storage_summary">Ҡулайламаның камераһы ярҙамында төшөрөлгән һүрәттәрҙе һаҡлау</string>
|
<string name="use_external_storage_summary">Ҡулайламаның камераһы ярҙамында төшөрөлгән һүрәттәрҙе һаҡлау</string>
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
<string name="menu_refresh">Обновяване</string>
|
<string name="menu_refresh">Обновяване</string>
|
||||||
<string name="ok">Добре</string>
|
<string name="ok">Добре</string>
|
||||||
<string name="warning">Предупреждение</string>
|
<string name="warning">Предупреждение</string>
|
||||||
<string name="file_exists">Този файл вече съществува в Общомедия. Наистина ли искате да продължите?</string>
|
<string name="upload_image_problem_duplicate">Този файл вече съществува в Общомедия. Наистина ли искате да продължите?</string>
|
||||||
<string name="yes">Да</string>
|
<string name="yes">Да</string>
|
||||||
<string name="no">Не</string>
|
<string name="no">Не</string>
|
||||||
<string name="media_detail_title">Заглавие</string>
|
<string name="media_detail_title">Заглавие</string>
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@
|
||||||
<string name="title_activity_nearby">কাছাকাছি স্থান</string>
|
<string name="title_activity_nearby">কাছাকাছি স্থান</string>
|
||||||
<string name="no_nearby">কাছাকাছি কোন স্থান পাওয়া যায়নি</string>
|
<string name="no_nearby">কাছাকাছি কোন স্থান পাওয়া যায়নি</string>
|
||||||
<string name="warning">সতর্কীকরণ</string>
|
<string name="warning">সতর্কীকরণ</string>
|
||||||
<string name="file_exists">এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?</string>
|
<string name="upload_image_problem_duplicate">এই ফাইলটি ইতিমধ্যে কমন্সে বিদ্যমান। আপনি কি নিশ্চিত আপনি সামনে এগুতে চান?</string>
|
||||||
<string name="yes">হ্যাঁ</string>
|
<string name="yes">হ্যাঁ</string>
|
||||||
<string name="no">না</string>
|
<string name="no">না</string>
|
||||||
<string name="media_detail_title">শিরোনাম</string>
|
<string name="media_detail_title">শিরোনাম</string>
|
||||||
|
|
@ -239,8 +239,8 @@
|
||||||
<string name="error_while_cache">ছবি আনার সময় ত্রুটি</string>
|
<string name="error_while_cache">ছবি আনার সময় ত্রুটি</string>
|
||||||
<string name="title_info">ফাইলের একটি স্বতন্ত্র বর্ণনামূলক নাম যা ফাইলের নাম হিসাবে কাজ করবে। অাপনি সাধারণ ভাষা ব্যবহার করতে পারেন শূন্যস্থানসহ। ফাইলের এক্সটেনশন যুক্ত করবেন না।</string>
|
<string name="title_info">ফাইলের একটি স্বতন্ত্র বর্ণনামূলক নাম যা ফাইলের নাম হিসাবে কাজ করবে। অাপনি সাধারণ ভাষা ব্যবহার করতে পারেন শূন্যস্থানসহ। ফাইলের এক্সটেনশন যুক্ত করবেন না।</string>
|
||||||
<string name="description_info">যতটা সম্ভব মিডিয়াটি বর্ণনা করুন: এটি কোথায় ধারণ করা হয়েছিল? এটি কি প্রদর্শন করে? এটির প্রসঙ্গ কি? ধারণকৃত বস্তু অথবা ব্যক্তির বর্ণনা করুন। সহজে অনুমান করা যায়না সেরকম তথ্য উদঘাটন করুন, উদাহরণস্বরূপ, যদি ল্যান্ডস্কেপ হয় তাহলে দিবসকালের সময় দিন।</string>
|
<string name="description_info">যতটা সম্ভব মিডিয়াটি বর্ণনা করুন: এটি কোথায় ধারণ করা হয়েছিল? এটি কি প্রদর্শন করে? এটির প্রসঙ্গ কি? ধারণকৃত বস্তু অথবা ব্যক্তির বর্ণনা করুন। সহজে অনুমান করা যায়না সেরকম তথ্য উদঘাটন করুন, উদাহরণস্বরূপ, যদি ল্যান্ডস্কেপ হয় তাহলে দিবসকালের সময় দিন।</string>
|
||||||
<string name="upload_image_too_dark">এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
|
<string name="upload_image_problem_dark">এই ছবিটি খুবই অন্ধকারময়, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
|
||||||
<string name="upload_image_blurry">এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
|
<string name="upload_image_problem_blurry">এই ছবিটি অস্পষ্ট, আপনি কি এটি আপলোড করতে চান? উইকিমিডিয়া কমন্স শুধুমাত্র বিশ্বকোষীয় মানের ছবির জন্য।</string>
|
||||||
<string name="give_permission">অনুমতি দিন</string>
|
<string name="give_permission">অনুমতি দিন</string>
|
||||||
<string name="use_external_storage">বাহ্যিক সঞ্চয়স্থান ব্যবহার করুন</string>
|
<string name="use_external_storage">বাহ্যিক সঞ্চয়স্থান ব্যবহার করুন</string>
|
||||||
<string name="use_external_storage_summary">অাপনার ডিভাইসের নিজস্ব ক্যামেরায় ধারণকৃত ছবি সংরক্ষণ করুন</string>
|
<string name="use_external_storage_summary">অাপনার ডিভাইসের নিজস্ব ক্যামেরায় ধারণকৃত ছবি সংরক্ষণ করুন</string>
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@
|
||||||
<string name="title_activity_nearby">Lec\'hioù nes</string>
|
<string name="title_activity_nearby">Lec\'hioù nes</string>
|
||||||
<string name="no_nearby">N\'eus bet kavet netra tostik</string>
|
<string name="no_nearby">N\'eus bet kavet netra tostik</string>
|
||||||
<string name="warning">Diwallit</string>
|
<string name="warning">Diwallit</string>
|
||||||
<string name="file_exists">Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?</string>
|
<string name="upload_image_problem_duplicate">Emañ ar restr-mañ war Commons c\'hoazh. Ha sur oc\'h e fell deoc\'h kenderc\'hel ?</string>
|
||||||
<string name="yes">Ya</string>
|
<string name="yes">Ya</string>
|
||||||
<string name="no">Ket</string>
|
<string name="no">Ket</string>
|
||||||
<string name="media_detail_title">Titl</string>
|
<string name="media_detail_title">Titl</string>
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@
|
||||||
<string name="title_activity_nearby">Mjesta u blizini</string>
|
<string name="title_activity_nearby">Mjesta u blizini</string>
|
||||||
<string name="no_nearby">Nema okolnih mjesta</string>
|
<string name="no_nearby">Nema okolnih mjesta</string>
|
||||||
<string name="warning">Upozorenje</string>
|
<string name="warning">Upozorenje</string>
|
||||||
<string name="file_exists">Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?</string>
|
<string name="upload_image_problem_duplicate">Ova datoteka već postoji na Commonsu. Jeste li sigurni da želite nastaviti?</string>
|
||||||
<string name="yes">Da</string>
|
<string name="yes">Da</string>
|
||||||
<string name="no">Ne</string>
|
<string name="no">Ne</string>
|
||||||
<string name="media_detail_title">Naslov</string>
|
<string name="media_detail_title">Naslov</string>
|
||||||
|
|
|
||||||
|
|
@ -139,7 +139,7 @@
|
||||||
<string name="title_activity_nearby">Llocs propers</string>
|
<string name="title_activity_nearby">Llocs propers</string>
|
||||||
<string name="no_nearby">No s\'han trobat llocs propers</string>
|
<string name="no_nearby">No s\'han trobat llocs propers</string>
|
||||||
<string name="warning">Avís</string>
|
<string name="warning">Avís</string>
|
||||||
<string name="file_exists">El fitxer ja existeix a Commons. Segur que voleu procedir?</string>
|
<string name="upload_image_problem_duplicate">El fitxer ja existeix a Commons. Segur que voleu procedir?</string>
|
||||||
<string name="yes">Sí</string>
|
<string name="yes">Sí</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="media_detail_title">Títol</string>
|
<string name="media_detail_title">Títol</string>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
<string name="ok">باشە</string>
|
<string name="ok">باشە</string>
|
||||||
<string name="title_activity_nearby">شوێنە نزیکەکان</string>
|
<string name="title_activity_nearby">شوێنە نزیکەکان</string>
|
||||||
<string name="warning">ئاگاداری</string>
|
<string name="warning">ئاگاداری</string>
|
||||||
<string name="file_exists">ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟</string>
|
<string name="upload_image_problem_duplicate">ئەم پەڕگەیە لەسەر کۆمنز ھەیە. دڵنیایت کە دەتەوێت بەردەوام بیت؟</string>
|
||||||
<string name="yes">بەڵێ</string>
|
<string name="yes">بەڵێ</string>
|
||||||
<string name="no">نەخێر</string>
|
<string name="no">نەخێر</string>
|
||||||
<string name="media_detail_title">ناونیشان</string>
|
<string name="media_detail_title">ناونیشان</string>
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@
|
||||||
<string name="title_activity_nearby">Místa v okolí</string>
|
<string name="title_activity_nearby">Místa v okolí</string>
|
||||||
<string name="no_nearby">Poblíž nebylo nic nalezeno</string>
|
<string name="no_nearby">Poblíž nebylo nic nalezeno</string>
|
||||||
<string name="warning">Upozornění</string>
|
<string name="warning">Upozornění</string>
|
||||||
<string name="file_exists">Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?</string>
|
<string name="upload_image_problem_duplicate">Tento soubor již na Commons existuje. Jste si jist, že chcete pokračovat?</string>
|
||||||
<string name="yes">Ano</string>
|
<string name="yes">Ano</string>
|
||||||
<string name="no">Ne</string>
|
<string name="no">Ne</string>
|
||||||
<string name="media_detail_title">Název</string>
|
<string name="media_detail_title">Název</string>
|
||||||
|
|
@ -243,8 +243,8 @@
|
||||||
<string name="error_while_cache">Chyba při meziukládání obrázků</string>
|
<string name="error_while_cache">Chyba při meziukládání obrázků</string>
|
||||||
<string name="title_info">Unikátní a popisný název pro daný soubor, který bude sloužit jako název souboru. Můžete použít běžný psaný jazyk s mezerami; nezahrnujte koncovku souboru.</string>
|
<string name="title_info">Unikátní a popisný název pro daný soubor, který bude sloužit jako název souboru. Můžete použít běžný psaný jazyk s mezerami; nezahrnujte koncovku souboru.</string>
|
||||||
<string name="description_info">Popište prosím obrázek, jak jen to je možné: Kde byl pořízen? Co znázorňuje? Jaký je kontext obrázku? Popisujte prosím významné předměty nebo osoby na obrázku a nezapomeňte na informace, které není možné snadno odhadnout ze samotného obrázku, jako je například denní doba, pokud jde o krajinu. Pokud je na obrázku něco neobvyklého, popište, co to dělá neobvyklým.</string>
|
<string name="description_info">Popište prosím obrázek, jak jen to je možné: Kde byl pořízen? Co znázorňuje? Jaký je kontext obrázku? Popisujte prosím významné předměty nebo osoby na obrázku a nezapomeňte na informace, které není možné snadno odhadnout ze samotného obrázku, jako je například denní doba, pokud jde o krajinu. Pokud je na obrázku něco neobvyklého, popište, co to dělá neobvyklým.</string>
|
||||||
<string name="upload_image_too_dark">Tento obrázek je příliš tmavý, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.</string>
|
<string name="upload_image_problem_dark">Tento obrázek je příliš tmavý, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.</string>
|
||||||
<string name="upload_image_blurry">Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.</string>
|
<string name="upload_image_problem_blurry">Tento obrázek je rozmazaný, jste si jist/a, že ho chcete nahrát? Wikimedia Commons slouží jenom pro obrázky s encyklopedickou hodnotou.</string>
|
||||||
<string name="give_permission">Dát povolení</string>
|
<string name="give_permission">Dát povolení</string>
|
||||||
<string name="use_external_storage">Použít externí úložiště</string>
|
<string name="use_external_storage">Použít externí úložiště</string>
|
||||||
<string name="use_external_storage_summary">Uložit obrázky pořízené fotoaparátem, jenž je součástí této aplikace</string>
|
<string name="use_external_storage_summary">Uložit obrázky pořízené fotoaparátem, jenž je součástí této aplikace</string>
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
<string name="title_activity_nearby">Steder i nærheden</string>
|
<string name="title_activity_nearby">Steder i nærheden</string>
|
||||||
<string name="no_nearby">Ingen steder i nærheden fundet</string>
|
<string name="no_nearby">Ingen steder i nærheden fundet</string>
|
||||||
<string name="warning">Advarsel</string>
|
<string name="warning">Advarsel</string>
|
||||||
<string name="file_exists">Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?</string>
|
<string name="upload_image_problem_duplicate">Denne fil findes allerede på Commons. Er du sikker på, at du ønsker at fortsætte?</string>
|
||||||
<string name="yes">Ja</string>
|
<string name="yes">Ja</string>
|
||||||
<string name="no">Nej</string>
|
<string name="no">Nej</string>
|
||||||
<string name="media_detail_title">Titel</string>
|
<string name="media_detail_title">Titel</string>
|
||||||
|
|
@ -235,8 +235,8 @@
|
||||||
<string name="error_while_cache">Fejl under mellemlagring af billeder</string>
|
<string name="error_while_cache">Fejl under mellemlagring af billeder</string>
|
||||||
<string name="title_info">En unik beskrivelse for filen, som vil fungere som et filnavn. Du kan bruge normalt sprog med mellemrum. Udelad filendelsen.</string>
|
<string name="title_info">En unik beskrivelse for filen, som vil fungere som et filnavn. Du kan bruge normalt sprog med mellemrum. Udelad filendelsen.</string>
|
||||||
<string name="description_info">Beskriv mediet så godt som muligt: Hvor blev det taget? Hvad viser det? Hvad er konteksten? Beskriv objekterne eller personerne. Giv information som ikke nemt kan gættes, for eksempel hvornår på dagen billedet blev taget, om det er et landskabsbillede. Om billedet viser noget usædvanligt, forklar hvad som gør det usædvanlig.</string>
|
<string name="description_info">Beskriv mediet så godt som muligt: Hvor blev det taget? Hvad viser det? Hvad er konteksten? Beskriv objekterne eller personerne. Giv information som ikke nemt kan gættes, for eksempel hvornår på dagen billedet blev taget, om det er et landskabsbillede. Om billedet viser noget usædvanligt, forklar hvad som gør det usædvanlig.</string>
|
||||||
<string name="upload_image_too_dark">Billedet er for mørkt. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder encyklopædisk værdi.</string>
|
<string name="upload_image_problem_dark">Billedet er for mørkt. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder encyklopædisk værdi.</string>
|
||||||
<string name="upload_image_blurry">Dette billede er sløret. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder med encyklopædisk værdi.</string>
|
<string name="upload_image_problem_blurry">Dette billede er sløret. Er du sikker på, at du ønsker at overføre det? Wikimedia Commons er kun for billeder med encyklopædisk værdi.</string>
|
||||||
<string name="give_permission">Giv tilladelse</string>
|
<string name="give_permission">Giv tilladelse</string>
|
||||||
<string name="use_external_storage">Brug eksternt lager</string>
|
<string name="use_external_storage">Brug eksternt lager</string>
|
||||||
<string name="use_external_storage_summary">Gem billeder taget med din enheds program på kameraet</string>
|
<string name="use_external_storage_summary">Gem billeder taget med din enheds program på kameraet</string>
|
||||||
|
|
|
||||||
|
|
@ -173,7 +173,7 @@
|
||||||
<string name="title_activity_nearby">Orte in der Nähe</string>
|
<string name="title_activity_nearby">Orte in der Nähe</string>
|
||||||
<string name="no_nearby">Keine Orte in der Nähe gefunden</string>
|
<string name="no_nearby">Keine Orte in der Nähe gefunden</string>
|
||||||
<string name="warning">Warnung</string>
|
<string name="warning">Warnung</string>
|
||||||
<string name="file_exists">Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?</string>
|
<string name="upload_image_problem_duplicate">Diese Datei ist bereits auf Commons vorhanden. Bist du sicher, dass du fortfahren möchtest?</string>
|
||||||
<string name="yes">Ja</string>
|
<string name="yes">Ja</string>
|
||||||
<string name="no">Nein</string>
|
<string name="no">Nein</string>
|
||||||
<string name="media_detail_title">Titel</string>
|
<string name="media_detail_title">Titel</string>
|
||||||
|
|
@ -235,8 +235,8 @@
|
||||||
<string name="error_while_cache">Fehler beim Zwischenspeichern der Bilder</string>
|
<string name="error_while_cache">Fehler beim Zwischenspeichern der Bilder</string>
|
||||||
<string name="title_info">Ein eindeutiger beschreibender Titel für die Datei, der als Dateiname dient. Du kannst Klartext mit Leerzeichen verwenden. Gib nicht die Dateierweiterung mit an.</string>
|
<string name="title_info">Ein eindeutiger beschreibender Titel für die Datei, der als Dateiname dient. Du kannst Klartext mit Leerzeichen verwenden. Gib nicht die Dateierweiterung mit an.</string>
|
||||||
<string name="description_info">Bitte beschreibe das Medium so gut wie möglich: Wo wurde es aufgenommen? Was zeigt es? Was ist der Kontext? Bitte beschreibe die Objekte oder Personen. Zeige Informationen auf, die nicht einfach erraten werden können, zum Beispiel die Tageszeit, falls es eine Landschaft ist. Falls das Medium etwas Ungewöhnliches zeigt, erkläre bitte, was es ungewöhnlich macht.</string>
|
<string name="description_info">Bitte beschreibe das Medium so gut wie möglich: Wo wurde es aufgenommen? Was zeigt es? Was ist der Kontext? Bitte beschreibe die Objekte oder Personen. Zeige Informationen auf, die nicht einfach erraten werden können, zum Beispiel die Tageszeit, falls es eine Landschaft ist. Falls das Medium etwas Ungewöhnliches zeigt, erkläre bitte, was es ungewöhnlich macht.</string>
|
||||||
<string name="upload_image_too_dark">Dieses Bild ist zu dunkel. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.</string>
|
<string name="upload_image_problem_dark">Dieses Bild ist zu dunkel. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.</string>
|
||||||
<string name="upload_image_blurry">Dieses Bild ist verschwommen. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.</string>
|
<string name="upload_image_problem_blurry">Dieses Bild ist verschwommen. Bist du sicher, dass du es hochladen möchtest? Wikimedia Commons ist nur für Bilder mit enzyklopädischem Wert gedacht.</string>
|
||||||
<string name="give_permission">Berechtigung geben</string>
|
<string name="give_permission">Berechtigung geben</string>
|
||||||
<string name="use_external_storage">Externen Speicher verwenden</string>
|
<string name="use_external_storage">Externen Speicher verwenden</string>
|
||||||
<string name="use_external_storage_summary">Mit der In-App-Kamera aufgenommene Bilder auf deinem Gerät speichern</string>
|
<string name="use_external_storage_summary">Mit der In-App-Kamera aufgenommene Bilder auf deinem Gerät speichern</string>
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
<string name="ok">हुन्छ</string>
|
<string name="ok">हुन्छ</string>
|
||||||
<string name="title_activity_nearby">नज्यूकाऽ ठउरअन</string>
|
<string name="title_activity_nearby">नज्यूकाऽ ठउरअन</string>
|
||||||
<string name="warning">चेतावनी</string>
|
<string name="warning">चेतावनी</string>
|
||||||
<string name="file_exists">यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?</string>
|
<string name="upload_image_problem_duplicate">यो फाइल कमन्स मी पैली बठेइ छ। तम पक्का छऽ कि तम ऐतिर जान चाहन्छऽ?</string>
|
||||||
<string name="yes">हो</string>
|
<string name="yes">हो</string>
|
||||||
<string name="no">नाइँ</string>
|
<string name="no">नाइँ</string>
|
||||||
<string name="media_detail_title">शीर्षक</string>
|
<string name="media_detail_title">शीर्षक</string>
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
<string name="title_activity_nearby">Κοντινοί Τόποι</string>
|
<string name="title_activity_nearby">Κοντινοί Τόποι</string>
|
||||||
<string name="no_nearby">Δεν βρέθηκαν τόποι εδώ κοντά</string>
|
<string name="no_nearby">Δεν βρέθηκαν τόποι εδώ κοντά</string>
|
||||||
<string name="warning">Προειδοποίηση</string>
|
<string name="warning">Προειδοποίηση</string>
|
||||||
<string name="file_exists">Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;</string>
|
<string name="upload_image_problem_duplicate">Αυτό το αρχείο υπάρχει ήδη στα Commons. Είστε σίγουρος ότι θέλετε να συνεχίσετε;</string>
|
||||||
<string name="yes">Ναι</string>
|
<string name="yes">Ναι</string>
|
||||||
<string name="no">Όχι</string>
|
<string name="no">Όχι</string>
|
||||||
<string name="media_detail_title">Τίτλος</string>
|
<string name="media_detail_title">Τίτλος</string>
|
||||||
|
|
@ -238,8 +238,8 @@
|
||||||
<string name="error_while_cache">Υπήρξε σφάλμα κατά την σκίαση εικόνων</string>
|
<string name="error_while_cache">Υπήρξε σφάλμα κατά την σκίαση εικόνων</string>
|
||||||
<string name="title_info">Ένας μοναδικός τίτλος περιγραφής του φακέλλου, που θα χρησιμεύσει ως όνομα φακέλλου. Μπορείτε να χρησιμοποιήσετε τις ήδη υπάρχουσες γλώσσες με διαστήματα. Μην συμπεριλάβετε την επέκταση φακέλλου</string>
|
<string name="title_info">Ένας μοναδικός τίτλος περιγραφής του φακέλλου, που θα χρησιμεύσει ως όνομα φακέλλου. Μπορείτε να χρησιμοποιήσετε τις ήδη υπάρχουσες γλώσσες με διαστήματα. Μην συμπεριλάβετε την επέκταση φακέλλου</string>
|
||||||
<string name="description_info">\nΠαρακαλώ περιγράψετε τα μέσα το δυνατό περισσότερο : Πού οδηγήθηκε αυτό; Τι δείχνει; Ποιο είναι το περιεχόμενο του; Παρακαλώ περιγράψετε τα αντικείμενα ή τα πρόσωπα. Αποκαλύψετε πληροφορίες που δεν μπορούν εύκολο να μαντέψει κανείς, για παράδειγμα την ώρα εντός της ημέρας αν πρόκειται για τοπίο. Αν τα μέσα δείξουν κάτι ασύνηθες, παρακαλώ εξηγήστε τι το καθιστά μη συνηθισμένα.</string>
|
<string name="description_info">\nΠαρακαλώ περιγράψετε τα μέσα το δυνατό περισσότερο : Πού οδηγήθηκε αυτό; Τι δείχνει; Ποιο είναι το περιεχόμενο του; Παρακαλώ περιγράψετε τα αντικείμενα ή τα πρόσωπα. Αποκαλύψετε πληροφορίες που δεν μπορούν εύκολο να μαντέψει κανείς, για παράδειγμα την ώρα εντός της ημέρας αν πρόκειται για τοπίο. Αν τα μέσα δείξουν κάτι ασύνηθες, παρακαλώ εξηγήστε τι το καθιστά μη συνηθισμένα.</string>
|
||||||
<string name="upload_image_too_dark">Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
|
<string name="upload_image_problem_dark">Αυτή η εικόνα είναι πολύ σκοτεινή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
|
||||||
<string name="upload_image_blurry">Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
|
<string name="upload_image_problem_blurry">Αυτή η εικόνα είναι θολή, είστε βέβαιοι ότι θέλετε να την ανεβάσετε; Το Wikimedia Commons είναι μόνο για εικόνες με εγκυκλοπαιδική αξία.</string>
|
||||||
<string name="give_permission">Χορηγήστε άδεια</string>
|
<string name="give_permission">Χορηγήστε άδεια</string>
|
||||||
<string name="use_external_storage">Χρησιμοποιήσετε την εξωτερική αποθήκευση</string>
|
<string name="use_external_storage">Χρησιμοποιήσετε την εξωτερική αποθήκευση</string>
|
||||||
<string name="use_external_storage_summary">Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας</string>
|
<string name="use_external_storage_summary">Αποθηκεύσετε εικόνες που παίρνονται στην κάμερα εφαρμογής στην συσκευή σας</string>
|
||||||
|
|
|
||||||
|
|
@ -181,7 +181,7 @@
|
||||||
<string name="title_activity_nearby">Lugares cercanos</string>
|
<string name="title_activity_nearby">Lugares cercanos</string>
|
||||||
<string name="no_nearby">No se encontraron lugares cercanos</string>
|
<string name="no_nearby">No se encontraron lugares cercanos</string>
|
||||||
<string name="warning">Atención</string>
|
<string name="warning">Atención</string>
|
||||||
<string name="file_exists">Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?</string>
|
<string name="upload_image_problem_duplicate">Este archivo ya existe en Commons. ¿Confirmas que quieres continuar?</string>
|
||||||
<string name="yes">Sí</string>
|
<string name="yes">Sí</string>
|
||||||
<string name="no">No</string>
|
<string name="no">No</string>
|
||||||
<string name="media_detail_title">Título</string>
|
<string name="media_detail_title">Título</string>
|
||||||
|
|
@ -243,8 +243,8 @@
|
||||||
<string name="error_while_cache">Error al almacenar imágenes en la antememoria</string>
|
<string name="error_while_cache">Error al almacenar imágenes en la antememoria</string>
|
||||||
<string name="title_info">Un título único descriptivo para el archivo, que servirá como un nombre de archivo. Puede usar un lenguaje claro con espacios. No incluya la extensión del archivo.</string>
|
<string name="title_info">Un título único descriptivo para el archivo, que servirá como un nombre de archivo. Puede usar un lenguaje claro con espacios. No incluya la extensión del archivo.</string>
|
||||||
<string name="description_info">Por favor, describa el elemento multimedia tanto como sea posible: ¿dónde fue tomado?, ¿qué muestra?, ¿cuál es el contexto? Por favor, describa los objetos o personas. Ofrezca la información que no puede ser inferida tan fácilmente, por ejemplo el momento del día si es un paisaje. Si el medio muestra algo inusual, explique qué lo hace insual.</string>
|
<string name="description_info">Por favor, describa el elemento multimedia tanto como sea posible: ¿dónde fue tomado?, ¿qué muestra?, ¿cuál es el contexto? Por favor, describa los objetos o personas. Ofrezca la información que no puede ser inferida tan fácilmente, por ejemplo el momento del día si es un paisaje. Si el medio muestra algo inusual, explique qué lo hace insual.</string>
|
||||||
<string name="upload_image_too_dark">Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
|
<string name="upload_image_problem_dark">Esta imagen es demasiado oscura. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
|
||||||
<string name="upload_image_blurry">Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
|
<string name="upload_image_problem_blurry">Esta imagen se ve borrosa. ¿Confirmas que quieres cargarla? Wikimedia Commons solo acepta imágenes con valor enciclopédico.</string>
|
||||||
<string name="give_permission">Otorgar permiso</string>
|
<string name="give_permission">Otorgar permiso</string>
|
||||||
<string name="use_external_storage">Utilizar almacenamiento externo</string>
|
<string name="use_external_storage">Utilizar almacenamiento externo</string>
|
||||||
<string name="use_external_storage_summary">Guardar en el dispositivo imágenes capturadas con la cámara de la aplicación</string>
|
<string name="use_external_storage_summary">Guardar en el dispositivo imágenes capturadas con la cámara de la aplicación</string>
|
||||||
|
|
|
||||||
|
|
@ -172,7 +172,7 @@
|
||||||
<string name="title_activity_nearby">Gertuko lekuak</string>
|
<string name="title_activity_nearby">Gertuko lekuak</string>
|
||||||
<string name="no_nearby">Ez da hurbileko lekurik aurkitu</string>
|
<string name="no_nearby">Ez da hurbileko lekurik aurkitu</string>
|
||||||
<string name="warning">Oharra</string>
|
<string name="warning">Oharra</string>
|
||||||
<string name="file_exists">Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?</string>
|
<string name="upload_image_problem_duplicate">Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula?</string>
|
||||||
<string name="yes">Bai</string>
|
<string name="yes">Bai</string>
|
||||||
<string name="no">Ez</string>
|
<string name="no">Ez</string>
|
||||||
<string name="media_detail_title">Izenburua</string>
|
<string name="media_detail_title">Izenburua</string>
|
||||||
|
|
@ -229,8 +229,8 @@
|
||||||
<string name="error_while_cache">Argazkiak hartzerakoan sortutako akatsa</string>
|
<string name="error_while_cache">Argazkiak hartzerakoan sortutako akatsa</string>
|
||||||
<string name="title_info">Fitxategi izenburu deskribatzaile bakarra, fitxategi-izen gisa balioko duena. Hizkuntza arrunta erabil dezakezu espazioekin. Ez sartu fitxategiaren luzapena.</string>
|
<string name="title_info">Fitxategi izenburu deskribatzaile bakarra, fitxategi-izen gisa balioko duena. Hizkuntza arrunta erabil dezakezu espazioekin. Ez sartu fitxategiaren luzapena.</string>
|
||||||
<string name="description_info">Mesedez, deskribatu multimedia elementua ahal duzun gehien: non hartu zen? zer erakusten du? zein da bere testuingurua? Mesedez, objektuak eta pertsonak deskribatu. Eman asmatzeko erraza ez den informazioa, adibidez, paisaia bat izatekotan, eguneko zein orudtan hartu den. Multimediak zerbait berezia erakusten badu, mesedez azaldu zerk egiten duen berezia.</string>
|
<string name="description_info">Mesedez, deskribatu multimedia elementua ahal duzun gehien: non hartu zen? zer erakusten du? zein da bere testuingurua? Mesedez, objektuak eta pertsonak deskribatu. Eman asmatzeko erraza ez den informazioa, adibidez, paisaia bat izatekotan, eguneko zein orudtan hartu den. Multimediak zerbait berezia erakusten badu, mesedez azaldu zerk egiten duen berezia.</string>
|
||||||
<string name="upload_image_too_dark">Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
|
<string name="upload_image_problem_dark">Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
|
||||||
<string name="upload_image_blurry">Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
|
<string name="upload_image_problem_blurry">Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu.</string>
|
||||||
<string name="give_permission">Baimena eman</string>
|
<string name="give_permission">Baimena eman</string>
|
||||||
<string name="use_external_storage">Kanpo-biltegia erabili</string>
|
<string name="use_external_storage">Kanpo-biltegia erabili</string>
|
||||||
<string name="use_external_storage_summary">Aplikazioaren kamerarekin ateratako argazkiak zure gailuan gorde</string>
|
<string name="use_external_storage_summary">Aplikazioaren kamerarekin ateratako argazkiak zure gailuan gorde</string>
|
||||||
|
|
|
||||||
|
|
@ -174,7 +174,7 @@
|
||||||
<string name="title_activity_nearby">مکانهای اطراف</string>
|
<string name="title_activity_nearby">مکانهای اطراف</string>
|
||||||
<string name="no_nearby">مکانی در نزدیکی یافت نشد</string>
|
<string name="no_nearby">مکانی در نزدیکی یافت نشد</string>
|
||||||
<string name="warning">هشدار</string>
|
<string name="warning">هشدار</string>
|
||||||
<string name="file_exists">پرونده در ویکیانبار موجود است. آیا مطمئنید که میخواهید ادامه دهید؟</string>
|
<string name="upload_image_problem_duplicate">پرونده در ویکیانبار موجود است. آیا مطمئنید که میخواهید ادامه دهید؟</string>
|
||||||
<string name="yes">بله</string>
|
<string name="yes">بله</string>
|
||||||
<string name="no">خیر</string>
|
<string name="no">خیر</string>
|
||||||
<string name="media_detail_title">عنوان</string>
|
<string name="media_detail_title">عنوان</string>
|
||||||
|
|
@ -236,8 +236,8 @@
|
||||||
<string name="error_while_cache">خطا در زمان دریافت تصاویر</string>
|
<string name="error_while_cache">خطا در زمان دریافت تصاویر</string>
|
||||||
<string name="title_info">عنوانی توصیفی و یکتا برای پرونده که به عنوان نام پرونده در نظر گرفته خواهد شد. ترجیحاً به زبان ساده باشد، میتوانید فاصله هم به کار ببرید. پسوند پرونده را ننویسید.</string>
|
<string name="title_info">عنوانی توصیفی و یکتا برای پرونده که به عنوان نام پرونده در نظر گرفته خواهد شد. ترجیحاً به زبان ساده باشد، میتوانید فاصله هم به کار ببرید. پسوند پرونده را ننویسید.</string>
|
||||||
<string name="description_info">لطفاً تصویر را تا حد توان شرح دهید. کجا گرفته شدهاست؟ شامل چه چیزی میشود؟ لطفاً اشیا یا افراد را شرح دهید. اطلاعاتی که به راحتی قابل مشاهده هستند را صرفهنظر کنید. اگر چیزی در تصویر غیر طبیعی به نظر میرسد آن را شرح دهید.</string>
|
<string name="description_info">لطفاً تصویر را تا حد توان شرح دهید. کجا گرفته شدهاست؟ شامل چه چیزی میشود؟ لطفاً اشیا یا افراد را شرح دهید. اطلاعاتی که به راحتی قابل مشاهده هستند را صرفهنظر کنید. اگر چیزی در تصویر غیر طبیعی به نظر میرسد آن را شرح دهید.</string>
|
||||||
<string name="upload_image_too_dark">این تصویر خیلی تیره است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.</string>
|
<string name="upload_image_problem_dark">این تصویر خیلی تیره است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.</string>
|
||||||
<string name="upload_image_blurry">این تصویر خیلی تار است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.</string>
|
<string name="upload_image_problem_blurry">این تصویر خیلی تار است آیا مطمئنید که میخواهید آن را بارگذاری کنید؟ ویکیانبار فقط برای نگهداری از تصاویری که ارزش دانشنامهای داشته باشند، است.</string>
|
||||||
<string name="give_permission">اجازه بده</string>
|
<string name="give_permission">اجازه بده</string>
|
||||||
<string name="use_external_storage">استفاده از حافظهٔ خارجی</string>
|
<string name="use_external_storage">استفاده از حافظهٔ خارجی</string>
|
||||||
<string name="use_external_storage_summary">ذخیرهٔ تصویرهای گرفته شده توسط دوربین درونکار اپلیکیشن بر روی دستگاه شما</string>
|
<string name="use_external_storage_summary">ذخیرهٔ تصویرهای گرفته شده توسط دوربین درونکار اپلیکیشن بر روی دستگاه شما</string>
|
||||||
|
|
|
||||||
|
|
@ -176,7 +176,7 @@
|
||||||
<string name="title_activity_nearby">Lähellä olevat paikat</string>
|
<string name="title_activity_nearby">Lähellä olevat paikat</string>
|
||||||
<string name="no_nearby">Lähistöltä ei löytynyt paikkoja</string>
|
<string name="no_nearby">Lähistöltä ei löytynyt paikkoja</string>
|
||||||
<string name="warning">Varoitus</string>
|
<string name="warning">Varoitus</string>
|
||||||
<string name="file_exists">Tämä tiedosto on jo Wikimedia Commonsissa. Haluatko varmasti jatkaa?</string>
|
<string name="upload_image_problem_duplicate">Tämä tiedosto on jo Wikimedia Commonsissa. Haluatko varmasti jatkaa?</string>
|
||||||
<string name="yes">Kyllä</string>
|
<string name="yes">Kyllä</string>
|
||||||
<string name="no">Ei</string>
|
<string name="no">Ei</string>
|
||||||
<string name="media_detail_title">Otsikko</string>
|
<string name="media_detail_title">Otsikko</string>
|
||||||
|
|
@ -237,8 +237,8 @@
|
||||||
<string name="error_while_cache">Virhe varastoidessa kuvia</string>
|
<string name="error_while_cache">Virhe varastoidessa kuvia</string>
|
||||||
<string name="title_info">Tiedoston yksilöllinen ja kuvaava otsikko, jota käytetään tiedostonimenä. Voit käyttää tavallista kieltä välilyönnein. Älä sisällytä tiedoston päätettä.</string>
|
<string name="title_info">Tiedoston yksilöllinen ja kuvaava otsikko, jota käytetään tiedostonimenä. Voit käyttää tavallista kieltä välilyönnein. Älä sisällytä tiedoston päätettä.</string>
|
||||||
<string name="description_info">Kuvaile mediaa niin paljon kuin mahdollista: Missä se otettiin? Mitä se esittää? Mikä on asiayhteys? Kuvaile esineitä tai henkilöitä. Tuo ilmi tietoja, joita ei ole helppo arvailla, esimerkiksi vuorokaudenaika, jos se on maisema. Jos media esittää jotain epätavallista, selitä, mikä tekee siitä epätavallisen.</string>
|
<string name="description_info">Kuvaile mediaa niin paljon kuin mahdollista: Missä se otettiin? Mitä se esittää? Mikä on asiayhteys? Kuvaile esineitä tai henkilöitä. Tuo ilmi tietoja, joita ei ole helppo arvailla, esimerkiksi vuorokaudenaika, jos se on maisema. Jos media esittää jotain epätavallista, selitä, mikä tekee siitä epätavallisen.</string>
|
||||||
<string name="upload_image_too_dark">Tämä kuva on liian tumma, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
|
<string name="upload_image_problem_dark">Tämä kuva on liian tumma, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
|
||||||
<string name="upload_image_blurry">Tämä kuva on epäselvä, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
|
<string name="upload_image_problem_blurry">Tämä kuva on epäselvä, haluatko varmasti ladata sen? Wikimedia Commons on vain kuville, joilla on tietosanakirja-arvo.</string>
|
||||||
<string name="give_permission">Anna lupa</string>
|
<string name="give_permission">Anna lupa</string>
|
||||||
<string name="use_external_storage">Käytä ulkoista tallennustilaa</string>
|
<string name="use_external_storage">Käytä ulkoista tallennustilaa</string>
|
||||||
<string name="use_external_storage_summary">Tallenna sovelluksen sisäisen kameran kanssa otetut kuvat laitteellesi</string>
|
<string name="use_external_storage_summary">Tallenna sovelluksen sisäisen kameran kanssa otetut kuvat laitteellesi</string>
|
||||||
|
|
|
||||||
|
|
@ -185,7 +185,7 @@
|
||||||
<string name="title_activity_nearby">Endroits à proximité</string>
|
<string name="title_activity_nearby">Endroits à proximité</string>
|
||||||
<string name="no_nearby">Rien trouvé dans le voisinage</string>
|
<string name="no_nearby">Rien trouvé dans le voisinage</string>
|
||||||
<string name="warning">Avertissement</string>
|
<string name="warning">Avertissement</string>
|
||||||
<string name="file_exists">Ce fichier existe déjà sur Commons. Êtes-vous sûr de vouloir continuer ?</string>
|
<string name="upload_image_problem_duplicate">Ce fichier existe déjà sur Commons. Êtes-vous sûr de vouloir continuer ?</string>
|
||||||
<string name="yes">Oui</string>
|
<string name="yes">Oui</string>
|
||||||
<string name="no">Non</string>
|
<string name="no">Non</string>
|
||||||
<string name="media_detail_title">Titre</string>
|
<string name="media_detail_title">Titre</string>
|
||||||
|
|
@ -247,8 +247,8 @@
|
||||||
<string name="error_while_cache">Erreur en mettant les images en cache</string>
|
<string name="error_while_cache">Erreur en mettant les images en cache</string>
|
||||||
<string name="title_info">Un titre descriptif unique pour le fichier, qui servira de nom de fichier. Vous pouvez utiliser un langage simple avec des espaces. N’incluez pas l’extension du fichier</string>
|
<string name="title_info">Un titre descriptif unique pour le fichier, qui servira de nom de fichier. Vous pouvez utiliser un langage simple avec des espaces. N’incluez pas l’extension du fichier</string>
|
||||||
<string name="description_info">Veuillez décrire le média autant que possible : Où a-t-il été enregistré ? Que montre-t-il ? Quel est le contexte ? Veuillez décrire les objets ou les personnes. Révélez les informations qui ne peuvent pas être devinées facilement, par exemple l’heure de la journée si c’est un paysage. Si le média montre quelque chose d’inhabituel, veuillez expliquer ce qui le rend exceptionnel.</string>
|
<string name="description_info">Veuillez décrire le média autant que possible : Où a-t-il été enregistré ? Que montre-t-il ? Quel est le contexte ? Veuillez décrire les objets ou les personnes. Révélez les informations qui ne peuvent pas être devinées facilement, par exemple l’heure de la journée si c’est un paysage. Si le média montre quelque chose d’inhabituel, veuillez expliquer ce qui le rend exceptionnel.</string>
|
||||||
<string name="upload_image_too_dark">Cette image est trop sombre, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images avec une valeur encyclopédique.</string>
|
<string name="upload_image_problem_dark">Cette image est trop sombre, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images avec une valeur encyclopédique.</string>
|
||||||
<string name="upload_image_blurry">Cette image est floue, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images ayant une valeur encyclopédique.</string>
|
<string name="upload_image_problem_blurry">Cette image est floue, êtes-vous sûr de vouloir la télécharger ? Wikimédia Communs n’est que pour les images ayant une valeur encyclopédique.</string>
|
||||||
<string name="give_permission">Accorder le droit</string>
|
<string name="give_permission">Accorder le droit</string>
|
||||||
<string name="use_external_storage">Utiliser le stockage externe</string>
|
<string name="use_external_storage">Utiliser le stockage externe</string>
|
||||||
<string name="use_external_storage_summary">Enregistrer les images prises avec l’appareil photo de votre appareil</string>
|
<string name="use_external_storage_summary">Enregistrer les images prises avec l’appareil photo de votre appareil</string>
|
||||||
|
|
|
||||||
|
|
@ -141,7 +141,7 @@
|
||||||
<string name="title_activity_nearby">Steeden naibi</string>
|
<string name="title_activity_nearby">Steeden naibi</string>
|
||||||
<string name="no_nearby">Nian steeden uun a naite fünjen</string>
|
<string name="no_nearby">Nian steeden uun a naite fünjen</string>
|
||||||
<string name="warning">Wäärnang</string>
|
<string name="warning">Wäärnang</string>
|
||||||
<string name="file_exists">Detdiar datei jaft det al üüb Commons. Beest dü seeker, dat dü widjer maage wel?</string>
|
<string name="upload_image_problem_duplicate">Detdiar datei jaft det al üüb Commons. Beest dü seeker, dat dü widjer maage wel?</string>
|
||||||
<string name="yes">Ja</string>
|
<string name="yes">Ja</string>
|
||||||
<string name="no">Naan</string>
|
<string name="no">Naan</string>
|
||||||
<string name="media_detail_title">Tiitel</string>
|
<string name="media_detail_title">Tiitel</string>
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@
|
||||||
<string name="title_activity_nearby">Lugares próximos</string>
|
<string name="title_activity_nearby">Lugares próximos</string>
|
||||||
<string name="no_nearby">Non se atoparon lugares preto</string>
|
<string name="no_nearby">Non se atoparon lugares preto</string>
|
||||||
<string name="warning">Aviso</string>
|
<string name="warning">Aviso</string>
|
||||||
<string name="file_exists">Este ficheiro xa existe en Commons. Está seguro de que quere continuar?</string>
|
<string name="upload_image_problem_duplicate">Este ficheiro xa existe en Commons. Está seguro de que quere continuar?</string>
|
||||||
<string name="yes">Si</string>
|
<string name="yes">Si</string>
|
||||||
<string name="no">Non</string>
|
<string name="no">Non</string>
|
||||||
<string name="media_detail_title">Título</string>
|
<string name="media_detail_title">Título</string>
|
||||||
|
|
@ -237,8 +237,8 @@
|
||||||
<string name="error_while_cache">Erro mentras se gardaban as imaxes na caché</string>
|
<string name="error_while_cache">Erro mentras se gardaban as imaxes na caché</string>
|
||||||
<string name="title_info">Un título único descritivo para o ficheiro, que servirá como un nome de ficheiro. Pode usar unha linguaxe clara con espazos. Non inclúa a extensión do ficheiro</string>
|
<string name="title_info">Un título único descritivo para o ficheiro, que servirá como un nome de ficheiro. Pode usar unha linguaxe clara con espazos. Non inclúa a extensión do ficheiro</string>
|
||||||
<string name="description_info">Por favor, describa o ficheiro todo o posibleː Onde se gravou? Cal é o contexto? Por favor, describa os obxectos ou persoas. Indique información que non pode ser adiviñada de forma doada, por exemplo, a hora do día se é unha paisaxe. Se o ficheiro amosa algo pouco habitual, por favor, explique que é o que o fai excepcional.</string>
|
<string name="description_info">Por favor, describa o ficheiro todo o posibleː Onde se gravou? Cal é o contexto? Por favor, describa os obxectos ou persoas. Indique información que non pode ser adiviñada de forma doada, por exemplo, a hora do día se é unha paisaxe. Se o ficheiro amosa algo pouco habitual, por favor, explique que é o que o fai excepcional.</string>
|
||||||
<string name="upload_image_too_dark">Esta imaxe é demasiado escura. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
|
<string name="upload_image_problem_dark">Esta imaxe é demasiado escura. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
|
||||||
<string name="upload_image_blurry">Esta imaxe está borrosa. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
|
<string name="upload_image_problem_blurry">Esta imaxe está borrosa. Confirma que quere subila? Wikimedia Commons só acepta imaxes con valor enciclopédico.</string>
|
||||||
<string name="give_permission">Outorgar permiso</string>
|
<string name="give_permission">Outorgar permiso</string>
|
||||||
<string name="use_external_storage">Usar o almacenamento externo</string>
|
<string name="use_external_storage">Usar o almacenamento externo</string>
|
||||||
<string name="use_external_storage_summary">Gardar as imaxes capturadas coa cámara do seu dispositivo</string>
|
<string name="use_external_storage_summary">Gardar as imaxes capturadas coa cámara do seu dispositivo</string>
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@
|
||||||
<string name="title_activity_nearby">आसपास के स्थान</string>
|
<string name="title_activity_nearby">आसपास के स्थान</string>
|
||||||
<string name="no_nearby">पास के कोई भी स्थान नहीं मिले</string>
|
<string name="no_nearby">पास के कोई भी स्थान नहीं मिले</string>
|
||||||
<string name="warning">चेतावनी</string>
|
<string name="warning">चेतावनी</string>
|
||||||
<string name="file_exists">यह फ़ाइल कॉमन्स पर पहले से है। क्या आप फिर भी आगे बढ़ना चाहते हैं?</string>
|
<string name="upload_image_problem_duplicate">यह फ़ाइल कॉमन्स पर पहले से है। क्या आप फिर भी आगे बढ़ना चाहते हैं?</string>
|
||||||
<string name="yes">हाँ</string>
|
<string name="yes">हाँ</string>
|
||||||
<string name="no">नहीं</string>
|
<string name="no">नहीं</string>
|
||||||
<string name="media_detail_title">शीर्षक</string>
|
<string name="media_detail_title">शीर्षक</string>
|
||||||
|
|
@ -211,7 +211,7 @@
|
||||||
<string name="error_while_cache">चित्र कैशिंग करते समय त्रुटि</string>
|
<string name="error_while_cache">चित्र कैशिंग करते समय त्रुटि</string>
|
||||||
<string name="title_info">फ़ाइल के लिए एक अद्वितीय वर्णनात्मक शीर्षक, जो एक फ़ाइल नाम के रूप में काम करेगा। आप रिक्त स्थान के साथ सादे भाषा का उपयोग कर सकते हैं। फ़ाइल विस्तार शामिल न करें</string>
|
<string name="title_info">फ़ाइल के लिए एक अद्वितीय वर्णनात्मक शीर्षक, जो एक फ़ाइल नाम के रूप में काम करेगा। आप रिक्त स्थान के साथ सादे भाषा का उपयोग कर सकते हैं। फ़ाइल विस्तार शामिल न करें</string>
|
||||||
<string name="description_info">कृपया मीडिया जितना संभव हो उतना बताएं: यह कहां लिया गया? यह क्या दिखाता है? संदर्भ क्या है? कृपया वस्तुओं या व्यक्तियों का वर्णन करें। ऐसी जानकारी का खुलासा करें जिसे आसानी से अनुमानित नहीं किया जा सकता, उदाहरण के लिए दिन का समय यदि यह परिदृश्य है। अगर मीडिया कुछ असामान्य दिखाता है, तो कृपया बताएं कि इसे क्या असामान्य बनाता है।</string>
|
<string name="description_info">कृपया मीडिया जितना संभव हो उतना बताएं: यह कहां लिया गया? यह क्या दिखाता है? संदर्भ क्या है? कृपया वस्तुओं या व्यक्तियों का वर्णन करें। ऐसी जानकारी का खुलासा करें जिसे आसानी से अनुमानित नहीं किया जा सकता, उदाहरण के लिए दिन का समय यदि यह परिदृश्य है। अगर मीडिया कुछ असामान्य दिखाता है, तो कृपया बताएं कि इसे क्या असामान्य बनाता है।</string>
|
||||||
<string name="upload_image_too_dark">यह चित्र बहुत गहरा है, क्या आप वाकई इसे अपलोड करना चाहते हैं? विकिमीडिया कॉमन्स केवल विश्वकोषीय मूल्य वाले चित्रों के लिए है।</string>
|
<string name="upload_image_problem_dark">यह चित्र बहुत गहरा है, क्या आप वाकई इसे अपलोड करना चाहते हैं? विकिमीडिया कॉमन्स केवल विश्वकोषीय मूल्य वाले चित्रों के लिए है।</string>
|
||||||
<string name="give_permission">अनुमति दें</string>
|
<string name="give_permission">अनुमति दें</string>
|
||||||
<string name="use_external_storage">बाहरी स्टॉरज का पृयोग करे।</string>
|
<string name="use_external_storage">बाहरी स्टॉरज का पृयोग करे।</string>
|
||||||
<string name="use_external_storage_summary">आप अपने डिवाइस के इन-ऐप कैमरा से ली गई तस्वीरों को सहेजें।</string>
|
<string name="use_external_storage_summary">आप अपने डिवाइस के इन-ऐप कैमरा से ली गई तस्वीरों को सहेजें।</string>
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue