mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Merge pull request #5 from commons-app/master
Update to latest upstream
This commit is contained in:
commit
0f98bca968
50 changed files with 834 additions and 960 deletions
129
README.md
129
README.md
|
|
@ -1,127 +1,38 @@
|
|||
# Wikimedia Commons Android app [](https://travis-ci.org/commons-app/apps-android-commons)
|
||||
|
||||
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][8], or view our [website][9].
|
||||
The Wikimedia Commons Android app allows users to upload pictures from their Android phone/tablet to Wikimedia Commons. Download the app [here][1], or view our [website][2].
|
||||
|
||||
Initially started by the Wikimedia Foundation, this app is now maintained by volunteers. Anyone is welcome to improve it, just choose among the [open issues](https://github.com/commons-app/apps-android-commons/issues) and send us a pull request :-)
|
||||
Initially started by the Wikimedia Foundation, this app is now maintained by volunteers. Anyone is welcome to improve it, just choose among the [open issues][3] and send us a pull request :-)
|
||||
|
||||
We are currently applying for an [IEG renewal][15] to work on the app for the next 6 months. Feedback is very much welcomed.
|
||||
We are currently applying for an [IEG renewal][10] to work on the app for the next 6 months. Feedback is very much welcomed.
|
||||
|
||||
<a href="https://f-droid.org/repository/browse/?fdid=fr.free.nrw.commons" target="_blank">
|
||||
<img src="https://f-droid.org/badge/get-it-on.png" alt="Get it on F-Droid" height="90"/></a>
|
||||
<a href="https://play.google.com/store/apps/details?id=fr.free.nrw.commons" target="_blank">
|
||||
<img src="https://play.google.com/intl/en_us/badges/images/generic/en-play-badge.png" alt="Get it on Google Play" height="90"/></a>
|
||||
|
||||
## Documentation
|
||||
|
||||
## Develop with Android Studio or IntelliJ ##
|
||||
We try to have an extensive documentation at [our wiki here at Github][5]:
|
||||
|
||||
[Download Android Studio][1] (recommended) or [IntelliJ][2].
|
||||
|
||||
1. Open Android Studio/IntelliJ. Open the project:
|
||||
``File`` > ``New`` > ``Project from Version Control...`` > ``Git``
|
||||
or
|
||||
(From Quick Start menu): ``Check out project from Version Control``
|
||||
2. Enter ``https://github.com/commons-app/apps-android-commons/`` as Git Repository URL. Specify a (new) local directory you would like to clone into and select ``OK``.
|
||||
|
||||
## Build Manually ##
|
||||
|
||||
### Requirements ###
|
||||
|
||||
1. Java SDK 8 (OpenJDK 8 or Oracle Java SE 8)
|
||||
2. [Android SDK][3] (Level 23)
|
||||
3. [Gradle][4]
|
||||
|
||||
### Build Instructions ###
|
||||
|
||||
1. Set the environment variable `ANDROID_HOME` to be the path to your Android SDK
|
||||
2. Set the environment variable `JAVA_HOME` to the path to your Java SDK
|
||||
3. Run `gradlew.bat assembleDebug` (Windows) or `./gradlew assembleDebug` (Mac / Linux) to build an unisgned apk
|
||||
4. Alternatively, you can also connect your Android device via USB and install the app on it directly by running `gradlew.bat installDebug` (Windows) or `./gradlew installDebug` (Mac / Linux)
|
||||
|
||||
There are more thorough instructions on the [Android Developers website][5]
|
||||
* [User Documentation][6]
|
||||
* [Contributor Documentation][7]
|
||||
* [Volunteers Welcome!][9]
|
||||
* [Developer Documentation][8]
|
||||
|
||||
## License ##
|
||||
|
||||
This software is open source, licensed under the [Apache License 2.0][6].
|
||||
|
||||
## Code Structure ##
|
||||
|
||||
Key breakdowns:
|
||||
|
||||
Activities started within the UI:
|
||||
* ContributionsActivity (ContributionsListFragment, MediaDetailPagerFragment, MediaDetailFragment) - main "my uploads" list and detail view
|
||||
* LoginActivity - login screen when setting up an account
|
||||
* SettingsActivity - settings screen
|
||||
* AboutActivity - about screen
|
||||
|
||||
Activities receiving intents:
|
||||
* ShareActivity (SingleUploadFragment, CategorizationFragment) - handles receiving a file from another app, accepting a title/desc, and slating it for upload
|
||||
* MultipleShareActivity (MultipleUploadListFragment, CategorizationFragment) - handles receiving a batch of multiple files from another app, accepting a title/desc, and slating them for upload
|
||||
|
||||
Services:
|
||||
* WikiAccountAuthenticatorService - authentication service
|
||||
* UploadService - performs actual file uploads in background
|
||||
* ContributionsSyncService - polls for updated contributions list from server
|
||||
* ModificationsSyncService - pushes category additions up to server
|
||||
|
||||
Content providers:
|
||||
* ContributionsContentProvider - private storage for local copy of user's contribution list
|
||||
* ModificationsContentProvider - private storage for pending category and template modifications
|
||||
* CategoryContentProvider - private storage for recently used categories
|
||||
|
||||
|
||||
## On-Device Storage ##
|
||||
|
||||
Account credentials are encapsulated in an account provider. Currently only one Wikimedia Commons account is supported at a time. (Question: what is the actual storage for credentials?)
|
||||
|
||||
Preferences are stored in Android's SharedPreferences.
|
||||
|
||||
Information about past and pending uploads is stored in the Contributions content provider, which uses an SQLite database on the backend.
|
||||
|
||||
A list of recently-used categories is stored in the Categories content provider, which uses an SQLite database on the backend.
|
||||
|
||||
Captured files are not currently stored within the app, but are passed by content: or file: URI from other apps.
|
||||
|
||||
Thumbnail images are not currently cached.
|
||||
|
||||
## Volunteers welcome! ##
|
||||
|
||||
We are always looking for volunteers, feel free to step in! It is very easy:
|
||||
|
||||
1. Fork the repository and clone it to your computer, then follow the build instructions above.
|
||||
2. Choose an [unassigned issue](https://github.com/commons-app/apps-android-commons/issues?q=is%3Aopen+is%3Aissue+no%3Aassignee) that sounds interesting to you.
|
||||
3. Read the issue's comments to make sure you understand what is the bug, or what feature is being proposed.
|
||||
4. Write a "I start working on this" comment on the issue
|
||||
5. Write the code :-)
|
||||
6. Commit and push
|
||||
7. Go to your fork's Github webpage, select the "Pull Requests" tab and click "create a pull request", as a comment, write something like "Fix for issue #12345 crash when rotating screen", then submit the pull request.
|
||||
8. Within a few hours or days, a core developer will review your patch, and either merge it or suggest a few corrections.
|
||||
9. If you change your mind, or if it is too difficult, no problem, just write "Sorry I don't work on this anymore" on the issue, if possible including feedback (for instance what approaches failed) and ideas.
|
||||
|
||||
Thanks a lot!
|
||||
|
||||
## Translating the app ##
|
||||
|
||||
Thanks to the translation work of many volunteers this app is available in a multitude of languages.
|
||||
|
||||
Translation of the text content of the Wikimedia Commons Android app happens on the [Commons Android App project][10] on [translatewiki.net][11]. If you want to help translate the app please create an account there (to get "translate rights" edit 20 [random keys][13] or ask in their [chat][14]).
|
||||
|
||||
The translations from the translatewiki project are [periodically committed directly to this project][12] by the translatewiki team and later released with the normal updates to the Play Store.
|
||||
This software is open source, licensed under the [Apache License 2.0][4].
|
||||
|
||||
|
||||
|
||||
|
||||
[1]: https://developer.android.com/studio/index.html
|
||||
[2]: http://www.jetbrains.com/idea/download/index.html
|
||||
[3]: https://developer.android.com/sdk/index.html
|
||||
[4]: http://gradle.org/gradle-download/
|
||||
[5]: https://developer.android.com/studio/build/building-cmdline.html
|
||||
[6]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
[7]: https://github.com/commons-app/apps-android-commons/issues
|
||||
[8]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
||||
[9]: https://commons-app.github.io/
|
||||
[10]: https://translatewiki.net/w/i.php?title=Special:Translate&group=commons-android
|
||||
[11]: https://translatewiki.net
|
||||
[12]: https://github.com/commons-app/apps-android-commons/commits/master?author=translatewiki
|
||||
[13]: https://translatewiki.net/wiki/Special:TranslationStash?
|
||||
[14]: https://translatewiki.net/wiki/Special:WebChat
|
||||
[15]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
|
||||
[1]: https://play.google.com/store/apps/details?id=fr.free.nrw.commons
|
||||
[2]: https://commons-app.github.io/
|
||||
[3]: https://github.com/commons-app/apps-android-commons/issues
|
||||
[4]: https://www.apache.org/licenses/LICENSE-2.0
|
||||
[5]: https://github.com/commons-app/apps-android-commons/wiki
|
||||
[6]: https://github.com/commons-app/apps-android-commons/wiki#user-documentation
|
||||
[7]: https://github.com/commons-app/apps-android-commons/wiki#contributor-documentation
|
||||
[8]: https://github.com/commons-app/apps-android-commons/wiki#developer-documentation
|
||||
[9]: https://github.com/commons-app/apps-android-commons/wiki/Volunteers-welcome%21
|
||||
[10]: https://meta.wikimedia.org/wiki/Grants:Project/Improve_%27Upload_to_Commons%27_Android_App/Renewal
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ dependencies {
|
|||
compile "com.android.support:design:${project.supportLibVersion}"
|
||||
compile 'com.google.code.gson:gson:2.8.0'
|
||||
compile "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
compile 'com.github.pedrovgs:renderers:3.3.0'
|
||||
compile 'com.github.pedrovgs:renderers:3.3.3'
|
||||
annotationProcessor "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
compile 'com.jakewharton.timber:timber:4.5.1'
|
||||
compile 'com.squareup.okhttp3:okhttp:3.8.1'
|
||||
|
|
@ -30,7 +30,10 @@ dependencies {
|
|||
// 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.
|
||||
compile 'io.reactivex.rxjava2:rxjava:2.1.2'
|
||||
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding:2.0.0'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0'
|
||||
compile 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0'
|
||||
|
||||
compile 'com.facebook.fresco:fresco:1.3.0'
|
||||
compile 'com.facebook.stetho:stetho:1.5.0'
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import android.content.pm.PackageManager;
|
|||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.util.LruCache;
|
||||
import android.util.Log;
|
||||
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.stetho.Stetho;
|
||||
|
|
@ -26,8 +25,8 @@ import java.io.IOException;
|
|||
|
||||
import fr.free.nrw.commons.auth.AccountUtil;
|
||||
import fr.free.nrw.commons.caching.CacheController;
|
||||
import fr.free.nrw.commons.category.Category;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi;
|
||||
|
|
@ -131,11 +130,14 @@ public class CommonsApplication extends Application {
|
|||
|
||||
Timber.plant(new Timber.DebugTree());
|
||||
|
||||
Stetho.initializeWithDefaults(this);
|
||||
|
||||
|
||||
if (!BuildConfig.DEBUG) {
|
||||
ACRA.init(this);
|
||||
} else {
|
||||
Stetho.initializeWithDefaults(this);
|
||||
}
|
||||
|
||||
// Fire progress callbacks for every 3% of uploaded content
|
||||
System.setProperty("in.yuvi.http.fluent.PROGRESS_TRIGGER_THRESHOLD", "3.0");
|
||||
|
||||
|
|
|
|||
|
|
@ -277,6 +277,6 @@ public class Utils {
|
|||
}
|
||||
|
||||
public static boolean isDarkTheme(Context context) {
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme",true);
|
||||
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean("theme",false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ class LoginTask extends AsyncTask<String, String, String> {
|
|||
protected void onPostExecute(String result) {
|
||||
super.onPostExecute(result);
|
||||
Timber.d("Login done!");
|
||||
|
||||
EventLog.schema(CommonsApplication.EVENT_LOGIN_ATTEMPT)
|
||||
.param("username", username)
|
||||
.param("result", result)
|
||||
|
|
|
|||
|
|
@ -2,18 +2,13 @@ package fr.free.nrw.commons.category;
|
|||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.os.RemoteException;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.app.Fragment;
|
||||
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;
|
||||
|
|
@ -24,24 +19,26 @@ import android.widget.EditText;
|
|||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||
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.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.category.CategoriesRenderer.CategoryClickedListener;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import fr.free.nrw.commons.upload.MwVolleyApi;
|
||||
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;
|
||||
|
|
@ -51,7 +48,8 @@ import static fr.free.nrw.commons.category.CategoryContentProvider.AUTHORITY;
|
|||
/**
|
||||
* Displays the category suggestion and selection screen. Category search is initiated here.
|
||||
*/
|
||||
public class CategorizationFragment extends Fragment implements CategoryClickedListener {
|
||||
public class CategorizationFragment extends Fragment {
|
||||
|
||||
public static final int SEARCH_CATS_LIMIT = 25;
|
||||
|
||||
@BindView(R.id.categoriesListBox)
|
||||
|
|
@ -68,19 +66,18 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
|
|||
private RVRendererAdapter<CategoryItem> categoriesAdapter;
|
||||
private OnCategoriesSaveHandler onCategoriesSaveHandler;
|
||||
private HashMap<String, ArrayList<String>> categoriesCache;
|
||||
private ArrayList<String> selectedCategories = new ArrayList<>();
|
||||
private ContentProviderClient client;
|
||||
private PrefixUpdater prefixUpdaterSub;
|
||||
private MethodAUpdater methodAUpdaterSub;
|
||||
private final CategoryTextWatcher textWatcher = new CategoryTextWatcher();
|
||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(this);
|
||||
private final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
|
||||
private final ArrayList<String> titleCatItems = new ArrayList<>();
|
||||
private final CountDownLatch mergeLatch = new CountDownLatch(1);
|
||||
// LHS guarantees ordered insertions, allowing for prioritized method A results
|
||||
private final Set<String> results = new LinkedHashSet<>();
|
||||
private List<CategoryItem> selectedCategories = new ArrayList<>();
|
||||
private ContentProviderClient databaseClient;
|
||||
|
||||
private final CategoriesAdapterFactory adapterFactory = new CategoriesAdapterFactory(item -> {
|
||||
if (item.isSelected()) {
|
||||
selectedCategories.add(item);
|
||||
updateCategoryCount(item, databaseClient);
|
||||
} else {
|
||||
selectedCategories.remove(item);
|
||||
}
|
||||
});
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
|
@ -89,27 +86,29 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
|
|||
|
||||
categoriesList.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
|
||||
categoriesSkip.setOnClickListener(view -> {
|
||||
getActivity().onBackPressed();
|
||||
getActivity().finish();
|
||||
});
|
||||
RxView.clicks(categoriesSkip)
|
||||
.takeUntil(RxView.detaches(categoriesSkip))
|
||||
.subscribe(o -> {
|
||||
getActivity().onBackPressed();
|
||||
getActivity().finish();
|
||||
});
|
||||
|
||||
ArrayList<CategoryItem> items;
|
||||
if (savedInstanceState == null) {
|
||||
items = new ArrayList<>();
|
||||
categoriesCache = new HashMap<>();
|
||||
} else {
|
||||
items = savedInstanceState.getParcelableArrayList("currentCategories");
|
||||
categoriesCache = (HashMap<String, ArrayList<String>>) savedInstanceState
|
||||
.getSerializable("categoriesCache");
|
||||
ArrayList<CategoryItem> items = new ArrayList<>();
|
||||
categoriesCache = new HashMap<>();
|
||||
if (savedInstanceState != null) {
|
||||
items.addAll(savedInstanceState.getParcelableArrayList("currentCategories"));
|
||||
categoriesCache.putAll((HashMap<String, ArrayList<String>>) savedInstanceState
|
||||
.getSerializable("categoriesCache"));
|
||||
}
|
||||
|
||||
categoriesAdapter = adapterFactory.create(items);
|
||||
categoriesList.setAdapter(categoriesAdapter);
|
||||
categoriesFilter.addTextChangedListener(textWatcher);
|
||||
|
||||
startUpdatingCategoryList();
|
||||
|
||||
RxTextView.textChanges(categoriesFilter)
|
||||
.takeUntil(RxView.detaches(categoriesFilter))
|
||||
.debounce(300, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(filter -> updateCategoryList(filter.toString()));
|
||||
return rootView;
|
||||
}
|
||||
|
||||
|
|
@ -129,7 +128,7 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
|
|||
rootView.requestFocus();
|
||||
rootView.setOnKeyListener((v, keyCode, event) -> {
|
||||
if (event.getAction() == ACTION_UP && keyCode == KEYCODE_BACK) {
|
||||
backButtonDialog();
|
||||
showBackButtonDialog();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
|
|
@ -137,16 +136,10 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
categoriesFilter.removeTextChangedListener(textWatcher);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
client.release();
|
||||
databaseClient.release();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -165,42 +158,17 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
|
|||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.menu_save_categories:
|
||||
|
||||
int numberSelected = 0;
|
||||
|
||||
selectedCategories = new ArrayList<>();
|
||||
int count = categoriesAdapter.getItemCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
CategoryItem item = categoriesAdapter.getItem(i);
|
||||
if (item.isSelected()) {
|
||||
selectedCategories.add(item.getName());
|
||||
numberSelected++;
|
||||
}
|
||||
}
|
||||
|
||||
//If no categories selected, display warning to user
|
||||
if (numberSelected == 0) {
|
||||
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("No, go back", (dialog, id) -> {
|
||||
//Exit menuItem so user can select their categories
|
||||
})
|
||||
.setNegativeButton("Yes, submit", (dialog, id) -> {
|
||||
//Proceed to submission
|
||||
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
if (selectedCategories.size() > 0) {
|
||||
//Some categories selected, proceed to submission
|
||||
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
|
||||
} else {
|
||||
//Proceed to submission
|
||||
onCategoriesSaveHandler.onCategoriesSave(selectedCategories);
|
||||
return true;
|
||||
//No categories selected, prompt the user to select some
|
||||
showConfirmationDialog();
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(menuItem);
|
||||
}
|
||||
return super.onOptionsItemSelected(menuItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -209,249 +177,161 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
|
|||
setHasOptionsMenu(true);
|
||||
onCategoriesSaveHandler = (OnCategoriesSaveHandler) getActivity();
|
||||
getActivity().setTitle(R.string.categories_activity_title);
|
||||
client = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
|
||||
databaseClient = getActivity().getContentResolver().acquireContentProviderClient(AUTHORITY);
|
||||
}
|
||||
|
||||
public HashMap<String, ArrayList<String>> getCategoriesCache() {
|
||||
return categoriesCache;
|
||||
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()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
s -> categoriesAdapter.add(s),
|
||||
throwable -> Timber.e(throwable),
|
||||
() -> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves category suggestions from title input
|
||||
*
|
||||
* @return a list containing title-related categories
|
||||
*/
|
||||
private ArrayList<String> titleCatQuery() {
|
||||
TitleCategories titleCategoriesSub;
|
||||
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() {
|
||||
return gpsCategories()
|
||||
.concatWith(titleCategories())
|
||||
.concatWith(recentCategories());
|
||||
}
|
||||
|
||||
private Observable<CategoryItem> gpsCategories() {
|
||||
return Observable.fromIterable(
|
||||
MwVolleyApi.GpsCatExists.getGpsCatExists()
|
||||
? MwVolleyApi.getGpsCat() : new ArrayList<>())
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
private Observable<CategoryItem> titleCategories() {
|
||||
//Retrieve the title that was saved when user tapped submit icon
|
||||
SharedPreferences titleDesc = PreferenceManager.getDefaultSharedPreferences(getActivity());
|
||||
String title = titleDesc.getString("Title", "");
|
||||
Timber.d("Title: %s", title);
|
||||
|
||||
//Override onPostExecute to access the results of async API call
|
||||
titleCategoriesSub = new TitleCategories(title) {
|
||||
@Override
|
||||
protected void onPostExecute(List<String> result) {
|
||||
super.onPostExecute(result);
|
||||
Timber.d("Results in onPostExecute: %s", result);
|
||||
titleCatItems.addAll(result);
|
||||
Timber.d("TitleCatItems in onPostExecute: %s", titleCatItems);
|
||||
mergeLatch.countDown();
|
||||
}
|
||||
};
|
||||
|
||||
titleCategoriesSub.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
Timber.d("TitleCatItems in titleCatQuery: %s", titleCatItems);
|
||||
|
||||
//Only return titleCatItems after API call has finished
|
||||
try {
|
||||
mergeLatch.await(5L, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Timber.e(e, "Interrupted exception: ");
|
||||
}
|
||||
return titleCatItems;
|
||||
return CommonsApplication.getInstance().getMWApi()
|
||||
.searchTitles(title, SEARCH_CATS_LIMIT)
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves recently-used categories
|
||||
*
|
||||
* @return a list containing recent categories
|
||||
*/
|
||||
private ArrayList<String> recentCatQuery() {
|
||||
ArrayList<String> items = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
Category.Table.COLUMN_LAST_USED + " DESC");
|
||||
// fixme add a limit on the original query instead of falling out of the loop?
|
||||
while (cursor != null && cursor.moveToNext()
|
||||
&& cursor.getPosition() < SEARCH_CATS_LIMIT) {
|
||||
Category cat = Category.fromCursor(cursor);
|
||||
items.add(cat.getName());
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return items;
|
||||
private Observable<CategoryItem> recentCategories() {
|
||||
return Observable.fromIterable(Category.recentCategories(databaseClient, SEARCH_CATS_LIMIT))
|
||||
.map(s -> new CategoryItem(s, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges nearby categories, categories suggested based on title, and recent categories...
|
||||
* without duplicates.
|
||||
*
|
||||
* @return a list containing merged categories
|
||||
*/
|
||||
ArrayList<String> mergeItems() {
|
||||
Set<String> mergedItems = new LinkedHashSet<>();
|
||||
|
||||
Timber.d("Calling APIs for GPS cats, title cats and recent cats...");
|
||||
|
||||
List<String> gpsItems = new ArrayList<>();
|
||||
if (MwVolleyApi.GpsCatExists.getGpsCatExists()) {
|
||||
gpsItems.addAll(MwVolleyApi.getGpsCat());
|
||||
}
|
||||
List<String> titleItems = new ArrayList<>(titleCatQuery());
|
||||
List<String> recentItems = new ArrayList<>(recentCatQuery());
|
||||
|
||||
//Await results of titleItems, which is likely to come in last
|
||||
try {
|
||||
mergeLatch.await(5L, TimeUnit.SECONDS);
|
||||
Timber.d("Waited for merge");
|
||||
} catch (InterruptedException e) {
|
||||
Timber.e(e, "Interrupted Exception: ");
|
||||
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();
|
||||
}
|
||||
|
||||
mergedItems.addAll(gpsItems);
|
||||
Timber.d("Adding GPS items: %s", gpsItems);
|
||||
mergedItems.addAll(titleItems);
|
||||
Timber.d("Adding title items: %s", titleItems);
|
||||
mergedItems.addAll(recentItems);
|
||||
Timber.d("Adding recent items: %s", recentItems);
|
||||
//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));
|
||||
}
|
||||
|
||||
// Needs to be an ArrayList and not a List unless we want to modify a big portion
|
||||
// of preexisting code
|
||||
ArrayList<String> mergedItemsList = new ArrayList<>(mergedItems);
|
||||
|
||||
Timber.d("Merged item list: %s", mergedItemsList);
|
||||
return mergedItemsList;
|
||||
//otherwise, search API for matching categories
|
||||
return CommonsApplication.getInstance().getMWApi()
|
||||
.allCategories(term, SEARCH_CATS_LIMIT)
|
||||
.map(name -> new CategoryItem(name, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays categories found to the user as they type in the search box
|
||||
*
|
||||
* @param categories a list of all categories found for the search string
|
||||
* @param filter the search string
|
||||
*/
|
||||
private void setCatsAfterAsync(ArrayList<String> categories, String filter) {
|
||||
if (getActivity() != null) {
|
||||
ArrayList<CategoryItem> items = new ArrayList<>();
|
||||
HashSet<String> existingKeys = new HashSet<>();
|
||||
int count = categoriesAdapter.getItemCount();
|
||||
for (int i = 0; i < count; i++) {
|
||||
CategoryItem item = categoriesAdapter.getItem(i);
|
||||
if (item.isSelected()) {
|
||||
items.add(item);
|
||||
existingKeys.add(item.getName());
|
||||
}
|
||||
}
|
||||
for (String category : categories) {
|
||||
if (!existingKeys.contains(category)) {
|
||||
items.add(new CategoryItem(category, false));
|
||||
}
|
||||
}
|
||||
|
||||
categoriesAdapter.setCollection(new ListAdapteeCollection<>(items));
|
||||
categoriesAdapter.notifyDataSetChanged();
|
||||
categoriesSearchInProgress.setVisibility(View.GONE);
|
||||
|
||||
if (categories.isEmpty()) {
|
||||
if (TextUtils.isEmpty(filter)) {
|
||||
// If we found no recent cats, show the skip message!
|
||||
categoriesSkip.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
categoriesNotFoundView.setText(getString(R.string.categories_not_found, filter));
|
||||
categoriesNotFoundView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
categoriesList.smoothScrollToPosition(existingKeys.size());
|
||||
}
|
||||
} else {
|
||||
Timber.e("Error: Fragment is null");
|
||||
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 CommonsApplication.getInstance().getMWApi()
|
||||
.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);
|
||||
|
||||
/**
|
||||
* Makes asynchronous calls to the Commons MediaWiki API via anonymous subclasses of
|
||||
* 'MethodAUpdater' and 'PrefixUpdater'. Some of their methods are overridden in order to
|
||||
* aggregate the results. A CountDownLatch is used to ensure that MethodA results are shown
|
||||
* above Prefix results.
|
||||
*/
|
||||
private void requestSearchResults() {
|
||||
final CountDownLatch latch = new CountDownLatch(1);
|
||||
int prevYear = year - 1;
|
||||
String prevYearInString = String.valueOf(prevYear);
|
||||
Timber.d("Previous year: %s", prevYearInString);
|
||||
|
||||
prefixUpdaterSub = new PrefixUpdater(this) {
|
||||
@Override
|
||||
protected List<String> doInBackground(Void... voids) {
|
||||
List<String> result = new ArrayList<>();
|
||||
try {
|
||||
result = super.doInBackground();
|
||||
latch.await();
|
||||
} catch (InterruptedException e) {
|
||||
Timber.w(e);
|
||||
//Thread.currentThread().interrupt();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<String> result) {
|
||||
super.onPostExecute(result);
|
||||
|
||||
results.addAll(result);
|
||||
Timber.d("Prefix result: %s", result);
|
||||
|
||||
String filter = categoriesFilter.getText().toString();
|
||||
ArrayList<String> resultsList = new ArrayList<>(results);
|
||||
categoriesCache.put(filter, resultsList);
|
||||
Timber.d("Final results List: %s", resultsList);
|
||||
|
||||
categoriesAdapter.notifyDataSetChanged();
|
||||
setCatsAfterAsync(resultsList, filter);
|
||||
}
|
||||
};
|
||||
|
||||
methodAUpdaterSub = new MethodAUpdater(this) {
|
||||
@Override
|
||||
protected void onPostExecute(List<String> result) {
|
||||
results.clear();
|
||||
super.onPostExecute(result);
|
||||
|
||||
results.addAll(result);
|
||||
Timber.d("Method A result: %s", result);
|
||||
categoriesAdapter.notifyDataSetChanged();
|
||||
|
||||
latch.countDown();
|
||||
}
|
||||
};
|
||||
prefixUpdaterSub.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
methodAUpdaterSub.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
//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)
|
||||
return ((item.matches(".*(19|20)\\d{2}.*") && !item.contains(yearInString) && !item.contains(prevYearInString))
|
||||
|| item.matches("(.*)needing(.*)") || item.matches("(.*)taken on(.*)"));
|
||||
}
|
||||
|
||||
private void startUpdatingCategoryList() {
|
||||
if (prefixUpdaterSub != null) {
|
||||
prefixUpdaterSub.cancel(true);
|
||||
private void updateCategoryCount(CategoryItem item, ContentProviderClient client) {
|
||||
Category cat = lookupCategory(item.getName());
|
||||
cat.incTimesUsed();
|
||||
cat.save(client);
|
||||
}
|
||||
|
||||
private Category lookupCategory(String name) {
|
||||
Category cat = Category.find(databaseClient, name);
|
||||
|
||||
if (cat == null) {
|
||||
// Newly used category...
|
||||
cat = new Category();
|
||||
cat.setName(name);
|
||||
cat.setLastUsed(new Date());
|
||||
cat.setTimesUsed(0);
|
||||
}
|
||||
|
||||
if (methodAUpdaterSub != null) {
|
||||
methodAUpdaterSub.cancel(true);
|
||||
}
|
||||
|
||||
requestSearchResults();
|
||||
return cat;
|
||||
}
|
||||
|
||||
public int getCurrentSelectedCount() {
|
||||
int count = 0;
|
||||
int numberOfItems = categoriesAdapter.getItemCount();
|
||||
for (int i = 0; i < numberOfItems; i++) {
|
||||
CategoryItem item = categoriesAdapter.getItem(i);
|
||||
if (item.isSelected()) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
return selectedCategories.size();
|
||||
}
|
||||
|
||||
public void backButtonDialog() {
|
||||
/**
|
||||
* 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.")
|
||||
|
|
@ -464,26 +344,20 @@ public class CategorizationFragment extends Fragment implements CategoryClickedL
|
|||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void categoryClicked(CategoryItem item) {
|
||||
if (item.isSelected()) {
|
||||
new CategoryCountUpdater(item.getName(), client).executeOnExecutor(executor);
|
||||
}
|
||||
}
|
||||
|
||||
private class CategoryTextWatcher 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) {
|
||||
startUpdatingCategoryList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
|
||||
}
|
||||
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("No, go back", (dialog, id) -> {
|
||||
//Exit menuItem so user can select their categories
|
||||
})
|
||||
.setNegativeButton("Yes, submit", (dialog, id) -> {
|
||||
//Proceed to submission
|
||||
onCategoriesSaveHandler.onCategoriesSave(getStringList(selectedCategories));
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import android.support.annotation.NonNull;
|
|||
import android.text.TextUtils;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.data.Category;
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
class CategoryCountUpdater extends AsyncTask<Void, Void, Void> {
|
||||
|
||||
private final String name;
|
||||
private final ContentProviderClient client;
|
||||
|
||||
CategoryCountUpdater(String name, ContentProviderClient client) {
|
||||
this.name = name;
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... voids) {
|
||||
Category cat = lookupCategory(name);
|
||||
cat.incTimesUsed();
|
||||
|
||||
cat.setContentProviderClient(client);
|
||||
cat.save();
|
||||
|
||||
return null; // Make the compiler happy.
|
||||
}
|
||||
|
||||
private Category lookupCategory(String name) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
Category.Table.COLUMN_NAME + "=?",
|
||||
new String[]{name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return Category.fromCursor(cursor);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// This feels lazy, but to hell with checked exceptions. :)
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
// Newly used category...
|
||||
Category cat = new Category();
|
||||
cat.setName(name);
|
||||
cat.setLastUsed(new Date());
|
||||
cat.setTimesUsed(0);
|
||||
return cat;
|
||||
}
|
||||
}
|
||||
|
|
@ -51,4 +51,24 @@ class CategoryItem implements Parcelable {
|
|||
parcel.writeString(name);
|
||||
parcel.writeInt(selected ? 1 : 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CategoryItem that = (CategoryItem) o;
|
||||
|
||||
return name.equals(that.name);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.view.View;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.category.CategorizationFragment.SEARCH_CATS_LIMIT;
|
||||
|
||||
/**
|
||||
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are close to
|
||||
* the keyword typed in by the user. The 'srsearch' action-specific parameter is used for this
|
||||
* purpose. This class should be subclassed in CategorizationFragment.java to aggregate the results.
|
||||
*/
|
||||
class MethodAUpdater extends AsyncTask<Void, Void, List<String>> {
|
||||
|
||||
private final CategorizationFragment catFragment;
|
||||
private String filter;
|
||||
|
||||
MethodAUpdater(CategorizationFragment catFragment) {
|
||||
this.catFragment = catFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
filter = catFragment.categoriesFilter.getText().toString();
|
||||
catFragment.categoriesSearchInProgress.setVisibility(View.VISIBLE);
|
||||
catFragment.categoriesNotFoundView.setVisibility(View.GONE);
|
||||
|
||||
catFragment.categoriesSkip.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove categories that contain a year in them (starting with 19__ or 20__), except for this year
|
||||
* and previous year
|
||||
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47
|
||||
*
|
||||
* @param items Unfiltered list of categories
|
||||
* @return Filtered category list
|
||||
*/
|
||||
private List<String> filterYears(List<String> items) {
|
||||
|
||||
Iterator<String> iterator;
|
||||
|
||||
//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);
|
||||
Timber.d("Year: %s", yearInString);
|
||||
|
||||
int prevYear = year - 1;
|
||||
String prevYearInString = String.valueOf(prevYear);
|
||||
Timber.d("Previous year: %s", prevYearInString);
|
||||
|
||||
//Copy to Iterator to prevent ConcurrentModificationException when removing item
|
||||
for (iterator = items.iterator(); iterator.hasNext(); ) {
|
||||
String s = iterator.next();
|
||||
|
||||
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard)
|
||||
//And that s does not equal the current year or previous year
|
||||
if (s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString)) {
|
||||
Timber.d("Filtering out year %s", s);
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
Timber.d("Items: %s", items);
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> doInBackground(Void... voids) {
|
||||
|
||||
//otherwise if user has typed something in that isn't in cache, search API for matching categories
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
List<String> categories = new ArrayList<>();
|
||||
|
||||
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
|
||||
try {
|
||||
categories = api.searchCategories(SEARCH_CATS_LIMIT, filter);
|
||||
Timber.d("Method A URL filter %s", categories);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "IO Exception: ");
|
||||
//Return empty arraylist
|
||||
return categories;
|
||||
}
|
||||
|
||||
Timber.d("Found categories from Method A search, waiting for filter");
|
||||
return new ArrayList<>(filterYears(categories));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public interface OnCategoriesSaveHandler {
|
||||
void onCategoriesSave(ArrayList<String> categories);
|
||||
void onCategoriesSave(List<String> categories);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.category.CategorizationFragment.SEARCH_CATS_LIMIT;
|
||||
|
||||
/**
|
||||
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that share the
|
||||
* same prefix as the keyword typed in by the user. The 'acprefix' action-specific parameter is used
|
||||
* for this purpose. This class should be subclassed in CategorizationFragment.java to aggregate
|
||||
* the results.
|
||||
*/
|
||||
class PrefixUpdater extends AsyncTask<Void, Void, List<String>> {
|
||||
|
||||
private final CategorizationFragment catFragment;
|
||||
private String filter;
|
||||
|
||||
PrefixUpdater(CategorizationFragment catFragment) {
|
||||
this.catFragment = catFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
super.onPreExecute();
|
||||
filter = catFragment.categoriesFilter.getText().toString();
|
||||
catFragment.categoriesSearchInProgress.setVisibility(View.VISIBLE);
|
||||
catFragment.categoriesNotFoundView.setVisibility(View.GONE);
|
||||
|
||||
catFragment.categoriesSkip.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove categories that contain a year in them (starting with 19__ or 20__), except for this year
|
||||
* and previous year
|
||||
* Rationale: https://github.com/commons-app/apps-android-commons/issues/47
|
||||
*
|
||||
* @param items Unfiltered list of categories
|
||||
* @return Filtered category list
|
||||
*/
|
||||
private List<String> filterIrrelevantResults(List<String> items) {
|
||||
|
||||
Iterator<String> iterator;
|
||||
|
||||
//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);
|
||||
Timber.d("Year: %s", yearInString);
|
||||
|
||||
int prevYear = year - 1;
|
||||
String prevYearInString = String.valueOf(prevYear);
|
||||
Timber.d("Previous year: %s", prevYearInString);
|
||||
|
||||
//Copy to Iterator to prevent ConcurrentModificationException when removing item
|
||||
for (iterator = items.iterator(); iterator.hasNext();) {
|
||||
String s = iterator.next();
|
||||
|
||||
//Check if s contains a 4-digit word anywhere within the string (.* is wildcard)
|
||||
//And that s 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)
|
||||
if ((s.matches(".*(19|20)\\d{2}.*") && !s.contains(yearInString) && !s.contains(prevYearInString))
|
||||
|| s.matches("(.*)needing(.*)")||s.matches("(.*)taken on(.*)")) {
|
||||
Timber.d("Filtering out irrelevant result: %s", s);
|
||||
iterator.remove();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Timber.d("Items: %s", items);
|
||||
return items;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> doInBackground(Void... voids) {
|
||||
//If user hasn't typed anything in yet, get GPS and recent items
|
||||
if (TextUtils.isEmpty(filter)) {
|
||||
ArrayList<String> mergedItems = new ArrayList<>(catFragment.mergeItems());
|
||||
Timber.d("Merged items, waiting for filter");
|
||||
return new ArrayList<>(filterIrrelevantResults(mergedItems));
|
||||
}
|
||||
|
||||
//if user types in something that is in cache, return cached category
|
||||
HashMap<String, ArrayList<String>> categoriesCache = catFragment.getCategoriesCache();
|
||||
if (categoriesCache.containsKey(filter)) {
|
||||
ArrayList<String> cachedItems = new ArrayList<>(categoriesCache.get(filter));
|
||||
Timber.d("Found cache items, waiting for filter");
|
||||
return new ArrayList<>(filterIrrelevantResults(cachedItems));
|
||||
}
|
||||
|
||||
//otherwise if user has typed something in that isn't in cache, search API for matching categories
|
||||
//URL: https://commons.wikimedia.org/w/api.php?action=query&list=allcategories&acprefix=filter&aclimit=25
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
List<String> categories = new ArrayList<>();
|
||||
try {
|
||||
categories = api.allCategories(SEARCH_CATS_LIMIT, this.filter);
|
||||
Timber.d("Prefix URL filter %s", categories);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "IO Exception: ");
|
||||
//Return empty arraylist
|
||||
return categories;
|
||||
}
|
||||
|
||||
Timber.d("Found categories from Prefix search, waiting for filter");
|
||||
return new ArrayList<>(filterIrrelevantResults(categories));
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.mwapi.MediaWikiApi;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Sends asynchronous queries to the Commons MediaWiki API to retrieve categories that are related to
|
||||
* the title entered in previous screen. The 'srsearch' action-specific parameter is used for this
|
||||
* purpose. This class should be subclassed in CategorizationFragment.java to add the results to recent and GPS cats.
|
||||
*/
|
||||
class TitleCategories extends AsyncTask<Void, Void, List<String>> {
|
||||
|
||||
private final static int SEARCH_CATS_LIMIT = 25;
|
||||
|
||||
private final String title;
|
||||
|
||||
TitleCategories(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> doInBackground(Void... voids) {
|
||||
|
||||
MediaWikiApi api = CommonsApplication.getInstance().getMWApi();
|
||||
List<String> titleCategories = new ArrayList<>();
|
||||
|
||||
//URL https://commons.wikimedia.org/w/api.php?action=query&format=xml&list=search&srwhat=text&srenablerewrites=1&srnamespace=14&srlimit=10&srsearch=
|
||||
try {
|
||||
titleCategories = api.searchTitles(SEARCH_CATS_LIMIT, this.title);
|
||||
} catch (IOException e) {
|
||||
Timber.e(e, "IO Exception: ");
|
||||
//Return empty arraylist
|
||||
return titleCategories;
|
||||
}
|
||||
|
||||
Timber.d("Title cat query results: %s", titleCategories);
|
||||
|
||||
return titleCategories;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package fr.free.nrw.commons.category;
|
||||
package fr.free.nrw.commons.data;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
|
|
@ -6,11 +6,15 @@ import android.database.Cursor;
|
|||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
|
||||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||
|
||||
public class Category {
|
||||
private ContentProviderClient client;
|
||||
private Uri contentUri;
|
||||
|
||||
private String name;
|
||||
|
|
@ -53,12 +57,13 @@ public class Category {
|
|||
touch();
|
||||
}
|
||||
|
||||
// Database/content-provider stuff
|
||||
public void setContentProviderClient(ContentProviderClient client) {
|
||||
this.client = client;
|
||||
}
|
||||
//region Database/content-provider stuff
|
||||
|
||||
public void save() {
|
||||
/**
|
||||
* Persist category.
|
||||
* @param client ContentProviderClient to handle DB connection
|
||||
*/
|
||||
public void save(ContentProviderClient client) {
|
||||
try {
|
||||
if (contentUri == null) {
|
||||
contentUri = client.insert(CategoryContentProvider.BASE_URI, this.toContentValues());
|
||||
|
|
@ -78,7 +83,7 @@ public class Category {
|
|||
return cv;
|
||||
}
|
||||
|
||||
public static Category fromCursor(Cursor cursor) {
|
||||
private static Category fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
Category c = new Category();
|
||||
c.contentUri = CategoryContentProvider.uriForId(cursor.getInt(0));
|
||||
|
|
@ -88,6 +93,65 @@ public class Category {
|
|||
return c;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persisted category in database, based on its name.
|
||||
* @param client ContentProviderClient to handle DB connection
|
||||
* @param name Category's name
|
||||
* @return category from database, or null if not found
|
||||
*/
|
||||
public static @Nullable Category find(ContentProviderClient client, String name) {
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
Category.Table.COLUMN_NAME + "=?",
|
||||
new String[]{name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return Category.fromCursor(cursor);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
// This feels lazy, but to hell with checked exceptions. :)
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recently-used categories, ordered by descending date.
|
||||
* @return a list containing recent categories
|
||||
*/
|
||||
public static @NonNull ArrayList<String> recentCategories(ContentProviderClient client, int limit) {
|
||||
ArrayList<String> items = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = client.query(
|
||||
CategoryContentProvider.BASE_URI,
|
||||
Category.Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
Category.Table.COLUMN_LAST_USED + " DESC");
|
||||
// fixme add a limit on the original query instead of falling out of the loop?
|
||||
while (cursor != null && cursor.moveToNext()
|
||||
&& cursor.getPosition() < limit) {
|
||||
Category cat = Category.fromCursor(cursor);
|
||||
items.add(cat.getName());
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "categories";
|
||||
|
||||
|
|
@ -144,4 +208,5 @@ public class Category {
|
|||
}
|
||||
}
|
||||
}
|
||||
//endregion
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@ import android.content.Context;
|
|||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
import fr.free.nrw.commons.category.Category;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.modifications.ModifierSequence;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ import fr.free.nrw.commons.MediaWikiImageView;
|
|||
import fr.free.nrw.commons.PageTitle;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaDetailFragment extends Fragment {
|
||||
|
|
@ -211,22 +212,8 @@ public class MediaDetailFragment extends Fragment {
|
|||
if (success) {
|
||||
extractor.fill(media);
|
||||
|
||||
// Set text of desc, license, and categories
|
||||
desc.setText(prettyDescription(media));
|
||||
license.setText(prettyLicense(media));
|
||||
coordinates.setText(prettyCoordinates(media));
|
||||
uploadedDate.setText(prettyUploadedDate(media));
|
||||
|
||||
categoryNames.clear();
|
||||
categoryNames.addAll(media.getCategories());
|
||||
|
||||
categoriesLoaded = true;
|
||||
categoriesPresent = (categoryNames.size() > 0);
|
||||
if (!categoriesPresent) {
|
||||
// Stick in a filler element.
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_none));
|
||||
}
|
||||
rebuildCatList();
|
||||
setTextFields(media);
|
||||
setOnClickListeners(media);
|
||||
} else {
|
||||
Timber.d("Failed to load photo details.");
|
||||
}
|
||||
|
|
@ -260,6 +247,31 @@ public class MediaDetailFragment extends Fragment {
|
|||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private void setTextFields(Media media) {
|
||||
desc.setText(prettyDescription(media));
|
||||
license.setText(prettyLicense(media));
|
||||
coordinates.setText(prettyCoordinates(media));
|
||||
uploadedDate.setText(prettyUploadedDate(media));
|
||||
|
||||
categoryNames.clear();
|
||||
categoryNames.addAll(media.getCategories());
|
||||
|
||||
categoriesLoaded = true;
|
||||
categoriesPresent = (categoryNames.size() > 0);
|
||||
if (!categoriesPresent) {
|
||||
// Stick in a filler element.
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_none));
|
||||
}
|
||||
rebuildCatList();
|
||||
}
|
||||
|
||||
private void setOnClickListeners(final Media media) {
|
||||
license.setOnClickListener(v -> openWebBrowser(licenseLink(media)));
|
||||
if (media.getCoordinates() != null) {
|
||||
coordinates.setOnClickListener(v -> openMap(media.getCoordinates()));
|
||||
}
|
||||
}
|
||||
|
||||
private void rebuildCatList() {
|
||||
categoryContainer.removeAllViews();
|
||||
// @fixme add the category items
|
||||
|
|
@ -271,7 +283,7 @@ public class MediaDetailFragment extends Fragment {
|
|||
|
||||
private View buildCatLabel(final String catName) {
|
||||
final View item = getLayoutInflater(null).inflate(R.layout.detail_category_item, null, false);
|
||||
final TextView textView = (TextView)item.findViewById(R.id.mediaDetailCategoryItemText);
|
||||
final CompatTextView textView = (CompatTextView)item.findViewById(R.id.mediaDetailCategoryItemText);
|
||||
|
||||
textView.setText(catName);
|
||||
if (categoriesLoaded && categoriesPresent) {
|
||||
|
|
@ -342,4 +354,34 @@ public class MediaDetailFragment extends Fragment {
|
|||
}
|
||||
return media.getCoordinates().getPrettyCoordinateString();
|
||||
}
|
||||
|
||||
|
||||
private @Nullable String licenseLink(Media media) {
|
||||
String licenseKey = media.getLicense();
|
||||
if (licenseKey == null || licenseKey.equals("")) {
|
||||
return null;
|
||||
}
|
||||
License licenseObj = licenseList.get(licenseKey);
|
||||
if (licenseObj == null) {
|
||||
return null;
|
||||
} else {
|
||||
return licenseObj.getUrl(Locale.getDefault().getLanguage());
|
||||
}
|
||||
}
|
||||
|
||||
private void openWebBrowser(String url) {
|
||||
Intent browser = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
|
||||
startActivity(browser);
|
||||
}
|
||||
|
||||
private void openMap(LatLng coordinates) {
|
||||
//Open map app at given position
|
||||
Uri gmmIntentUri = Uri.parse(
|
||||
"geo:0,0?q=" + coordinates.getLatitude() + "," + coordinates.getLatitude());
|
||||
Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri);
|
||||
|
||||
if (mapIntent.resolveActivity(getActivity().getPackageManager()) != null) {
|
||||
startActivity(mapIntent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ import fr.free.nrw.commons.BuildConfig;
|
|||
import fr.free.nrw.commons.PageTitle;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import in.yuvi.http.fluent.Http;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
|
@ -205,78 +206,104 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi {
|
|||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<String> searchCategories(int searchCatsLimit, String filterValue) throws IOException {
|
||||
List<ApiResult> categoryNodes = api.action("query")
|
||||
.param("format", "xml")
|
||||
.param("list", "search")
|
||||
.param("srwhat", "text")
|
||||
.param("srnamespace", "14")
|
||||
.param("srlimit", searchCatsLimit)
|
||||
.param("srsearch", filterValue)
|
||||
.get()
|
||||
.getNodes("/api/query/search/p/@title");
|
||||
public Observable<String> searchCategories(String filterValue, int searchCatsLimit) {
|
||||
return Single.fromCallable(() -> {
|
||||
List<ApiResult> categoryNodes = null;
|
||||
try {
|
||||
categoryNodes = api.action("query")
|
||||
.param("format", "xml")
|
||||
.param("list", "search")
|
||||
.param("srwhat", "text")
|
||||
.param("srnamespace", "14")
|
||||
.param("srlimit", searchCatsLimit)
|
||||
.param("srsearch", filterValue)
|
||||
.get()
|
||||
.getNodes("/api/query/search/p/@title");
|
||||
} catch (IOException e) {
|
||||
Timber.e("Failed to obtain searchCategories", e);
|
||||
}
|
||||
|
||||
if (categoryNodes == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (categoryNodes == null) {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
|
||||
List<String> categories = new ArrayList<>();
|
||||
for (ApiResult categoryNode : categoryNodes) {
|
||||
String cat = categoryNode.getDocument().getTextContent();
|
||||
String catString = cat.replace("Category:", "");
|
||||
categories.add(catString);
|
||||
}
|
||||
List<String> categories = new ArrayList<>();
|
||||
for (ApiResult categoryNode : categoryNodes) {
|
||||
String cat = categoryNode.getDocument().getTextContent();
|
||||
String catString = cat.replace("Category:", "");
|
||||
categories.add(catString);
|
||||
}
|
||||
|
||||
return categories;
|
||||
return categories;
|
||||
})
|
||||
.flatMapObservable(list -> Observable.fromIterable(list));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<String> allCategories(int searchCatsLimit, String filterValue) throws IOException {
|
||||
ArrayList<ApiResult> categoryNodes = api.action("query")
|
||||
.param("list", "allcategories")
|
||||
.param("acprefix", filterValue)
|
||||
.param("aclimit", searchCatsLimit)
|
||||
.get()
|
||||
.getNodes("/api/query/allcategories/c");
|
||||
public Observable<String> allCategories(String filterValue, int searchCatsLimit) {
|
||||
return Single.fromCallable(() -> {
|
||||
ArrayList<ApiResult> categoryNodes = null;
|
||||
try {
|
||||
categoryNodes = api.action("query")
|
||||
.param("list", "allcategories")
|
||||
.param("acprefix", filterValue)
|
||||
.param("aclimit", searchCatsLimit)
|
||||
.get()
|
||||
.getNodes("/api/query/allcategories/c");
|
||||
} catch (IOException e) {
|
||||
Timber.e("Failed to obtain allCategories", e);
|
||||
}
|
||||
|
||||
if (categoryNodes == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (categoryNodes == null) {
|
||||
return new ArrayList<String>();
|
||||
}
|
||||
|
||||
List<String> categories = new ArrayList<>();
|
||||
for (ApiResult categoryNode : categoryNodes) {
|
||||
categories.add(categoryNode.getDocument().getTextContent());
|
||||
}
|
||||
List<String> categories = new ArrayList<>();
|
||||
for (ApiResult categoryNode : categoryNodes) {
|
||||
categories.add(categoryNode.getDocument().getTextContent());
|
||||
}
|
||||
|
||||
return categories;
|
||||
return categories;
|
||||
})
|
||||
.flatMapObservable(list -> Observable.fromIterable(list));
|
||||
}
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public List<String> searchTitles(int searchCatsLimit, String title) throws IOException {
|
||||
ArrayList<ApiResult> categoryNodes = api.action("query")
|
||||
.param("format", "xml")
|
||||
.param("list", "search")
|
||||
.param("srwhat", "text")
|
||||
.param("srnamespace", "14")
|
||||
.param("srlimit", searchCatsLimit)
|
||||
.param("srsearch", title)
|
||||
.get()
|
||||
.getNodes("/api/query/search/p/@title");
|
||||
public Observable<String> searchTitles(String title, int searchCatsLimit) {
|
||||
return Single.fromCallable(() -> {
|
||||
ArrayList<ApiResult> categoryNodes = null;
|
||||
|
||||
if (categoryNodes == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
try {
|
||||
categoryNodes = api.action("query")
|
||||
.param("format", "xml")
|
||||
.param("list", "search")
|
||||
.param("srwhat", "text")
|
||||
.param("srnamespace", "14")
|
||||
.param("srlimit", searchCatsLimit)
|
||||
.param("srsearch", title)
|
||||
.get()
|
||||
.getNodes("/api/query/search/p/@title");
|
||||
} catch (IOException e) {
|
||||
Timber.e("Failed to obtain searchTitles", e);
|
||||
return new ArrayList();
|
||||
}
|
||||
|
||||
List<String> titleCategories = new ArrayList<>();
|
||||
for (ApiResult categoryNode : categoryNodes) {
|
||||
String cat = categoryNode.getDocument().getTextContent();
|
||||
String catString = cat.replace("Category:", "");
|
||||
titleCategories.add(catString);
|
||||
}
|
||||
if (categoryNodes == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
return titleCategories;
|
||||
List<String> titleCategories = new ArrayList<>();
|
||||
for (ApiResult categoryNode : categoryNodes) {
|
||||
String cat = categoryNode.getDocument().getTextContent();
|
||||
String catString = cat.replace("Category:", "");
|
||||
titleCategories.add(catString);
|
||||
}
|
||||
|
||||
return titleCategories;
|
||||
})
|
||||
.flatMapObservable(list -> Observable.fromIterable(list));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
public interface MediaWikiApi {
|
||||
|
|
@ -38,13 +39,13 @@ public interface MediaWikiApi {
|
|||
MediaResult fetchMediaByFilename(String filename) throws IOException;
|
||||
|
||||
@NonNull
|
||||
List<String> searchCategories(int searchCatsLimit, String filterValue) throws IOException;
|
||||
Observable<String> searchCategories(String filterValue, int searchCatsLimit);
|
||||
|
||||
@NonNull
|
||||
List<String> allCategories(int searchCatsLimit, String filter) throws IOException;
|
||||
Observable<String> allCategories(String filter, int searchCatsLimit);
|
||||
|
||||
@NonNull
|
||||
List<String> searchTitles(int searchCatsLimit, String title) throws IOException;
|
||||
Observable<String> searchTitles(String title, int searchCatsLimit);
|
||||
|
||||
@Nullable
|
||||
String revisionsByFilename(String filename) throws IOException;
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ public class NearbyMapFragment extends android.support.v4.app.Fragment {
|
|||
|
||||
addCurrentLocationMarker(mapboxMap);
|
||||
});
|
||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",true)) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",false)) {
|
||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_dark));
|
||||
} else {
|
||||
mapView.setStyleUrl(getResources().getString(R.string.map_theme_light));
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ public class SettingsActivity extends NavigationBaseActivity {
|
|||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
// Check prefs on every activity starts
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",true)) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false)) {
|
||||
setTheme(R.style.DarkAppTheme);
|
||||
} else {
|
||||
setTheme(R.style.LightAppTheme);
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ public class BaseActivity extends AppCompatActivity {
|
|||
@Override
|
||||
protected void onResume() {
|
||||
// Restart activity if theme is changed
|
||||
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",true);
|
||||
boolean newTheme = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("theme",false);
|
||||
if(currentTheme!=newTheme){ //is activity theme changed
|
||||
Intent intent = getIntent();
|
||||
finish();
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
package fr.free.nrw.commons.theme;
|
||||
|
||||
import android.accounts.Account;
|
||||
import android.accounts.AccountManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
|
|
@ -10,9 +12,11 @@ import android.support.v4.widget.DrawerLayout;
|
|||
import android.support.v7.app.ActionBarDrawerToggle;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.util.Log;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import butterknife.BindView;
|
||||
|
|
@ -39,9 +43,15 @@ public class NavigationBaseActivity extends BaseActivity
|
|||
DrawerLayout drawerLayout;
|
||||
|
||||
private ActionBarDrawerToggle toggle;
|
||||
private String username;
|
||||
private TextView usernameTextView;
|
||||
|
||||
public void initDrawer() {
|
||||
navigationView.setNavigationItemSelectedListener(this);
|
||||
username = CommonsApplication.getInstance().getCurrentAccount().name;
|
||||
usernameTextView = ((TextView) navigationView.getHeaderView(0)
|
||||
.findViewById(R.id.userNameText));
|
||||
usernameTextView.setText(username != null ? username : "");
|
||||
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,73 @@
|
|||
package fr.free.nrw.commons.ui.widget;
|
||||
|
||||
/**
|
||||
* Created by mikel on 07/08/2017.
|
||||
*/
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v7.widget.AppCompatDrawableManager;
|
||||
import android.support.v7.widget.AppCompatTextView;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.utils.UiUtils;
|
||||
|
||||
public class CompatTextView extends AppCompatTextView {
|
||||
public CompatTextView(Context context) {
|
||||
super(context);
|
||||
init(null);
|
||||
}
|
||||
|
||||
public CompatTextView(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
public CompatTextView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(attrs);
|
||||
}
|
||||
|
||||
private void init(@Nullable AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
Context context = getContext();
|
||||
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CompatTextView);
|
||||
|
||||
// Obtain DrawableManager used to pull Drawables safely, and check if we're in RTL
|
||||
AppCompatDrawableManager dm = AppCompatDrawableManager.get();
|
||||
boolean rtl = ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_RTL;
|
||||
|
||||
// Grab the compat drawable padding from the XML
|
||||
float drawablePadding = a.getDimension(R.styleable.CompatTextView_drawablePadding, 0);
|
||||
|
||||
// Grab the compat drawable resources from the XML
|
||||
int startDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableStart, 0);
|
||||
int topDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableTop, 0);
|
||||
int endDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableEnd, 0);
|
||||
int bottomDrawableRes = a.getResourceId(R.styleable.CompatTextView_drawableBottom, 0);
|
||||
|
||||
// Load the used drawables, fall back to whatever was set in an "android:"
|
||||
Drawable[] currentDrawables = getCompoundDrawables();
|
||||
Drawable left = startDrawableRes != 0
|
||||
? dm.getDrawable(context, startDrawableRes) : currentDrawables[0];
|
||||
Drawable right = endDrawableRes != 0
|
||||
? dm.getDrawable(context, endDrawableRes) : currentDrawables[1];
|
||||
Drawable top = topDrawableRes != 0
|
||||
? dm.getDrawable(context, topDrawableRes) : currentDrawables[2];
|
||||
Drawable bottom = bottomDrawableRes != 0
|
||||
? dm.getDrawable(context, bottomDrawableRes) : currentDrawables[3];
|
||||
|
||||
// Account for RTL and apply the compound Drawables
|
||||
Drawable start = rtl ? right : left;
|
||||
Drawable end = rtl ? left : right;
|
||||
setCompoundDrawablesWithIntrinsicBounds(start, top, end, bottom);
|
||||
setCompoundDrawablePadding((int) UiUtils.convertDpToPixel(drawablePadding, getContext()));
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -22,6 +22,7 @@ import android.widget.AdapterView;
|
|||
import android.widget.Toast;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
|
|
@ -160,7 +161,7 @@ public class MultipleShareActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCategoriesSave(ArrayList<String> categories) {
|
||||
public void onCategoriesSave(List<String> categories) {
|
||||
if(categories.size() > 0) {
|
||||
ContentProviderClient client = getContentResolver().acquireContentProviderClient(ModificationsContentProvider.AUTHORITY);
|
||||
for(Contribution contribution: photosList) {
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ public class ShareActivity
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onCategoriesSave(ArrayList<String> categories) {
|
||||
public void onCategoriesSave(List<String> categories) {
|
||||
if(categories.size() > 0) {
|
||||
ModifierSequence categoriesSequence = new ModifierSequence(contribution.getContentUri());
|
||||
|
||||
|
|
@ -525,7 +525,7 @@ public class ShareActivity
|
|||
switch (item.getItemId()) {
|
||||
case android.R.id.home:
|
||||
if(categorizationFragment!=null && categorizationFragment.isVisible()) {
|
||||
categorizationFragment.backButtonDialog();
|
||||
categorizationFragment.showBackButtonDialog();
|
||||
} else {
|
||||
onBackPressed();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ public class SingleUploadFragment extends Fragment {
|
|||
Timber.d(license);
|
||||
|
||||
ArrayAdapter<String> adapter;
|
||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",true)) {
|
||||
if (PreferenceManager.getDefaultSharedPreferences(getActivity()).getBoolean("theme",false)) {
|
||||
// dark theme
|
||||
adapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_dropdown_item, licenseItems);
|
||||
}else {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
package fr.free.nrw.commons.utils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
import android.support.graphics.drawable.VectorDrawableCompat;
|
||||
import android.util.DisplayMetrics;
|
||||
|
||||
public class UiUtils {
|
||||
|
||||
|
|
@ -19,4 +21,26 @@ public class UiUtils {
|
|||
vectorDrawable.draw(canvas);
|
||||
return bitmap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts dp unit to equivalent pixels.
|
||||
* @param dp density independent pixels
|
||||
* @param context Context to access display metrics
|
||||
* @return px equivalent to dp value
|
||||
*/
|
||||
public static float convertDpToPixel(float dp, Context context) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
return dp * ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts device specific pixels to dp.
|
||||
* @param px pixels
|
||||
* @param context Context to access display metrics
|
||||
* @return dp equivalent to px value
|
||||
*/
|
||||
public static float convertPixelsToDp(float px, Context context) {
|
||||
DisplayMetrics metrics = context.getResources().getDisplayMetrics();
|
||||
return px / ((float) metrics.densityDpi / DisplayMetrics.DENSITY_DEFAULT);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_info_outline_white_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_info_outline_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="M11,17h2v-6h-2v6zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8zM11,9h2L13,7h-2v2z"/>
|
||||
</vector>
|
||||
|
|
@ -1,20 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
>
|
||||
|
||||
<TextView
|
||||
<fr.free.nrw.commons.ui.widget.CompatTextView
|
||||
android:id="@+id/mediaDetailCategoryItemText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/subBackground"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:minHeight="48dp"
|
||||
android:padding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:id="@+id/mediaDetailCategoryItemText"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@android:color/white"
|
||||
android:background="?attr/subBackground"
|
||||
android:textSize="14sp"
|
||||
app:drawablePadding="6dp"
|
||||
app:drawableStart="@drawable/ic_info_outline_white_24dp"
|
||||
/>
|
||||
|
||||
<fr.free.nrw.commons.media.MediaDetailSpacer
|
||||
|
|
|
|||
|
|
@ -1,8 +1,27 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/pictureOfTheDay"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="172dp"
|
||||
android:background="@android:color/darker_gray"
|
||||
android:padding="16dp"
|
||||
android:src="@drawable/commons_logo_large"/>
|
||||
android:layout_height="172dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/pictureOfTheDay"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="0.8"
|
||||
android:paddingTop="5dp"
|
||||
android:src="@drawable/commons_logo_large" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/userNameText"
|
||||
style="?android:textAppearanceLargeInverse"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="0.2"
|
||||
android:maxLines="1"
|
||||
android:textAlignment="center"
|
||||
android:text="@string/placeholder_place_name"
|
||||
android:textColor="@android:color/white" />
|
||||
</LinearLayout>
|
||||
|
|
@ -24,6 +24,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:hint="@string/categories_search_text_hint"
|
||||
android:maxLines="1"
|
||||
android:inputType="text"
|
||||
android:imeOptions="flagNoExtractUi"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -26,227 +26,221 @@
|
|||
/>
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/mediaDetailScrollView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/mediaDetailScrollView"
|
||||
android:fillViewport="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:cacheColorHint="@android:color/transparent"
|
||||
>
|
||||
android:fillViewport="true">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
>
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Placeholder. Height gets set at runtime based on container size; the initial value is a hack to keep
|
||||
the detail info offscreen until it's placed properly. May be a better way to do this. -->
|
||||
<fr.free.nrw.commons.media.MediaDetailSpacer
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp"
|
||||
android:id="@+id/mediaDetailSpacer"
|
||||
/>
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/fragmentCategorisationBackground"
|
||||
android:padding="16dp"
|
||||
>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/subBackground"
|
||||
android:padding="16dp"
|
||||
>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:text="@string/media_detail_title"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="6dp"
|
||||
/>
|
||||
android:text="@string/media_detail_title"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediaDetailTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/media_detail_media_title"
|
||||
android:id="@+id/mediaDetailTitle"
|
||||
android:layout_gravity="start"
|
||||
android:textColor="@android:color/white"
|
||||
android:background="?attr/subBackground"
|
||||
android:textSize="14sp"
|
||||
android:padding="12dp"
|
||||
/>
|
||||
android:text="@string/media_detail_media_title"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<fr.free.nrw.commons.media.MediaDetailSpacer
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
/>
|
||||
android:layout_height="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/subBackground"
|
||||
android:padding="16dp"
|
||||
>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:text="@string/media_detail_description"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="6dp"
|
||||
/>
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediaDetailDesc"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/media_detail_description_explanation"
|
||||
android:background="?attr/subBackground"
|
||||
android:id="@+id/mediaDetailDesc"
|
||||
android:textColor="@android:color/white"
|
||||
android:layout_gravity="start"
|
||||
android:textSize="14sp"
|
||||
android:background="?attr/subBackground"
|
||||
android:padding="12dp"
|
||||
/>
|
||||
android:text="@string/media_detail_description_explanation"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
<fr.free.nrw.commons.media.MediaDetailSpacer
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
/>
|
||||
android:layout_height="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/subBackground"
|
||||
android:padding="16dp"
|
||||
>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:text="@string/media_detail_license"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="6dp"
|
||||
/>
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
<fr.free.nrw.commons.ui.widget.CompatTextView
|
||||
android:id="@+id/mediaDetailLicense"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:background="?attr/subBackground"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="12dp"
|
||||
android:text="@string/media_detail_license"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:padding="12dp"
|
||||
app:drawablePadding="6dp"
|
||||
app:drawableStart="@drawable/ic_info_outline_white_24dp"
|
||||
tools:text="License link" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/subBackground"
|
||||
android:padding="16dp"
|
||||
>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/white"
|
||||
android:text="@string/media_detail_coordinates"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="6dp"
|
||||
/>
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/media_detail_coordinates"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<fr.free.nrw.commons.ui.widget.CompatTextView
|
||||
android:id="@+id/mediaDetailCoordinates"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:background="?attr/subBackground"
|
||||
android:foreground="?attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="12dp"
|
||||
android:text="@string/media_detail_coordinates"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp"
|
||||
android:padding="12dp"
|
||||
tools:text="Coordinates link"
|
||||
/>
|
||||
app:drawablePadding="6dp"
|
||||
app:drawableStart="@drawable/ic_map_white_24dp"
|
||||
tools:text="Coordinates link" />
|
||||
</LinearLayout>
|
||||
|
||||
<fr.free.nrw.commons.media.MediaDetailSpacer
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
/>
|
||||
android:layout_height="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/subBackground"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp"
|
||||
android:textStyle="bold"
|
||||
>
|
||||
android:textStyle="bold">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start"
|
||||
android:paddingBottom="6dp"
|
||||
android:text="@string/detail_panel_cats_label"
|
||||
android:textSize="16sp"
|
||||
android:layout_gravity="start"
|
||||
android:textColor="@android:color/white"
|
||||
android:paddingBottom="6dp"
|
||||
/>
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:id="@+id/mediaDetailCategoryContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/mediaDetailCategoryContainer"
|
||||
/>
|
||||
android:orientation="vertical" />
|
||||
</LinearLayout>
|
||||
|
||||
<fr.free.nrw.commons.media.MediaDetailSpacer
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="8dp"
|
||||
/>
|
||||
android:layout_height="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/subBackground"
|
||||
android:padding="16dp"
|
||||
>
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingBottom="6dp"
|
||||
android:text="@string/media_detail_uploaded_date"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="16sp"
|
||||
android:textStyle="bold"
|
||||
android:paddingBottom="6dp"
|
||||
/>
|
||||
android:textStyle="bold" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediaDetailuploadeddate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/media_detail_uploaded_date"
|
||||
android:background="?attr/subBackground"
|
||||
android:id="@+id/mediaDetailuploadeddate"
|
||||
android:textColor="@android:color/white"
|
||||
android:layout_gravity="start"
|
||||
android:textSize="14sp"
|
||||
android:background="?attr/subBackground"
|
||||
android:padding="12dp"
|
||||
/>
|
||||
android:text="@string/media_detail_uploaded_date"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="14sp" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
|||
|
|
@ -137,5 +137,4 @@
|
|||
<string name="navigation_item_feedback">Òpinije</string>
|
||||
<string name="navigation_item_logout">Wëlogùjë</string>
|
||||
<string name="feedback_popup_decline">Nié, dzãkùjã</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">Wir planen mehrere neue Funktionen und Verbesserungen für die App! Möchtest du sie dir anschauen und eine Rückmeldung geben?\n\nDu kannst immer auf diese Meldung zugreifen, indem du in der Navigation „Entwicklerpläne“ auswählst.</string>
|
||||
<string name="feedback_popup_decline">Nein danke</string>
|
||||
<string name="feedback_popup_accept">Sicher, bring mich dorthin!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Entwicklerpläne</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -192,10 +192,11 @@
|
|||
<string name="nearby_info_menu_commons_article">Página del archivo en Commons</string>
|
||||
<string name="nearby_info_menu_wikidata_article">Elemento de Wikidata</string>
|
||||
<string name="error_while_cache">Error mientras se guardaban imágenes en la caché</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 facilmente, 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="feedback_popup_title">Se aprecian comentarios</string>
|
||||
<string name="feedback_popup_description">Estamos planeando muchas funcionalidades nuevas y mejoras para la aplicación. ¿Te gustaría reseñarlas y comentar tu opinión?\n\n(Siempre puedas acceder a esto seleccionando \"Planes de los desarrolladores\" en el cajón de navegación)</string>
|
||||
<string name="feedback_popup_decline">No, gracias</string>
|
||||
<string name="feedback_popup_accept">Seguro, ¡vamos ahí!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Planes de los desarrolladores</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -141,6 +141,5 @@
|
|||
<string name="feedback_popup_description">Aplikaziorako karakteristika eta hobekuntza berriak prestatzen ari gara! Zure iritzia eman nahi diguzu hauen inguruan?\n\n(\"Garatzaile planak\" hautatuz sartu zaitezke)</string>
|
||||
<string name="feedback_popup_decline">Ez, eskerrik asko</string>
|
||||
<string name="feedback_popup_accept">Noski, eraman nazazu hara!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Garatzaile planak</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">Nous planifions plusieurs nouvelles fonctionnalités et améliorations pour l’application ! Voudriez-vous les voir et donner votre avis ? \n\n(Vous pouvez toujours accéder à cela en sélectionnant « Plans du développeur » dans le panneau de navigation)</string>
|
||||
<string name="feedback_popup_decline">Non merci</string>
|
||||
<string name="feedback_popup_accept">Bien sûr, amenez-y-moi !</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Plans du développeur</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">Estamos planeando moitas funcionalidades novas e melloras para a aplicaciónǃ Gustaríalle revisalas e comentar a súa opinión?\n\n(Sempre pode acceder a isto seleccionando \"Plans dos desenvolvedores\" no panel de navegación)</string>
|
||||
<string name="feedback_popup_decline">Non, grazas</string>
|
||||
<string name="feedback_popup_accept">Claro, lévame alí!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Plans de desenvolvedor</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -195,6 +195,5 @@
|
|||
<string name="feedback_popup_description">앱을 위한 일부 새로운 기능과 개선사항을 계획하고 있습니다. 검토해 보시고 의견을 주시겠습니까?\n\n(탐색 표시줄에서 \"개발자 계획\"을 선택하면 여기로 언제나 접근할 수 있습니다)</string>
|
||||
<string name="feedback_popup_decline">괜찮습니다</string>
|
||||
<string name="feedback_popup_accept">물론이죠, 거기 가고싶네요!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">개발자 계획</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,12 @@
|
|||
<string name="warning">Brīdinājums</string>
|
||||
<string name="yes">Jā</string>
|
||||
<string name="no">Nē</string>
|
||||
<string name="media_detail_title">Nosaukums</string>
|
||||
<string name="media_detail_uploaded_date">Augšupielādēšanas datums</string>
|
||||
<string name="media_detail_license">Licence</string>
|
||||
<string name="media_detail_coordinates">Koordinātas</string>
|
||||
<string name="media_detail_coordinates_empty">Nav norādītas</string>
|
||||
<string name="_2fa_code">2FA kods</string>
|
||||
<string name="upload_image">Augšupielādēt attēlu</string>
|
||||
<string name="welcome_image_llamas">Lama</string>
|
||||
<string name="welcome_image_tulip">Tulpe</string>
|
||||
|
|
@ -81,4 +87,6 @@
|
|||
<string name="navigation_item_logout">Iziet</string>
|
||||
<string name="navigation_item_info" fuzzy="true">Ievads</string>
|
||||
<string name="no_description_found">apraksts nav atrasts</string>
|
||||
<string name="feedback_popup_decline">Nē, paldies</string>
|
||||
<string name="navigation_item_developer_plans">Izstrādātāju plāni</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">Планираме неколку нови можности и подобрувања на прилогот! Дали би сакале да ги прегледате и да ни дадете ваше мислење? \n\n(Ова можете да го сторите во секое време, избирајќи го „Планови за развој“ во изборникот)</string>
|
||||
<string name="feedback_popup_decline">Не, благодарам</string>
|
||||
<string name="feedback_popup_accept">Секако, ајде да видам!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Планови за развој</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -135,6 +135,7 @@
|
|||
<string name="location_permission_rationale">Opcjonalne zezwolenie: uzyskiwanie bieżącej lokalizacji dla wygenerowania propozycji kategorii</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="title_activity_nearby">Pobliskie miejsca</string>
|
||||
<string name="no_nearby">Nie znaleziono niczego w pobliżu</string>
|
||||
<string name="warning">Ostrzeżenie</string>
|
||||
<string name="file_exists">Ten plik już istnieje na Commons. Jesteś pewien, że chcesz kontynuować?</string>
|
||||
<string name="yes">Tak</string>
|
||||
|
|
@ -143,6 +144,7 @@
|
|||
<string name="media_detail_media_title">Tytuł pliku</string>
|
||||
<string name="media_detail_description">Opis</string>
|
||||
<string name="media_detail_description_explanation">Tu jest miejsce na opis pliku. Mogą być dość długie i wymagać przewijania podczas czytania. Chcemy, by wszystko wyglądało dobrze.</string>
|
||||
<string name="media_detail_uploaded_date">Data przesłania</string>
|
||||
<string name="media_detail_license">Licencja</string>
|
||||
<string name="media_detail_coordinates">Współrzędne</string>
|
||||
<string name="become_a_tester_title">Zostań beta-testerem</string>
|
||||
|
|
@ -150,6 +152,14 @@
|
|||
<string name="use_wikidata">Użyj Wikidanych</string>
|
||||
<string name="_2fa_code">Kod 2FA</string>
|
||||
<string name="login_failed_2fa_not_supported">Uwierzytelnianie dwuskładnikowe obecnie nie jest obsługiwane.</string>
|
||||
<string name="logout_verification">Czy na pewno wylogować?</string>
|
||||
<string name="commons_logo">Logo Commons</string>
|
||||
<string name="background_image">Obraz w tle</string>
|
||||
<string name="upload_image">Załaduj zdjęcie</string>
|
||||
<string name="welcome_image_mount_zao">Zaō</string>
|
||||
<string name="welcome_image_llamas">Lamy</string>
|
||||
<string name="welcome_image_rainbow_bridge">Rainbow Bridge</string>
|
||||
<string name="welcome_image_tulip">Tulipan</string>
|
||||
<string name="welcome_image_welcome_wikipedia">Witaj na Wikipedii</string>
|
||||
<string name="welcome_image_welcome_copyright" fuzzy="true">Witaj w prawach autorskich.</string>
|
||||
<string name="cancel">Anuluj</string>
|
||||
|
|
@ -157,11 +167,15 @@
|
|||
<string name="navigation_drawer_close">Zamknij</string>
|
||||
<string name="navigation_item_home">Dom</string>
|
||||
<string name="navigation_item_upload">Prześlij</string>
|
||||
<string name="navigation_item_nearby">W pobliżu</string>
|
||||
<string name="navigation_item_about">O aplikacji</string>
|
||||
<string name="navigation_item_settings">Ustawienia</string>
|
||||
<string name="navigation_item_feedback">Opinie</string>
|
||||
<string name="navigation_item_logout">Wyloguj</string>
|
||||
<string name="navigation_item_info">Samouczek</string>
|
||||
<string name="no_description_found">nie znaleziono opisu</string>
|
||||
<string name="nearby_info_menu_wikidata_article">Element Wikidanych</string>
|
||||
<string name="title_info">Podaj krótką, opisową i unikalną nazwę, która będzie służyła jako nazwa pliku. Możesz używać prostego języka i spacji. Nie dodawaj rozszerzenia pliku.</string>
|
||||
<string name="feedback_popup_decline">Nie, dziękuję</string>
|
||||
<string name="feedback_popup_accept">Chętnie!</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">I antivëddoma vàire neuve fonsionalità e ameliorassion për l\'aplicassion! Vorij-lo vëddje e dene soa opinion?\n\n(A peul sempe acede a sòn an selessionand «Pian dël dësvlupador» ant la plancia ëd navigassion)</string>
|
||||
<string name="feedback_popup_decline">Nò, mersì</string>
|
||||
<string name="feedback_popup_accept">Bò, mneme ambelelà!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Pian dël dësvlupador</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -192,10 +192,11 @@
|
|||
<string name="nearby_info_menu_commons_article">Página de arquivo do Commons</string>
|
||||
<string name="nearby_info_menu_wikidata_article">Item do Wikidata</string>
|
||||
<string name="error_while_cache">Erro durante o cache de imagens</string>
|
||||
<string name="title_info">Um título descritivo exclusivo para o arquivo, que servirá como um nome de arquivo. Você pode usar linguagem simples com espaços. Não inclua a extensão do arquivo</string>
|
||||
<string name="description_info">Por favor, descreva a mídia tanto quanto possível: onde foi tomada? O que isso mostra? Qual é o contexto? Descreva os objetos ou pessoas. Revelar informações que não podem ser facilmente adivinhadas, por exemplo, a hora do dia, se for uma paisagem. Se a mídia mostrar algo incomum, explique o que torna incomum.</string>
|
||||
<string name="feedback_popup_title">Feedback desejado</string>
|
||||
<string name="feedback_popup_description">Estamos planejando vários novos recursos e melhorias para o aplicativo! Gostaria de revisá-los e fornecer feedback?\n\n(Você sempre pode acessar isso selecionando \"Planos do desenvolvedor\" na gaveta de navegação)</string>
|
||||
<string name="feedback_popup_decline">Não, obrigado</string>
|
||||
<string name="feedback_popup_accept">Claro, leve-me lá!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Planos de desenvolvedor</string>
|
||||
</resources>
|
||||
|
|
|
|||
100
app/src/main/res/values-skr/strings.xml
Normal file
100
app/src/main/res/values-skr/strings.xml
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name">کامنز</string>
|
||||
<string name="menu_settings">ترتیباں</string>
|
||||
<string name="username">ورتݨ آلا ناں</string>
|
||||
<string name="password">پاس ورڈ</string>
|
||||
<string name="login">لاگ ان تھیوو</string>
|
||||
<string name="signup">سائن اپ</string>
|
||||
<string name="logging_in_title">لاگ ان تھیندا پئے</string>
|
||||
<string name="logging_in_message">انتظار کرو۔۔۔</string>
|
||||
<string name="login_success">لاگ ان کامیاب!</string>
|
||||
<string name="login_failed">لاگ ان ناکام!</string>
|
||||
<string name="upload_failed">فائل کائنی لبھی،ٻئی فائل کیتے کوشش کرو۔</string>
|
||||
<string name="uploading_started">اپ لوڈ شروع!</string>
|
||||
<string name="upload_completed_notification_title">%1$s اپ لوڈ تھی ڳیا!</string>
|
||||
<string name="upload_progress_notification_title_start">اپ لوڈ %1$s شروع تھیندا پئے</string>
|
||||
<string name="upload_progress_notification_title_in_progress">%1$s اپ لوڈ تھیندا پئے</string>
|
||||
<string name="upload_progress_notification_title_finishing">%1$s اپ لوڈ پورا تھیندا پئے</string>
|
||||
<string name="title_activity_contributions">میݙے حالیہ اپ لوڈ</string>
|
||||
<string name="contribution_state_queued">قطار وچ</string>
|
||||
<string name="contribution_state_failed">ناکام</string>
|
||||
<string name="contribution_state_in_progress">%1$d%% مکمل</string>
|
||||
<string name="contribution_state_starting">اپ لوڈ تھیندا پئے</string>
|
||||
<string name="menu_from_gallery">گیلری وچوں</string>
|
||||
<string name="menu_from_camera">فوٹو چھکو</string>
|
||||
<string name="menu_nearby">نیڑے</string>
|
||||
<string name="provider_contributions">میݙے اپ لوڈ</string>
|
||||
<string name="menu_share">شیئر</string>
|
||||
<string name="menu_open_in_browser">براؤزر وچ ݙیکھو</string>
|
||||
<string name="share_title_hint">عنوان</string>
|
||||
<string name="share_description_hint">تفصیل</string>
|
||||
<string name="login_failed_generic">لاگ ان ناکام</string>
|
||||
<string name="share_upload_button">اپلوڈ</string>
|
||||
<string name="multiple_share_base_title">ایں سیٹ دا ناں ݙسو</string>
|
||||
<string name="provider_modifications">تبدیلیاں</string>
|
||||
<string name="menu_upload_single">اپلوڈ</string>
|
||||
<string name="categories_search_text_hint">قسماں دی ڳول</string>
|
||||
<string name="menu_save_categories">بچاؤ</string>
|
||||
<string name="refresh_button">سجرا، تازہ کرو</string>
|
||||
<string name="enable_gps">جی پی ایس چلاؤ</string>
|
||||
<string name="contributions_subtitle_zero">اڄݨ ککھ وی اپ لوڈ نی تھیا</string>
|
||||
<string name="categories_activity_title">قسماں، زمرے</string>
|
||||
<string name="title_activity_settings">ترتیباں</string>
|
||||
<string name="title_activity_signup">سائن اپ</string>
|
||||
<string name="menu_about">تعارف</string>
|
||||
<string name="title_activity_about">تعارف</string>
|
||||
<string name="provider_categories">حالیہ ورتیاں ڳیاں قسماں</string>
|
||||
<string name="menu_retry_upload">ولدا کوشش کرو</string>
|
||||
<string name="menu_cancel_upload">منسوخ</string>
|
||||
<string name="menu_download">ڈاؤن لوڈ ، لہاوݨ</string>
|
||||
<string name="preference_license">لائیسنس</string>
|
||||
<string name="preference_theme">رات آلا مزاج</string>
|
||||
<string name="license_name_cc0">سی سی او</string>
|
||||
<string name="license_name_cc_zero">سی سی زیرو</string>
|
||||
<string name="tutorial_3_text">براہ مہربانی اپ لوڈ نہ کرو</string>
|
||||
<string name="tutorial_4_text">مثال اپ لوڈ:</string>
|
||||
<string name="welcome_wikipedia_subtext">وکی پیڈیا تے فوٹو وکی میڈیا کامنز کنوں امدن۔</string>
|
||||
<string name="welcome_copyright_text">تہاݙے فوٹو پوری دنیا دے لوکاں کوں تعلیم ݙیوݨ کیتے مدد ݙیندن</string>
|
||||
<string name="welcome_final_button_text">جیا!</string>
|
||||
<string name="detail_panel_cats_label">قسماں، زمرے</string>
|
||||
<string name="detail_panel_cats_loading">لوڈ تھیدا پئے۔۔۔</string>
|
||||
<string name="detail_panel_cats_none">کجھ نی چݨیا</string>
|
||||
<string name="detail_description_empty">کوئی تفصیل کائنی</string>
|
||||
<string name="detail_license_empty">نامعلوم لائسنس</string>
|
||||
<string name="menu_refresh">سجرا، تازہ کرو</string>
|
||||
<string name="ok">ٹھیک ہے</string>
|
||||
<string name="title_activity_nearby">نیڑے جاہیں</string>
|
||||
<string name="no_nearby">کوئی نیڑے جاہیں نی لبھیاں</string>
|
||||
<string name="warning">ݙراوا</string>
|
||||
<string name="yes">جیا</string>
|
||||
<string name="no">کو</string>
|
||||
<string name="media_detail_title">عنوان</string>
|
||||
<string name="media_detail_media_title">میڈیا دا عنوان</string>
|
||||
<string name="media_detail_description">تفصیل</string>
|
||||
<string name="media_detail_license">لائیسنس</string>
|
||||
<string name="media_detail_coordinates">کوآرڈینیٹ</string>
|
||||
<string name="media_detail_coordinates_empty">کجھ نی ݙسیا</string>
|
||||
<string name="use_wikidata">وکی ڈیٹا ورتو</string>
|
||||
<string name="number_of_uploads">میݙے حالیہ اپ لوڈ دی حد</string>
|
||||
<string name="maximum_limit">زیادہ حد</string>
|
||||
<string name="set_limit">حالیہ اپ لوڈ دی حد مقرر کرو</string>
|
||||
<string name="logout_verification">بھلا تساں سچی دا لاگ آؤٹ تھیوݨ چاہندے ہو؟</string>
|
||||
<string name="commons_logo">کامنز لوگو</string>
|
||||
<string name="mediaimage_failed">میڈیا فوٹو ناکام</string>
|
||||
<string name="no_image_found">فوٹو نی لبھا</string>
|
||||
<string name="upload_image">فوٹو اپ لوڈ کرو</string>
|
||||
<string name="welcome_image_welcome_wikipedia">وکی پیڈیا وچ ست بسم اللہ</string>
|
||||
<string name="cancel">منسوخ</string>
|
||||
<string name="navigation_drawer_open">کھولو</string>
|
||||
<string name="navigation_drawer_close">بند کرو</string>
|
||||
<string name="navigation_item_home">گھر</string>
|
||||
<string name="navigation_item_upload">اپلوڈ</string>
|
||||
<string name="navigation_item_nearby">نیڑے</string>
|
||||
<string name="navigation_item_about">تعارف</string>
|
||||
<string name="navigation_item_settings">ترتیباں</string>
|
||||
<string name="navigation_item_feedback">تہاڈی رائے</string>
|
||||
<string name="navigation_item_logout">لاگ آؤٹ</string>
|
||||
<string name="nearby_info_menu_wikidata_article">وکی ڈیٹا آئٹم</string>
|
||||
<string name="feedback_popup_decline">کو، شکریہ</string>
|
||||
</resources>
|
||||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">Vi planerar flera nya funktioner och förbättringar för appen! Vill du prova dem och ge återkoppling?\n\n(Kan kan alltid komma åt detta genom att markera \"Utvecklarplaner\" i navigeringsmenyn).</string>
|
||||
<string name="feedback_popup_decline">Nej tack</string>
|
||||
<string name="feedback_popup_accept">Visst, ta mig dit!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">Utvecklarplaner</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">我們正計劃替應用程式添加數種新功能以及改進!您是否有意想檢閱看看並提供回饋意見?\n\n(您可以隨時透過導覽選單選擇「開發者計劃」來存取那些內容)</string>
|
||||
<string name="feedback_popup_decline">不用了,謝謝</string>
|
||||
<string name="feedback_popup_accept">當然,請帶我到那!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">開發者計劃</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -198,6 +198,5 @@
|
|||
<string name="feedback_popup_description">我们正在计划为应用添加多个新功能,以及进行多方面改进!您是否需要查阅它们,并提供反馈?\n\n(您随时都可以通过选择导航菜单的“开发人员计划”访问它)</string>
|
||||
<string name="feedback_popup_decline">不用了,谢谢</string>
|
||||
<string name="feedback_popup_accept">好的,带我看看!</string>
|
||||
<string name="feedback_page_url">https://meta.wikimedia.org/wiki/Grants:Project/Improve_\'Upload_to_Commons\'_Android_App/Renewal/User_feedback</string>
|
||||
<string name="navigation_item_developer_plans">开发人员计划</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -13,4 +13,12 @@
|
|||
<attr name="iconCamera" format="reference"/>
|
||||
<attr name="iconPhoto" format="reference"/>
|
||||
<attr name="iconUndo" format="reference"/>
|
||||
|
||||
<declare-styleable name="CompatTextView">
|
||||
<attr name="drawablePadding" format="dimension"/>
|
||||
<attr name="drawableStart" format="reference"/>
|
||||
<attr name="drawableTop" format="reference"/>
|
||||
<attr name="drawableEnd" format="reference"/>
|
||||
<attr name="drawableBottom" format="reference"/>
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
|
||||
<CheckBoxPreference
|
||||
android:title="@string/preference_theme"
|
||||
android:defaultValue="true"
|
||||
android:defaultValue="false"
|
||||
android:summary="@string/preference_theme_summary"
|
||||
android:key="theme"
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue