Merge branch 'main' of github.com:commons-app/apps-android-commons into issue-6357-Fix-security-exception-on-open-document

This commit is contained in:
Ritika Pahwa 2025-08-16 18:52:46 +05:30
commit 970d38ecc2
139 changed files with 6146 additions and 5778 deletions

View file

@ -1,5 +1,16 @@
# Wikimedia Commons for Android
## v5.6.1
### What's changed
* The app no longer uploads images to Wikidata if one exists already for a given item
* File usage displays correctly now
* No more infinite circular progress bar on nominating an image for deletion
* Enhanced location updates while using GPS
* Author/uploader names are now available in Media Details for Commons licensing compliance
* Improved usage of popups in Nearby
* Bug fixes and stability improvements
## v5.5.0
### What's changed

View file

@ -24,8 +24,8 @@ android {
applicationId = "fr.free.nrw.commons"
minSdk = 21
targetSdk = 34
versionCode = 1053
versionName = "5.5.0"
versionCode = 1055
versionName = "5.6.1"
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View file

@ -15,9 +15,8 @@ import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
import fr.free.nrw.commons.auth.LoginActivity
import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.concurrency.BackgroundPoolExceptionHandler
import fr.free.nrw.commons.concurrency.ThreadPoolService
@ -257,8 +256,8 @@ class CommonsApplication : MultiDexApplication() {
} catch (e: SQLiteException) {
Timber.e(e)
}
BookmarkPicturesDao.Table.onDelete(db)
BookmarkItemsDao.Table.onDelete(db)
BookmarksTable.onDelete(db)
BookmarkItemsTable.onDelete(db)
}

View file

@ -92,7 +92,19 @@ class LoginActivity : AccountAuthenticatorActivity() {
aboutPrivacyPolicy.setOnClickListener { onPrivacyPolicyClicked() }
signUpButton.setOnClickListener { signUp() }
loginButton.setOnClickListener { performLogin() }
loginPassword.setOnEditorActionListener(::onEditorAction)
loginPassword.setOnEditorActionListener { textView, actionId, keyEvent ->
if (binding!!.loginButton.isEnabled && isTriggerAction(actionId, keyEvent)) {
if (actionId == EditorInfo.IME_ACTION_NEXT && lastLoginResult != null) {
askUserForTwoFactorAuthWithKeyboard()
true
} else {
performLogin()
true
}
} else {
false
}
}
loginPassword.onFocusChangeListener =
View.OnFocusChangeListener(::onPasswordFocusChanged)
@ -113,6 +125,39 @@ class LoginActivity : AccountAuthenticatorActivity() {
}
}
@VisibleForTesting
fun askUserForTwoFactorAuthWithKeyboard() {
if (binding == null) {
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuthWithKeyboard")
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding!!.root)
}
progressDialog!!.dismiss()
if (binding != null) {
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(loginTwoFactor, InputMethodManager.SHOW_IMPLICIT)
loginTwoFactor.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE ||
(event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
performLogin()
true
} else {
false
}
}
}
} else {
Timber.e("Binding is null in askUserForTwoFactorAuthWithKeyboard after reinitialization attempt")
}
showMessageAndCancelDialog(getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.login_failed_email_auth_needed else R.string.login_failed_2fa_needed))
}
override fun onPostCreate(savedInstanceState: Bundle?) {
super.onPostCreate(savedInstanceState)
delegate.onPostCreate(savedInstanceState)
@ -236,7 +281,7 @@ class LoginActivity : AccountAuthenticatorActivity() {
} else false
private fun isTriggerAction(actionId: Int, keyEvent: KeyEvent?) =
actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
actionId == EditorInfo.IME_ACTION_NEXT || actionId == EditorInfo.IME_ACTION_DONE || keyEvent?.keyCode == KeyEvent.KEYCODE_ENTER
private fun skipLogin() {
AlertDialog.Builder(this)
@ -286,14 +331,14 @@ class LoginActivity : AccountAuthenticatorActivity() {
Timber.d("Requesting 2FA prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
askUserForTwoFactorAuthWithKeyboard()
}
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) {
override fun emailAuthPrompt(loginResult: LoginResult, caught: Throwable, token: String?) = runOnUiThread {
Timber.d("Requesting email auth prompt")
progressDialog!!.dismiss()
lastLoginResult = loginResult
askUserForTwoFactorAuth()
askUserForTwoFactorAuthWithKeyboard()
}
override fun passwordResetPrompt(token: String?) = runOnUiThread {
@ -348,12 +393,31 @@ class LoginActivity : AccountAuthenticatorActivity() {
@VisibleForTesting
fun askUserForTwoFactorAuth() {
if (binding == null) {
Timber.w("Binding is null, reinitializing in askUserForTwoFactorAuth")
binding = ActivityLoginBinding.inflate(layoutInflater)
setContentView(binding!!.root)
}
progressDialog!!.dismiss()
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
if (binding != null) {
with(binding!!) {
twoFactorContainer.visibility = View.VISIBLE
twoFactorContainer.hint = getString(if (lastLoginResult is LoginResult.EmailAuthResult) R.string.email_auth_code else R.string._2fa_code)
loginTwoFactor.visibility = View.VISIBLE
loginTwoFactor.requestFocus()
loginTwoFactor.setOnEditorActionListener { _, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_DONE ||
(event != null && event.keyCode == KeyEvent.KEYCODE_ENTER && event.action == KeyEvent.ACTION_DOWN)) {
performLogin()
true
} else {
false
}
}
}
} else {
Timber.e("Binding is null in askUserForTwoFactorAuth after reinitialization attempt")
}
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
imm.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY)

View file

@ -1,105 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentManager;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentBookmarksBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.theme.BaseActivity;
import javax.inject.Inject;
import fr.free.nrw.commons.contributions.ContributionController;
import javax.inject.Named;
public class BookmarkFragment extends CommonsDaggerSupportFragment {
private FragmentManager supportFragmentManager;
private BookmarksPagerAdapter adapter;
FragmentBookmarksBinding binding;
@Inject
ContributionController controller;
/**
* To check if the user is loggedIn or not.
*/
@Inject
@Named("default_preferences")
public
JsonKvStore applicationKvStore;
@NonNull
public static BookmarkFragment newInstance() {
BookmarkFragment fragment = new BookmarkFragment();
fragment.setRetainInstance(true);
return fragment;
}
public void setScroll(boolean canScroll) {
if (binding!=null) {
binding.viewPagerBookmarks.setCanScroll(canScroll);
}
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
binding = FragmentBookmarksBinding.inflate(inflater, container, false);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
supportFragmentManager = getChildFragmentManager();
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
applicationKvStore.getBoolean("login_skipped"));
binding.viewPagerBookmarks.setAdapter(adapter);
binding.tabLayout.setupWithViewPager(binding.viewPagerBookmarks);
((MainActivity) getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
setupTabLayout();
return binding.getRoot();
}
/**
* This method sets up the tab layout. If the adapter has only one element it sets the
* visibility of tabLayout to gone.
*/
public void setupTabLayout() {
binding.tabLayout.setVisibility(View.VISIBLE);
if (adapter.getCount() == 1) {
binding.tabLayout.setVisibility(View.GONE);
}
}
public void onBackPressed() {
if (((BookmarkListRootFragment) (adapter.getItem(binding.tabLayout.getSelectedTabPosition())))
.backPressed()) {
// The event is handled internally by the adapter , no further action required.
return;
}
// Event is not handled by the adapter ( performed back action ) change action bar.
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,98 @@
package fr.free.nrw.commons.bookmarks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentBookmarksBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.theme.BaseActivity
import javax.inject.Inject
import javax.inject.Named
class BookmarkFragment : CommonsDaggerSupportFragment() {
private var adapter: BookmarksPagerAdapter? = null
@JvmField
var binding: FragmentBookmarksBinding? = null
@JvmField
@Inject
var controller: ContributionController? = null
/**
* To check if the user is loggedIn or not.
*/
@JvmField
@Inject
@Named("default_preferences")
var applicationKvStore: JsonKvStore? = null
fun setScroll(canScroll: Boolean) {
binding?.let {
it.viewPagerBookmarks.canScroll = canScroll
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreateView(inflater, container, savedInstanceState)
binding = FragmentBookmarksBinding.inflate(inflater, container, false)
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
val supportFragmentManager = childFragmentManager
adapter = BookmarksPagerAdapter(
supportFragmentManager, requireContext(),
applicationKvStore!!.getBoolean("login_skipped")
)
binding!!.viewPagerBookmarks.adapter = adapter
binding!!.tabLayout.setupWithViewPager(binding!!.viewPagerBookmarks)
(requireActivity() as MainActivity).showTabs()
(requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
setupTabLayout()
return binding!!.root
}
/**
* This method sets up the tab layout. If the adapter has only one element it sets the
* visibility of tabLayout to gone.
*/
fun setupTabLayout() {
binding!!.tabLayout.visibility = View.VISIBLE
if (adapter!!.count == 1) {
binding!!.tabLayout.visibility = View.GONE
}
}
fun onBackPressed() {
if (((adapter!!.getItem(binding!!.tabLayout.selectedTabPosition)) as BookmarkListRootFragment).backPressed()) {
// The event is handled internally by the adapter , no further action required.
return
}
// Event is not handled by the adapter ( performed back action ) change action bar.
(requireActivity() as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
companion object {
fun newInstance(): BookmarkFragment = BookmarkFragment().apply {
retainInstance = true
}
}
}

View file

@ -1,267 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.category.GridViewAdapter;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailProvider;
import fr.free.nrw.commons.navtab.NavTab;
import java.util.ArrayList;
import java.util.Iterator;
import timber.log.Timber;
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
FragmentManager.OnBackStackChangedListener,
MediaDetailProvider,
AdapterView.OnItemClickListener, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails;
//private BookmarkPicturesFragment bookmarkPicturesFragment;
private BookmarkLocationsFragment bookmarkLocationsFragment;
public Fragment listFragment;
private BookmarksPagerAdapter bookmarksPagerAdapter;
FragmentFeaturedRootBinding binding;
public BookmarkListRootFragment() {
//empty constructor necessary otherwise crashes on recreate
}
public BookmarkListRootFragment(Bundle bundle, BookmarksPagerAdapter bookmarksPagerAdapter) {
String title = bundle.getString("categoryName");
int order = bundle.getInt("order");
final int orderItem = bundle.getInt("orderItem");
switch (order){
case 0: listFragment = new BookmarkPicturesFragment();
break;
case 1: listFragment = new BookmarkLocationsFragment();
break;
case 3: listFragment = new BookmarkCategoriesFragment();
break;
}
if(orderItem == 2) {
listFragment = new BookmarkItemsFragment();
}
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
this.bookmarksPagerAdapter = bookmarksPagerAdapter;
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState == null) {
setFragment(listFragment, mediaDetails);
}
}
public void setFragment(Fragment fragment, Fragment otherFragment) {
if (fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
}
}
public void removeFragment(Fragment fragment) {
getChildFragmentManager()
.beginTransaction()
.remove(fragment)
.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public void onMediaClicked(int position) {
Timber.d("on media clicked");
/*container.setVisibility(View.VISIBLE);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true, position);
setFragment(mediaDetails, bookmarkPicturesFragment);*/
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
// not yet ready to return data
return null;
} else {
return (Media) bookmarksPagerAdapter.getMediaAdapter().getItem(i);
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (bookmarksPagerAdapter.getMediaAdapter() == null) {
return 0;
}
return bookmarksPagerAdapter.getMediaAdapter().getCount();
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (mediaDetails != null && !listFragment.isVisible()) {
removeFragment(mediaDetails);
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((BookmarkFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(index);
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
}
}
public boolean backPressed() {
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
if (mediaDetails != null) {
if (mediaDetails.isVisible()) {
// todo add get list fragment
((BookmarkFragment) getParentFragment()).setupTabLayout();
ArrayList<Integer> removed = mediaDetails.getRemovedItems();
removeFragment(mediaDetails);
((BookmarkFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
if (listFragment instanceof BookmarkPicturesFragment) {
GridViewAdapter adapter = ((GridViewAdapter) ((BookmarkPicturesFragment) listFragment)
.getAdapter());
Iterator i = removed.iterator();
while (i.hasNext()) {
adapter.remove(adapter.getItem((int) i.next()));
}
mediaDetails.clearRemoved();
}
} else {
moveToContributionsFragment();
}
} else {
moveToContributionsFragment();
}
// notify mediaDetails did not handled the backPressed further actions required.
return false;
}
void moveToContributionsFragment() {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
((MainActivity) getActivity()).showTabs();
}
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Timber.d("on media clicked");
binding.exploreContainer.setVisibility(View.VISIBLE);
((BookmarkFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((BookmarkFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(position);
}
@Override
public void onBackStackChanged() {
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,226 @@
package fr.free.nrw.commons.bookmarks
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.category.BookmarkCategoriesFragment
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsFragment
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsFragment
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.category.GridViewAdapter
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment.Companion.newInstance
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.navtab.NavTab
import timber.log.Timber
class BookmarkListRootFragment : CommonsDaggerSupportFragment,
FragmentManager.OnBackStackChangedListener, MediaDetailProvider, OnItemClickListener,
CategoryImagesCallback {
private var mediaDetails: MediaDetailPagerFragment? = null
private val bookmarkLocationsFragment: BookmarkLocationsFragment? = null
var listFragment: Fragment? = null
private var bookmarksPagerAdapter: BookmarksPagerAdapter? = null
var binding: FragmentFeaturedRootBinding? = null
constructor()
constructor(bundle: Bundle, bookmarksPagerAdapter: BookmarksPagerAdapter) {
val title = bundle.getString("categoryName")
val order = bundle.getInt("order")
val orderItem = bundle.getInt("orderItem")
when (order) {
0 -> listFragment = BookmarkPicturesFragment()
1 -> listFragment = BookmarkLocationsFragment()
3 -> listFragment = BookmarkCategoriesFragment()
}
if (orderItem == 2) {
listFragment = BookmarkItemsFragment()
}
val featuredArguments = Bundle()
featuredArguments.putString("categoryName", title)
listFragment!!.setArguments(featuredArguments)
this.bookmarksPagerAdapter = bookmarksPagerAdapter
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
super.onCreate(savedInstanceState)
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false)
return binding!!.getRoot()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) {
setFragment(listFragment!!, mediaDetails)
}
}
fun setFragment(fragment: Fragment, otherFragment: Fragment?) {
if (fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
getChildFragmentManager().executePendingTransactions()
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
getChildFragmentManager().executePendingTransactions()
} else if (!fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
getChildFragmentManager().executePendingTransactions()
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
getChildFragmentManager().executePendingTransactions()
}
}
fun removeFragment(fragment: Fragment) {
getChildFragmentManager()
.beginTransaction()
.remove(fragment)
.commit()
getChildFragmentManager().executePendingTransactions()
}
override fun onMediaClicked(position: Int) {
Timber.d("on media clicked")
/*container.setVisibility(View.VISIBLE);
((BookmarkFragment)getParentFragment()).tabLayout.setVisibility(View.GONE);
mediaDetails = new MediaDetailPagerFragment(false, true, position);
setFragment(mediaDetails, bookmarkPicturesFragment);*/
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
override fun getMediaAtPosition(i: Int): Media? =
bookmarksPagerAdapter!!.mediaAdapter?.getItem(i) as Media?
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
override fun getTotalMediaCount(): Int =
bookmarksPagerAdapter!!.mediaAdapter?.count ?: 0
override fun getContributionStateAt(position: Int): Int? {
return null
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (mediaDetails != null && !listFragment!!.isVisible()) {
removeFragment(mediaDetails!!)
mediaDetails = newInstance(false, true)
(parentFragment as BookmarkFragment).setScroll(false)
setFragment(mediaDetails!!, listFragment)
mediaDetails!!.showImage(index)
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails!!.notifyDataSetChanged()
}
}
fun backPressed(): Boolean {
//check mediaDetailPage fragment is not null then we check mediaDetail.is Visible or not to avoid NullPointerException
if (mediaDetails != null) {
if (mediaDetails!!.isVisible()) {
// todo add get list fragment
(parentFragment as BookmarkFragment).setupTabLayout()
val removed: ArrayList<Int> = mediaDetails!!.removedItems
removeFragment(mediaDetails!!)
(parentFragment as BookmarkFragment).setScroll(true)
setFragment(listFragment!!, mediaDetails)
(requireActivity() as MainActivity).showTabs()
if (listFragment is BookmarkPicturesFragment) {
val adapter = ((listFragment as BookmarkPicturesFragment)
.getAdapter() as GridViewAdapter?)
val i: MutableIterator<*> = removed.iterator()
while (i.hasNext()) {
adapter!!.remove(adapter.getItem(i.next() as Int))
}
mediaDetails!!.clearRemoved()
}
} else {
moveToContributionsFragment()
}
} else {
moveToContributionsFragment()
}
// notify mediaDetails did not handled the backPressed further actions required.
return false
}
fun moveToContributionsFragment() {
(requireActivity() as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code())
(requireActivity() as MainActivity).showTabs()
}
override fun onItemClick(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
Timber.d("on media clicked")
binding!!.exploreContainer.visibility = View.VISIBLE
(parentFragment as BookmarkFragment).binding!!.tabLayout.setVisibility(View.GONE)
mediaDetails = newInstance(false, true)
(parentFragment as BookmarkFragment).setScroll(false)
setFragment(mediaDetails!!, listFragment)
mediaDetails!!.showImage(position)
}
override fun onBackStackChanged() = Unit
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -1,94 +0,0 @@
package fr.free.nrw.commons.bookmarks;
import android.content.Context;
import android.os.Bundle;
import android.widget.ListAdapter;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
import java.util.ArrayList;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment;
public class BookmarksPagerAdapter extends FragmentPagerAdapter {
private ArrayList<BookmarkPages> pages;
/**
* Default Constructor
* @param fm
* @param context
* @param onlyPictures is true if the fragment requires only BookmarkPictureFragment
* (i.e. when no user is logged in).
*/
BookmarksPagerAdapter(FragmentManager fm, Context context,boolean onlyPictures) {
super(fm);
pages = new ArrayList<>();
Bundle picturesBundle = new Bundle();
picturesBundle.putString("categoryName", context.getString(R.string.title_page_bookmarks_pictures));
picturesBundle.putInt("order", 0);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(picturesBundle, this),
context.getString(R.string.title_page_bookmarks_pictures)));
if (!onlyPictures) {
// if onlyPictures is false we also add the location fragment.
Bundle locationBundle = new Bundle();
locationBundle.putString("categoryName",
context.getString(R.string.title_page_bookmarks_locations));
locationBundle.putInt("order", 1);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_locations)));
locationBundle.putInt("orderItem", 2);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_items)));
}
final Bundle categoriesBundle = new Bundle();
categoriesBundle.putString("categoryName",
context.getString(R.string.title_page_bookmarks_categories));
categoriesBundle.putInt("order", 3);
pages.add(new BookmarkPages(
new BookmarkListRootFragment(categoriesBundle, this),
context.getString(R.string.title_page_bookmarks_categories)));
notifyDataSetChanged();
}
@Override
public Fragment getItem(int position) {
return pages.get(position).getPage();
}
@Override
public int getCount() {
return pages.size();
}
@Nullable
@Override
public CharSequence getPageTitle(int position) {
return pages.get(position).getTitle();
}
/**
* Return the Adapter used to display the picture gridview
* @return adapter
*/
public ListAdapter getMediaAdapter() {
BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment);
return fragment.getAdapter();
}
/**
* Update the pictures list for the bookmark fragment
*/
public void requestPictureListUpdate() {
BookmarkPicturesFragment fragment = (BookmarkPicturesFragment)(((BookmarkListRootFragment)pages.get(0).getPage()).listFragment);
fragment.onResume();
}
}

View file

@ -0,0 +1,82 @@
package fr.free.nrw.commons.bookmarks
import android.content.Context
import android.widget.ListAdapter
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesFragment
class BookmarksPagerAdapter internal constructor(
fm: FragmentManager, context: Context, onlyPictures: Boolean
) : FragmentPagerAdapter(fm) {
private val pages = mutableListOf<BookmarkPages>()
/**
* Default Constructor
* @param fm
* @param context
* @param onlyPictures is true if the fragment requires only BookmarkPictureFragment
* (i.e. when no user is logged in).
*/
init {
pages.add(
BookmarkPages(
BookmarkListRootFragment(
bundleOf(
"categoryName" to context.getString(R.string.title_page_bookmarks_pictures),
"order" to 0
), this
), context.getString(R.string.title_page_bookmarks_pictures)
)
)
if (!onlyPictures) {
// if onlyPictures is false we also add the location fragment.
val locationBundle = bundleOf(
"categoryName" to context.getString(R.string.title_page_bookmarks_locations),
"order" to 1
)
pages.add(
BookmarkPages(
BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_locations)
)
)
locationBundle.putInt("orderItem", 2)
pages.add(
BookmarkPages(
BookmarkListRootFragment(locationBundle, this),
context.getString(R.string.title_page_bookmarks_items)
)
)
}
pages.add(
BookmarkPages(
BookmarkListRootFragment(
bundleOf(
"categoryName" to context.getString(R.string.title_page_bookmarks_categories),
"order" to 3
), this),
context.getString(R.string.title_page_bookmarks_categories)
)
)
notifyDataSetChanged()
}
override fun getItem(position: Int): Fragment = pages[position].page!!
override fun getCount(): Int = pages.size
override fun getPageTitle(position: Int): CharSequence? = pages[position].title
/**
* Return the Adapter used to display the picture gridview
* @return adapter
*/
val mediaAdapter: ListAdapter?
get() = (((pages[0].page as BookmarkListRootFragment).listFragment) as BookmarkPicturesFragment).getAdapter()
}

View file

@ -1,129 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao.Table.TABLE_NAME;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import javax.inject.Inject;
import timber.log.Timber;
/**
* Handles private storage for bookmarked items
*/
public class BookmarkItemsContentProvider extends CommonsDaggerContentProvider {
private static final String BASE_PATH = "bookmarksItems";
public static final Uri BASE_URI =
Uri.parse("content://" + BuildConfig.BOOKMARK_ITEMS_AUTHORITY + "/" + BASE_PATH);
/**
* Append bookmark items ID to the base uri
*/
public static Uri uriForName(final String id) {
return Uri.parse(BASE_URI + "/" + id);
}
@Inject
DBOpenHelper dbOpenHelper;
@Override
public String getType(@NonNull final Uri uri) {
return null;
}
/**
* Queries the SQLite database for the bookmark items
* @param uri : contains the uri for bookmark items
* @param projection : contains the all fields of the table
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull final Uri uri, final String[] projection, final String selection,
final String[] selectionArgs, final String sortOrder) {
final SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
final Cursor cursor = queryBuilder.query(db, projection, selection,
selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark items
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull final Uri uri, final ContentValues contentValues,
final String selection, final String[] selectionArgs) {
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
final int rowsUpdated;
if (TextUtils.isEmpty(selection)) {
final int id = Integer.parseInt(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
/**
* Handles the insertion of new bookmark items record to local SQLite Database
* @param uri
* @param contentValues
* @return
*/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull final Uri uri, final ContentValues contentValues) {
final SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
final long id = sqlDB.insert(TABLE_NAME, null, contentValues);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
/**
* Handles the deletion of new bookmark items record to local SQLite Database
* @param uri
* @param s
* @param strings
* @return
*/
@SuppressWarnings("ConstantConditions")
@Override
public int delete(@NonNull final Uri uri, final String s, final String[] strings) {
final int rows;
final SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
rows = db.delete(
TABLE_NAME,
"item_id = ?",
new String[]{uri.getLastPathSegment()}
);
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
}

View file

@ -0,0 +1,101 @@
package fr.free.nrw.commons.bookmarks.items
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.TABLE_NAME
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import androidx.core.net.toUri
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID
/**
* Handles private storage for bookmarked items
*/
class BookmarkItemsContentProvider : CommonsDaggerContentProvider() {
override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the bookmark items
* @param uri : contains the uri for bookmark items
* @param projection : contains the all fields of the table
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
return queryBuilder.query(
requireDb(), projection, selection,
selectionArgs, null, null, sortOrder
).apply {
setNotificationUri(requireContext().contentResolver, uri)
}
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark items
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
override fun update(
uri: Uri, contentValues: ContentValues?,
selection: String?, selectionArgs: Array<String>?
): Int {
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment!!.toInt()
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID"
)
}
requireContext().contentResolver.notifyChange(uri, null)
return rowsUpdated
}
/**
* Handles the insertion of new bookmark items record to local SQLite Database
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
val id = requireDb().insert(TABLE_NAME, null, contentValues)
requireContext().contentResolver.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
/**
* Handles the deletion of new bookmark items record to local SQLite Database
*/
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val rows: Int = requireDb().delete(
TABLE_NAME,
"$COLUMN_ID = ?",
arrayOf(uri.lastPathSegment)
)
requireContext().contentResolver.notifyChange(uri, null)
return rows
}
companion object {
private const val BASE_PATH = "bookmarksItems"
val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_ITEMS_AUTHORITY}/$BASE_PATH".toUri()
fun uriForName(id: String) = "$BASE_URI/$id".toUri()
}
}

View file

@ -1,27 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
/**
* Handles loading bookmarked items from Database
*/
@Singleton
public class BookmarkItemsController {
@Inject
BookmarkItemsDao bookmarkItemsDao;
@Inject
public BookmarkItemsController() {}
/**
* Load from DB the bookmarked items
* @return a list of DepictedItem objects.
*/
public List<DepictedItem> loadFavoritesItems() {
return bookmarkItemsDao.getAllBookmarksItems();
}
}

View file

@ -0,0 +1,23 @@
package fr.free.nrw.commons.bookmarks.items
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import javax.inject.Inject
import javax.inject.Singleton
/**
* Handles loading bookmarked items from Database
*/
@Singleton
class BookmarkItemsController @Inject constructor() {
@JvmField
@Inject
var bookmarkItemsDao: BookmarkItemsDao? = null
/**
* Load from DB the bookmarked items
* @return a list of DepictedItem objects.
*/
fun loadFavoritesItems(): List<DepictedItem> {
return bookmarkItemsDao?.getAllBookmarksItems() ?: emptyList()
}
}

View file

@ -1,329 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import fr.free.nrw.commons.category.CategoryItem;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.apache.commons.lang3.StringUtils;
/**
* Handles database operations for bookmarked items
*/
@Singleton
public class BookmarkItemsDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public BookmarkItemsDao(
@Named("bookmarksItem") final Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* Find all persisted items bookmarks on database
* @return list of bookmarks
*/
public List<DepictedItem> getAllBookmarksItems() {
final List<DepictedItem> items = new ArrayList<>();
final ContentProviderClient db = clientProvider.get();
try (final Cursor cursor = db.query(
BookmarkItemsContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
null)) {
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor));
}
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
return items;
}
/**
* Look for a bookmark in database and in order to insert or delete it
* @param depictedItem : Bookmark object
* @return boolean : is bookmark now favorite ?
*/
public boolean updateBookmarkItem(final DepictedItem depictedItem) {
final boolean bookmarkExists = findBookmarkItem(depictedItem.getId());
if (bookmarkExists) {
deleteBookmarkItem(depictedItem);
} else {
addBookmarkItem(depictedItem);
}
return !bookmarkExists;
}
/**
* Add a Bookmark to database
* @param depictedItem : Bookmark to add
*/
private void addBookmarkItem(final DepictedItem depictedItem) {
final ContentProviderClient db = clientProvider.get();
try {
db.insert(BookmarkItemsContentProvider.BASE_URI, toContentValues(depictedItem));
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Delete a bookmark from database
* @param depictedItem : Bookmark to delete
*/
private void deleteBookmarkItem(final DepictedItem depictedItem) {
final ContentProviderClient db = clientProvider.get();
try {
db.delete(BookmarkItemsContentProvider.uriForName(depictedItem.getId()), null, null);
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find a bookmark from database based on its name
* @param depictedItemID : Bookmark to find
* @return boolean : is bookmark in database ?
*/
public boolean findBookmarkItem(final String depictedItemID) {
if (depictedItemID == null) { //Avoiding NPE's
return false;
}
final ContentProviderClient db = clientProvider.get();
try (final Cursor cursor = db.query(
BookmarkItemsContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_ID + "=?",
new String[]{depictedItemID},
null
)) {
if (cursor != null && cursor.moveToFirst()) {
return true;
}
} catch (final RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
return false;
}
/**
* Recives real data from cursor
* @param cursor : Object for storing database data
* @return DepictedItem
*/
@SuppressLint("Range")
DepictedItem fromCursor(final Cursor cursor) {
final String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME));
final String description
= cursor.getString(cursor.getColumnIndex(Table.COLUMN_DESCRIPTION));
final String imageUrl = cursor.getString(cursor.getColumnIndex(Table.COLUMN_IMAGE));
final String instanceListString
= cursor.getString(cursor.getColumnIndex(Table.COLUMN_INSTANCE_LIST));
final List<String> instanceList = StringToArray(instanceListString);
final String categoryNameListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_NAME_LIST));
final List<String> categoryNameList = StringToArray(categoryNameListString);
final String categoryDescriptionListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST));
final List<String> categoryDescriptionList = StringToArray(categoryDescriptionListString);
final String categoryThumbnailListString = cursor.getString(cursor
.getColumnIndex(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST));
final List<String> categoryThumbnailList = StringToArray(categoryThumbnailListString);
final List<CategoryItem> categoryList = convertToCategoryItems(categoryNameList,
categoryDescriptionList, categoryThumbnailList);
final boolean isSelected
= Boolean.parseBoolean(cursor.getString(cursor
.getColumnIndex(Table.COLUMN_IS_SELECTED)));
final String id = cursor.getString(cursor.getColumnIndex(Table.COLUMN_ID));
return new DepictedItem(
fileName,
description,
imageUrl,
instanceList,
categoryList,
isSelected,
id
);
}
private List<CategoryItem> convertToCategoryItems(List<String> categoryNameList,
List<String> categoryDescriptionList, List<String> categoryThumbnailList) {
List<CategoryItem> categoryItems = new ArrayList<>();
for(int i=0; i<categoryNameList.size(); i++){
categoryItems.add(new CategoryItem(categoryNameList.get(i),
categoryDescriptionList.get(i),
categoryThumbnailList.get(i), false));
}
return categoryItems;
}
/**
* Converts string to List
* @param listString comma separated single string from of list items
* @return List of string
*/
private List<String> StringToArray(final String listString) {
final String[] elements = listString.split(",");
return Arrays.asList(elements);
}
/**
* Converts string to List
* @param list list of items
* @return string comma separated single string of items
*/
private String ArrayToString(final List<String> list) {
if (list != null) {
return StringUtils.join(list, ',');
}
return null;
}
/**
* Takes data from DepictedItem and create a content value object
* @param depictedItem depicted item
* @return ContentValues
*/
private ContentValues toContentValues(final DepictedItem depictedItem) {
final List<String> namesOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
namesOfCommonsCategories.add(category.getName());
}
final List<String> descriptionsOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
descriptionsOfCommonsCategories.add(category.getDescription());
}
final List<String> thumbnailsOfCommonsCategories = new ArrayList<>();
for (final CategoryItem category :
depictedItem.getCommonsCategories()) {
thumbnailsOfCommonsCategories.add(category.getThumbnail());
}
final ContentValues cv = new ContentValues();
cv.put(Table.COLUMN_NAME, depictedItem.getName());
cv.put(Table.COLUMN_DESCRIPTION, depictedItem.getDescription());
cv.put(Table.COLUMN_IMAGE, depictedItem.getImageUrl());
cv.put(Table.COLUMN_INSTANCE_LIST, ArrayToString(depictedItem.getInstanceOfs()));
cv.put(Table.COLUMN_CATEGORIES_NAME_LIST, ArrayToString(namesOfCommonsCategories));
cv.put(Table.COLUMN_CATEGORIES_DESCRIPTION_LIST,
ArrayToString(descriptionsOfCommonsCategories));
cv.put(Table.COLUMN_CATEGORIES_THUMBNAIL_LIST,
ArrayToString(thumbnailsOfCommonsCategories));
cv.put(Table.COLUMN_IS_SELECTED, depictedItem.isSelected());
cv.put(Table.COLUMN_ID, depictedItem.getId());
return cv;
}
/**
* Table of bookmarksItems data
*/
public static final class Table {
public static final String TABLE_NAME = "bookmarksItems";
public static final String COLUMN_NAME = "item_name";
public static final String COLUMN_DESCRIPTION = "item_description";
public static final String COLUMN_IMAGE = "item_image_url";
public static final String COLUMN_INSTANCE_LIST = "item_instance_of";
public static final String COLUMN_CATEGORIES_NAME_LIST = "item_name_categories";
public static final String COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories";
public static final String COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories";
public static final String COLUMN_IS_SELECTED = "item_is_selected";
public static final String COLUMN_ID = "item_id";
public static final String[] ALL_FIELDS = {
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_IMAGE,
COLUMN_INSTANCE_LIST,
COLUMN_CATEGORIES_NAME_LIST,
COLUMN_CATEGORIES_DESCRIPTION_LIST,
COLUMN_CATEGORIES_THUMBNAIL_LIST,
COLUMN_IS_SELECTED,
COLUMN_ID
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_NAME + " STRING,"
+ COLUMN_DESCRIPTION + " STRING,"
+ COLUMN_IMAGE + " STRING,"
+ COLUMN_INSTANCE_LIST + " STRING,"
+ COLUMN_CATEGORIES_NAME_LIST + " STRING,"
+ COLUMN_CATEGORIES_DESCRIPTION_LIST + " STRING,"
+ COLUMN_CATEGORIES_THUMBNAIL_LIST + " STRING,"
+ COLUMN_IS_SELECTED + " STRING,"
+ COLUMN_ID + " STRING PRIMARY KEY"
+ ");";
/**
* Creates table
* @param db SQLiteDatabase
*/
public static void onCreate(final SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
/**
* Deletes database
* @param db SQLiteDatabase
*/
public static void onDelete(final SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
/**
* Updates database
* @param db SQLiteDatabase
* @param from starting
* @param to end
*/
public static void onUpdate(final SQLiteDatabase db, int from, final int to) {
if (from == to) {
return;
}
if (from < 18) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 18) {
// table added in version 19
onCreate(db);
from++;
onUpdate(db, from, to);
}
}
}
}

View file

@ -0,0 +1,199 @@
package fr.free.nrw.commons.bookmarks.items
import android.annotation.SuppressLint
import android.content.ContentProviderClient
import android.content.ContentValues
import android.database.Cursor
import android.os.RemoteException
import androidx.core.content.contentValuesOf
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.BASE_URI
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsContentProvider.Companion.uriForName
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_DESCRIPTION_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_NAME_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_CATEGORIES_THUMBNAIL_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_DESCRIPTION
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_ID
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IMAGE
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_INSTANCE_LIST
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_IS_SELECTED
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable.COLUMN_NAME
import fr.free.nrw.commons.category.CategoryItem
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.arrayToString
import fr.free.nrw.commons.utils.getString
import fr.free.nrw.commons.utils.getStringArray
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import javax.inject.Singleton
/**
* Handles database operations for bookmarked items
*/
@Singleton
class BookmarkItemsDao @Inject constructor(
@param:Named("bookmarksItem") private val clientProvider: Provider<ContentProviderClient>
) {
/**
* Find all persisted items bookmarks on database
* @return list of bookmarks
*/
fun getAllBookmarksItems(): List<DepictedItem> {
val items: MutableList<DepictedItem> = mutableListOf()
val db = clientProvider.get()
try {
db.query(
BASE_URI,
BookmarkItemsTable.ALL_FIELDS,
null,
arrayOf(),
null
).use { cursor ->
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor))
}
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
return items
}
/**
* Look for a bookmark in database and in order to insert or delete it
* @param depictedItem : Bookmark object
* @return boolean : is bookmark now favorite ?
*/
fun updateBookmarkItem(depictedItem: DepictedItem): Boolean {
val bookmarkExists = findBookmarkItem(depictedItem.id)
if (bookmarkExists) {
deleteBookmarkItem(depictedItem)
} else {
addBookmarkItem(depictedItem)
}
return !bookmarkExists
}
/**
* Add a Bookmark to database
* @param depictedItem : Bookmark to add
*/
private fun addBookmarkItem(depictedItem: DepictedItem) {
val db = clientProvider.get()
try {
db.insert(BASE_URI, toContentValues(depictedItem))
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Delete a bookmark from database
* @param depictedItem : Bookmark to delete
*/
private fun deleteBookmarkItem(depictedItem: DepictedItem) {
val db = clientProvider.get()
try {
db.delete(uriForName(depictedItem.id), null, null)
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Find a bookmark from database based on its name
* @param depictedItemID : Bookmark to find
* @return boolean : is bookmark in database ?
*/
fun findBookmarkItem(depictedItemID: String?): Boolean {
if (depictedItemID == null) { //Avoiding NPE's
return false
}
val db = clientProvider.get()
try {
db.query(
BASE_URI,
BookmarkItemsTable.ALL_FIELDS,
COLUMN_ID + "=?",
arrayOf(depictedItemID),
null
).use { cursor ->
if (cursor != null && cursor.moveToFirst()) {
return true
}
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
return false
}
/**
* Recives real data from cursor
* @param cursor : Object for storing database data
* @return DepictedItem
*/
@SuppressLint("Range")
fun fromCursor(cursor: Cursor) = with(cursor) {
DepictedItem(
getString(COLUMN_NAME),
getString(COLUMN_DESCRIPTION),
getString(COLUMN_IMAGE),
getStringArray(COLUMN_INSTANCE_LIST),
convertToCategoryItems(
getStringArray(COLUMN_CATEGORIES_NAME_LIST),
getStringArray(COLUMN_CATEGORIES_DESCRIPTION_LIST),
getStringArray(COLUMN_CATEGORIES_THUMBNAIL_LIST)
),
getString(COLUMN_IS_SELECTED).toBoolean(),
getString(COLUMN_ID)
)
}
private fun convertToCategoryItems(
categoryNameList: List<String>,
categoryDescriptionList: List<String>,
categoryThumbnailList: List<String>
): List<CategoryItem> {
return buildList {
for (i in categoryNameList.indices) {
add(
CategoryItem(
categoryNameList[i],
categoryDescriptionList[i],
categoryThumbnailList[i],
false
)
)
}
}
}
/**
* Takes data from DepictedItem and create a content value object
* @param depictedItem depicted item
* @return ContentValues
*/
private fun toContentValues(depictedItem: DepictedItem): ContentValues {
return contentValuesOf(
COLUMN_NAME to depictedItem.name,
COLUMN_DESCRIPTION to depictedItem.description,
COLUMN_IMAGE to depictedItem.imageUrl,
COLUMN_INSTANCE_LIST to arrayToString(depictedItem.instanceOfs),
COLUMN_CATEGORIES_NAME_LIST to arrayToString(depictedItem.commonsCategories.map { it.name }),
COLUMN_CATEGORIES_DESCRIPTION_LIST to arrayToString(depictedItem.commonsCategories.map { it.description }),
COLUMN_CATEGORIES_THUMBNAIL_LIST to arrayToString(depictedItem.commonsCategories.map { it.thumbnail }),
COLUMN_IS_SELECTED to depictedItem.isSelected,
COLUMN_ID to depictedItem.id,
)
}
}

View file

@ -1,81 +0,0 @@
package fr.free.nrw.commons.bookmarks.items;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import java.util.List;
import javax.inject.Inject;
import org.jetbrains.annotations.NotNull;
/**
* Tab fragment to show list of bookmarked Wikidata Items
*/
public class BookmarkItemsFragment extends DaggerFragment {
private FragmentBookmarksItemsBinding binding;
@Inject
BookmarkItemsController controller;
public static BookmarkItemsFragment newInstance() {
return new BookmarkItemsFragment();
}
@Override
public View onCreateView(
@NonNull final LayoutInflater inflater,
final ViewGroup container,
final Bundle savedInstanceState
) {
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(final @NotNull View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initList(requireContext());
}
@Override
public void onResume() {
super.onResume();
initList(requireContext());
}
/**
* Get list of DepictedItem and sets to the adapter
* @param context context
*/
private void initList(final Context context) {
final List<DepictedItem> depictItems = controller.loadFavoritesItems();
final BookmarkItemsAdapter adapter = new BookmarkItemsAdapter(depictItems, context);
binding.listView.setAdapter(adapter);
binding.loadingImagesProgressBar.setVisibility(View.GONE);
if (depictItems.isEmpty()) {
binding.statusMessage.setText(R.string.bookmark_empty);
binding.statusMessage.setVisibility(View.VISIBLE);
} else {
binding.statusMessage.setVisibility(View.GONE);
}
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,62 @@
package fr.free.nrw.commons.bookmarks.items
import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import dagger.android.support.DaggerFragment
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.FragmentBookmarksItemsBinding
import javax.inject.Inject
/**
* Tab fragment to show list of bookmarked Wikidata Items
*/
class BookmarkItemsFragment : DaggerFragment() {
private var binding: FragmentBookmarksItemsBinding? = null
@JvmField
@Inject
var controller: BookmarkItemsController? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentBookmarksItemsBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initList(requireContext())
}
override fun onResume() {
super.onResume()
initList(requireContext())
}
/**
* Get list of DepictedItem and sets to the adapter
* @param context context
*/
private fun initList(context: Context) {
val depictItems = controller!!.loadFavoritesItems()
binding!!.listView.adapter = BookmarkItemsAdapter(depictItems, context)
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (depictItems.isEmpty()) {
binding!!.statusMessage.setText(R.string.bookmark_empty)
binding!!.statusMessage.visibility = View.VISIBLE
} else {
binding!!.statusMessage.visibility = View.GONE
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -0,0 +1,90 @@
package fr.free.nrw.commons.bookmarks.items
import android.database.sqlite.SQLiteDatabase
/**
* Table of bookmarksItems data
*/
object BookmarkItemsTable {
const val TABLE_NAME = "bookmarksItems"
const val COLUMN_NAME = "item_name"
const val COLUMN_DESCRIPTION = "item_description"
const val COLUMN_IMAGE = "item_image_url"
const val COLUMN_INSTANCE_LIST = "item_instance_of"
const val COLUMN_CATEGORIES_NAME_LIST = "item_name_categories"
const val COLUMN_CATEGORIES_DESCRIPTION_LIST = "item_description_categories"
const val COLUMN_CATEGORIES_THUMBNAIL_LIST = "item_thumbnail_categories"
const val COLUMN_IS_SELECTED = "item_is_selected"
const val COLUMN_ID = "item_id"
val ALL_FIELDS = arrayOf(
COLUMN_NAME,
COLUMN_DESCRIPTION,
COLUMN_IMAGE,
COLUMN_INSTANCE_LIST,
COLUMN_CATEGORIES_NAME_LIST,
COLUMN_CATEGORIES_DESCRIPTION_LIST,
COLUMN_CATEGORIES_THUMBNAIL_LIST,
COLUMN_IS_SELECTED,
COLUMN_ID
)
const val DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS $TABLE_NAME"
val CREATE_TABLE_STATEMENT =
"""CREATE TABLE $TABLE_NAME (
$COLUMN_NAME STRING,
$COLUMN_DESCRIPTION STRING,
$COLUMN_IMAGE STRING,
$COLUMN_INSTANCE_LIST STRING,
$COLUMN_CATEGORIES_NAME_LIST STRING,
$COLUMN_CATEGORIES_DESCRIPTION_LIST STRING,
$COLUMN_CATEGORIES_THUMBNAIL_LIST STRING,
$COLUMN_IS_SELECTED STRING,
$COLUMN_ID STRING PRIMARY KEY
);""".trimIndent()
/**
* Creates table
*
* @param db SQLiteDatabase
*/
fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT)
}
/**
* Deletes database
*
* @param db SQLiteDatabase
*/
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db)
}
/**
* Updates database
*
* @param db SQLiteDatabase
* @param from starting
* @param to end
*/
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
if (from == to) {
return
}
if (from < 18) {
// doesn't exist yet
onUpdate(db, from + 1, to)
return
}
if (from == 18) {
// table added in version 19
onCreate(db)
onUpdate(db, from + 1, to)
}
}
}

View file

@ -1,120 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
// We can get uri using java.Net.Uri, but andoid implimentation is faster (but it's forgiving with handling exceptions though)
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import javax.inject.Inject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME;
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao.Table.TABLE_NAME;
/**
* Handles private storage for Bookmark pictures
*/
public class BookmarkPicturesContentProvider extends CommonsDaggerContentProvider {
private static final String BASE_PATH = "bookmarks";
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.BOOKMARK_AUTHORITY + "/" + BASE_PATH);
/**
* Append bookmark pictures name to the base uri
*/
public static Uri uriForName(String name) {
return Uri.parse(BASE_URI.toString() + "/" + name);
}
@Inject
DBOpenHelper dbOpenHelper;
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* Queries the SQLite database for the bookmark pictures
* @param uri : contains the uri for bookmark pictures
* @param projection
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor = queryBuilder.query(db, projection, selection, selectionArgs, null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark pictures
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_MEDIA_NAME + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
/**
* Handles the insertion of new bookmark pictures record to local SQLite Database
*/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id = sqlDB.insert(BookmarkPicturesDao.Table.TABLE_NAME, null, contentValues);
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
@SuppressWarnings("ConstantConditions")
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows;
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Timber.d("Deleting bookmark name %s", uri.getLastPathSegment());
rows = db.delete(TABLE_NAME,
"media_name = ?",
new String[]{uri.getLastPathSegment()}
);
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
}

View file

@ -0,0 +1,100 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import androidx.core.net.toUri
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.TABLE_NAME
/**
* Handles private storage for Bookmark pictures
*/
class BookmarkPicturesContentProvider : CommonsDaggerContentProvider() {
override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the bookmark pictures
* @param uri : contains the uri for bookmark pictures
* @param projection
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
* @param sortOrder : ascending or descending
*/
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val cursor = queryBuilder.query(
requireDb(), projection, selection,
selectionArgs, null, null, sortOrder
)
cursor.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
/**
* Handles the update query of local SQLite Database
* @param uri : contains the uri for bookmark pictures
* @param contentValues : new values to be entered to db
* @param selection : handles Where
* @param selectionArgs : the condition of Where clause
*/
override fun update(
uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment!!.toInt()
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_MEDIA_NAME = ?",
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID"
)
}
requireContext().contentResolver.notifyChange(uri, null)
return rowsUpdated
}
/**
* Handles the insertion of new bookmark pictures record to local SQLite Database
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
val id = requireDb().insert(TABLE_NAME, null, contentValues)
requireContext().contentResolver.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val rows: Int = requireDb().delete(
TABLE_NAME,
"media_name = ?",
arrayOf(uri.lastPathSegment)
)
requireContext().contentResolver.notifyChange(uri, null)
return rows
}
companion object {
private const val BASE_PATH = "bookmarks"
@JvmField
val BASE_URI: Uri = "content://${BuildConfig.BOOKMARK_AUTHORITY}/$BASE_PATH".toUri()
@JvmStatic
fun uriForName(name: String): Uri = "$BASE_URI/$name".toUri()
}
}

View file

@ -1,63 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.bookmarks.models.Bookmark;
import fr.free.nrw.commons.media.MediaClient;
import io.reactivex.Observable;
import io.reactivex.ObservableSource;
import io.reactivex.Single;
import io.reactivex.functions.Function;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class BookmarkPicturesController {
private final MediaClient mediaClient;
private final BookmarkPicturesDao bookmarkDao;
private List<Bookmark> currentBookmarks;
@Inject
public BookmarkPicturesController(MediaClient mediaClient, BookmarkPicturesDao bookmarkDao) {
this.mediaClient = mediaClient;
this.bookmarkDao = bookmarkDao;
currentBookmarks = new ArrayList<>();
}
/**
* Loads the Media objects from the raw data stored in DB and the API.
* @return a list of bookmarked Media object
*/
Single<List<Media>> loadBookmarkedPictures() {
List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
currentBookmarks = bookmarks;
return Observable.fromIterable(bookmarks)
.flatMap((Function<Bookmark, ObservableSource<Media>>) this::getMediaFromBookmark)
.toList();
}
private Observable<Media> getMediaFromBookmark(Bookmark bookmark) {
return mediaClient.getMedia(bookmark.getMediaName())
.toObservable()
.onErrorResumeNext(Observable.empty());
}
/**
* Loads the Media objects from the raw data stored in DB and the API.
* @return a list of bookmarked Media object
*/
boolean needRefreshBookmarkedPictures() {
List<Bookmark> bookmarks = bookmarkDao.getAllBookmarks();
return bookmarks.size() != currentBookmarks.size();
}
/**
* Cancels the requests to the API and the DB
*/
void stop() {
//noop
}
}

View file

@ -0,0 +1,38 @@
package fr.free.nrw.commons.bookmarks.pictures
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.bookmarks.models.Bookmark
import fr.free.nrw.commons.media.MediaClient
import io.reactivex.Observable
import io.reactivex.Single
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class BookmarkPicturesController @Inject constructor(
private val mediaClient: MediaClient,
private val bookmarkDao: BookmarkPicturesDao
) {
private var currentBookmarks: List<Bookmark> = listOf()
/**
* Loads the Media objects from the raw data stored in DB and the API.
* @return a list of bookmarked Media object
*/
fun loadBookmarkedPictures(): Single<List<Media>> {
val bookmarks = bookmarkDao.getAllBookmarks()
currentBookmarks = bookmarks
return Observable.fromIterable(bookmarks).flatMap {
mediaClient.getMedia(it.mediaName)
.toObservable()
.onErrorResumeNext(Observable.empty())
}.toList()
}
fun needRefreshBookmarkedPictures(): Boolean {
val bookmarks = bookmarkDao.getAllBookmarks()
return bookmarks.size != currentBookmarks.size
}
fun stop() = Unit
}

View file

@ -1,227 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import javax.inject.Singleton;
import fr.free.nrw.commons.bookmarks.models.Bookmark;
import static fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.BASE_URI;
@Singleton
public class BookmarkPicturesDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public BookmarkPicturesDao(@Named("bookmarks") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* Find all persisted pictures bookmarks on database
*
* @return list of bookmarks
*/
@NonNull
public List<Bookmark> getAllBookmarks() {
List<Bookmark> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
BookmarkPicturesContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
null);
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor));
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
/**
* Look for a bookmark in database and in order to insert or delete it
*
* @param bookmark : Bookmark object
* @return boolean : is bookmark now fav ?
*/
public boolean updateBookmark(Bookmark bookmark) {
boolean bookmarkExists = findBookmark(bookmark);
if (bookmarkExists) {
deleteBookmark(bookmark);
} else {
addBookmark(bookmark);
}
return !bookmarkExists;
}
/**
* Add a Bookmark to database
*
* @param bookmark : Bookmark to add
*/
private void addBookmark(Bookmark bookmark) {
ContentProviderClient db = clientProvider.get();
try {
db.insert(BASE_URI, toContentValues(bookmark));
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Delete a bookmark from database
*
* @param bookmark : Bookmark to delete
*/
private void deleteBookmark(Bookmark bookmark) {
ContentProviderClient db = clientProvider.get();
try {
if (bookmark.getContentUri() == null) {
throw new RuntimeException("tried to delete item with no content URI");
} else {
db.delete(bookmark.getContentUri(), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find a bookmark from database based on its name
*
* @param bookmark : Bookmark to find
* @return boolean : is bookmark in database ?
*/
public boolean findBookmark(Bookmark bookmark) {
if (bookmark == null) {//Avoiding NPE's
return false;
}
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
BookmarkPicturesContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_MEDIA_NAME + "=?",
new String[]{bookmark.getMediaName()},
null);
if (cursor != null && cursor.moveToFirst()) {
return true;
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return false;
}
@SuppressLint("Range")
@NonNull
Bookmark fromCursor(Cursor cursor) {
String fileName = cursor.getString(cursor.getColumnIndex(Table.COLUMN_MEDIA_NAME));
return new Bookmark(
fileName,
cursor.getString(cursor.getColumnIndex(Table.COLUMN_CREATOR)),
BookmarkPicturesContentProvider.uriForName(fileName)
);
}
private ContentValues toContentValues(Bookmark bookmark) {
ContentValues cv = new ContentValues();
cv.put(BookmarkPicturesDao.Table.COLUMN_MEDIA_NAME, bookmark.getMediaName());
cv.put(BookmarkPicturesDao.Table.COLUMN_CREATOR, bookmark.getMediaCreator());
return cv;
}
public static class Table {
public static final String TABLE_NAME = "bookmarks";
public static final String COLUMN_MEDIA_NAME = "media_name";
public static final String COLUMN_CREATOR = "media_creator";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_MEDIA_NAME,
COLUMN_CREATOR
};
public static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
public static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_MEDIA_NAME + " STRING PRIMARY KEY,"
+ COLUMN_CREATOR + " STRING"
+ ");";
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if (from == to) {
return;
}
if (from < 7) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 7) {
// table added in version 8
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if (from == 8) {
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -0,0 +1,141 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.content.ContentProviderClient
import android.content.ContentValues
import android.database.Cursor
import android.os.RemoteException
import androidx.core.content.contentValuesOf
import fr.free.nrw.commons.bookmarks.models.Bookmark
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.BASE_URI
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider.Companion.uriForName
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.ALL_FIELDS
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_CREATOR
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable.COLUMN_MEDIA_NAME
import fr.free.nrw.commons.utils.getString
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
import javax.inject.Singleton
@Singleton
class BookmarkPicturesDao @Inject constructor(
@param:Named("bookmarks") private val clientProvider: Provider<ContentProviderClient>
) {
/**
* Find all persisted pictures bookmarks on database
*
* @return list of bookmarks
*/
fun getAllBookmarks(): List<Bookmark> {
val items: MutableList<Bookmark> = mutableListOf()
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
BASE_URI, ALL_FIELDS, null, arrayOf(), null
)
while (cursor != null && cursor.moveToNext()) {
items.add(fromCursor(cursor))
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return items
}
/**
* Look for a bookmark in database and in order to insert or delete it
*
* @param bookmark : Bookmark object
* @return boolean : is bookmark now fav ?
*/
fun updateBookmark(bookmark: Bookmark): Boolean {
val bookmarkExists = findBookmark(bookmark)
if (bookmarkExists) {
deleteBookmark(bookmark)
} else {
addBookmark(bookmark)
}
return !bookmarkExists
}
/**
* Add a Bookmark to database
*
* @param bookmark : Bookmark to add
*/
private fun addBookmark(bookmark: Bookmark) {
val db = clientProvider.get()
try {
db.insert(BASE_URI, toContentValues(bookmark))
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Delete a bookmark from database
*
* @param bookmark : Bookmark to delete
*/
private fun deleteBookmark(bookmark: Bookmark) {
val db = clientProvider.get()
try {
if (bookmark.contentUri == null) {
throw RuntimeException("tried to delete item with no content URI")
} else {
db.delete(bookmark.contentUri!!, null, null)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Find a bookmark from database based on its name
*
* @param bookmark : Bookmark to find
* @return boolean : is bookmark in database ?
*/
fun findBookmark(bookmark: Bookmark?): Boolean {
if (bookmark == null) {
return false
}
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
BASE_URI, ALL_FIELDS, "$COLUMN_MEDIA_NAME=?", arrayOf(bookmark.mediaName), null
)
if (cursor != null && cursor.moveToFirst()) {
return true
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return false
}
fun fromCursor(cursor: Cursor): Bookmark {
val fileName = cursor.getString(COLUMN_MEDIA_NAME)
return Bookmark(
fileName, cursor.getString(COLUMN_CREATOR), uriForName(fileName)
)
}
private fun toContentValues(bookmark: Bookmark): ContentValues = contentValuesOf(
COLUMN_MEDIA_NAME to bookmark.mediaName,
COLUMN_CREATOR to bookmark.mediaCreator
)
}

View file

@ -1,218 +0,0 @@
package fr.free.nrw.commons.bookmarks.pictures;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import dagger.android.support.DaggerFragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment;
import fr.free.nrw.commons.category.GridViewAdapter;
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding;
import fr.free.nrw.commons.utils.NetworkUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import java.util.List;
import javax.inject.Inject;
import timber.log.Timber;
public class BookmarkPicturesFragment extends DaggerFragment {
private GridViewAdapter gridAdapter;
private CompositeDisposable compositeDisposable = new CompositeDisposable();
private FragmentBookmarksPicturesBinding binding;
@Inject
BookmarkPicturesController controller;
/**
* Create an instance of the fragment with the right bundle parameters
* @return an instance of the fragment
*/
public static BookmarkPicturesFragment newInstance() {
return new BookmarkPicturesFragment();
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState
) {
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
binding.bookmarkedPicturesList.setOnItemClickListener((AdapterView.OnItemClickListener) getParentFragment());
initList();
}
@Override
public void onStop() {
super.onStop();
controller.stop();
}
@Override
public void onDestroy() {
super.onDestroy();
compositeDisposable.clear();
binding = null;
}
@Override
public void onResume() {
super.onResume();
if (controller.needRefreshBookmarkedPictures()) {
binding.bookmarkedPicturesList.setVisibility(GONE);
if (gridAdapter != null) {
gridAdapter.clear();
((BookmarkListRootFragment)getParentFragment()).viewPagerNotifyDataSetChanged();
}
initList();
}
}
/**
* Checks for internet connection and then initializes
* the recycler view with bookmarked pictures
*/
@SuppressLint("CheckResult")
private void initList() {
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
handleNoInternet();
return;
}
binding.loadingImagesProgressBar.setVisibility(VISIBLE);
binding.statusMessage.setVisibility(GONE);
compositeDisposable.add(controller.loadBookmarkedPictures()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleSuccess, this::handleError));
}
/**
* Handles the UI updates for no internet scenario
*/
private void handleNoInternet() {
binding.loadingImagesProgressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
binding.statusMessage.setVisibility(VISIBLE);
binding.statusMessage.setText(getString(R.string.no_internet));
} else {
ViewUtil.showShortSnackbar(binding.parentLayout, R.string.no_internet);
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private void handleError(Throwable throwable) {
Timber.e(throwable, "Error occurred while loading images inside a category");
try{
ViewUtil.showShortSnackbar(binding.getRoot(), R.string.error_loading_images);
initErrorView();
}catch (Exception e){
e.printStackTrace();
}
}
/**
* Handles the UI updates for a error scenario
*/
private void initErrorView() {
binding.loadingImagesProgressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
binding.statusMessage.setVisibility(VISIBLE);
binding.statusMessage.setText(getString(R.string.no_images_found));
} else {
binding.statusMessage.setVisibility(GONE);
}
}
/**
* Handles the UI updates when there is no bookmarks
*/
private void initEmptyBookmarkListView() {
binding.loadingImagesProgressBar.setVisibility(GONE);
if (gridAdapter == null || gridAdapter.isEmpty()) {
binding.statusMessage.setVisibility(VISIBLE);
binding.statusMessage.setText(getString(R.string.bookmark_empty));
} else {
binding.statusMessage.setVisibility(GONE);
}
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection List of new Media to be displayed
*/
private void handleSuccess(List<Media> collection) {
if (collection == null) {
initErrorView();
return;
}
if (collection.isEmpty()) {
initEmptyBookmarkListView();
return;
}
if (gridAdapter == null) {
setAdapter(collection);
} else {
if (gridAdapter.containsAll(collection)) {
binding.loadingImagesProgressBar.setVisibility(GONE);
binding.statusMessage.setVisibility(GONE);
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
return;
}
gridAdapter.addItems(collection);
((BookmarkListRootFragment) getParentFragment()).viewPagerNotifyDataSetChanged();
}
binding.loadingImagesProgressBar.setVisibility(GONE);
binding.statusMessage.setVisibility(GONE);
binding.bookmarkedPicturesList.setVisibility(VISIBLE);
}
/**
* Initializes the adapter with a list of Media objects
* @param mediaList List of new Media to be displayed
*/
private void setAdapter(List<Media> mediaList) {
gridAdapter = new GridViewAdapter(
this.getContext(),
R.layout.layout_category_images,
mediaList
);
binding.bookmarkedPicturesList.setAdapter(gridAdapter);
}
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
* @return GridView Adapter
*/
public ListAdapter getAdapter() {
return binding.bookmarkedPicturesList.getAdapter();
}
}

View file

@ -0,0 +1,201 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.annotation.SuppressLint
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView.OnItemClickListener
import android.widget.ListAdapter
import dagger.android.support.DaggerFragment
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.bookmarks.BookmarkListRootFragment
import fr.free.nrw.commons.category.GridViewAdapter
import fr.free.nrw.commons.databinding.FragmentBookmarksPicturesBinding
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.CompositeDisposable
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import timber.log.Timber
import javax.inject.Inject
class BookmarkPicturesFragment : DaggerFragment() {
private var gridAdapter: GridViewAdapter? = null
private val compositeDisposable = CompositeDisposable()
private var binding: FragmentBookmarksPicturesBinding? = null
@JvmField
@Inject
var controller: BookmarkPicturesController? = null
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentBookmarksPicturesBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding!!.bookmarkedPicturesList.onItemClickListener =
parentFragment as OnItemClickListener?
initList()
}
override fun onStop() {
super.onStop()
controller!!.stop()
}
override fun onDestroy() {
super.onDestroy()
compositeDisposable.clear()
binding = null
}
override fun onResume() {
super.onResume()
if (controller!!.needRefreshBookmarkedPictures()) {
binding!!.bookmarkedPicturesList.visibility = View.GONE
gridAdapter?.let {
it.clear()
(parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged()
}
initList()
}
}
/**
* Checks for internet connection and then initializes
* the recycler view with bookmarked pictures
*/
@SuppressLint("CheckResult")
private fun initList() {
if (!isInternetConnectionEstablished(context)) {
handleNoInternet()
return
}
binding!!.loadingImagesProgressBar.visibility = View.VISIBLE
binding!!.statusMessage.visibility = View.GONE
compositeDisposable.add(
controller!!.loadBookmarkedPictures()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(::handleSuccess, ::handleError)
)
}
/**
* Handles the UI updates for no internet scenario
*/
private fun handleNoInternet() {
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (gridAdapter == null || gridAdapter!!.isEmpty) {
binding!!.statusMessage.visibility = View.VISIBLE
binding!!.statusMessage.text = getString(R.string.no_internet)
} else {
showShortSnackbar(binding!!.parentLayout, R.string.no_internet)
}
}
/**
* Logs and handles API error scenario
* @param throwable
*/
private fun handleError(throwable: Throwable) {
Timber.e(throwable, "Error occurred while loading images inside a category")
try {
showShortSnackbar(binding!!.root, R.string.error_loading_images)
initErrorView()
} catch (e: Exception) {
Timber.e(e)
}
}
/**
* Handles the UI updates for a error scenario
*/
private fun initErrorView() {
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (gridAdapter == null || gridAdapter!!.isEmpty) {
binding!!.statusMessage.visibility = View.VISIBLE
binding!!.statusMessage.text = getString(R.string.no_images_found)
} else {
binding!!.statusMessage.visibility = View.GONE
}
}
/**
* Handles the UI updates when there is no bookmarks
*/
private fun initEmptyBookmarkListView() {
binding!!.loadingImagesProgressBar.visibility = View.GONE
if (gridAdapter == null || gridAdapter!!.isEmpty) {
binding!!.statusMessage.visibility = View.VISIBLE
binding!!.statusMessage.text = getString(R.string.bookmark_empty)
} else {
binding!!.statusMessage.visibility = View.GONE
}
}
/**
* Handles the success scenario
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
* @param collection List of new Media to be displayed
*/
private fun handleSuccess(collection: List<Media>?) {
if (collection == null) {
initErrorView()
return
}
if (collection.isEmpty()) {
initEmptyBookmarkListView()
return
}
if (gridAdapter == null) {
setAdapter(collection)
} else {
if (gridAdapter!!.containsAll(collection)) {
binding!!.loadingImagesProgressBar.visibility = View.GONE
binding!!.statusMessage.visibility = View.GONE
binding!!.bookmarkedPicturesList.visibility = View.VISIBLE
binding!!.bookmarkedPicturesList.adapter = gridAdapter
return
}
gridAdapter!!.addItems(collection)
(parentFragment as BookmarkListRootFragment).viewPagerNotifyDataSetChanged()
}
binding!!.loadingImagesProgressBar.visibility = View.GONE
binding!!.statusMessage.visibility = View.GONE
binding!!.bookmarkedPicturesList.visibility = View.VISIBLE
}
/**
* Initializes the adapter with a list of Media objects
* @param mediaList List of new Media to be displayed
*/
private fun setAdapter(mediaList: List<Media>) {
gridAdapter = GridViewAdapter(
requireContext(),
R.layout.layout_category_images,
mediaList.toMutableList()
)
binding?.let { it.bookmarkedPicturesList.adapter = gridAdapter }
}
/**
* It return an instance of gridView adapter which helps in extracting media details
* used by the gridView
* @return GridView Adapter
*/
fun getAdapter(): ListAdapter? = binding?.bookmarkedPicturesList?.adapter
}

View file

@ -0,0 +1,54 @@
package fr.free.nrw.commons.bookmarks.pictures
import android.database.sqlite.SQLiteDatabase
object BookmarksTable {
const val TABLE_NAME: String = "bookmarks"
const val COLUMN_MEDIA_NAME: String = "media_name"
const val COLUMN_CREATOR: String = "media_creator"
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
val ALL_FIELDS = arrayOf(
COLUMN_MEDIA_NAME,
COLUMN_CREATOR
)
const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME"
const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME (" +
"$COLUMN_MEDIA_NAME STRING PRIMARY KEY, " +
"$COLUMN_CREATOR STRING" +
");")
fun onCreate(db: SQLiteDatabase) =
db.execSQL(CREATE_TABLE_STATEMENT)
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db)
}
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
if (from == to) {
return
}
if (from < 7) {
// doesn't exist yet
onUpdate(db, from+1, to)
return
}
if (from == 7) {
// table added in version 8
onCreate(db)
onUpdate(db, from+1, to)
return
}
if (from == 8) {
onUpdate(db, from+1, to)
return
}
}
}

View file

@ -9,12 +9,9 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.text.TextUtils
import androidx.annotation.NonNull
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import timber.log.Timber
import javax.inject.Inject
import androidx.core.net.toUri
class CategoryContentProvider : CommonsDaggerContentProvider() {
@ -23,9 +20,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
addURI(BuildConfig.CATEGORY_AUTHORITY, "${BASE_PATH}/#", CATEGORIES_ID)
}
@Inject
lateinit var dbOpenHelper: DBOpenHelper
@SuppressWarnings("ConstantConditions")
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
@ -34,7 +28,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
}
val uriType = uriMatcher.match(uri)
val db = dbOpenHelper.readableDatabase
val db = requireDb()
val cursor: Cursor? = when (uriType) {
CATEGORIES -> queryBuilder.query(
@ -58,45 +52,37 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
else -> throw IllegalArgumentException("Unknown URI $uri")
}
cursor?.setNotificationUri(context?.contentResolver, uri)
cursor?.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
override fun getType(uri: Uri): String? {
return null
}
override fun getType(uri: Uri): String? = null
@SuppressWarnings("ConstantConditions")
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
override fun insert(uri: Uri, contentValues: ContentValues?): Uri {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val id: Long
when (uriType) {
CATEGORIES -> {
id = sqlDB.insert(TABLE_NAME, null, contentValues)
id = requireDb().insert(TABLE_NAME, null, contentValues)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
context?.contentResolver?.notifyChange(uri, null)
return Uri.parse("${Companion.BASE_URI}/$id")
requireContext().contentResolver?.notifyChange(uri, null)
return "${BASE_URI}/$id".toUri()
}
@SuppressWarnings("ConstantConditions")
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
// Not implemented
return 0
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
@SuppressWarnings("ConstantConditions")
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
Timber.d("Hello, bulk insert! (CategoryContentProvider)")
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val sqlDB = requireDb()
sqlDB.beginTransaction()
when (uriType) {
CATEGORIES -> {
for (value in values) {
Timber.d("Inserting! %s", value)
sqlDB.insert(TABLE_NAME, null, value)
}
sqlDB.setTransactionSuccessful()
@ -104,7 +90,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
sqlDB.endTransaction()
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return values.size
}
@ -112,17 +98,18 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
override fun update(uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
val uriType = uriMatcher.match(uri)
val sqlDB = dbOpenHelper.writableDatabase
val rowsUpdated: Int
when (uriType) {
CATEGORIES_ID -> {
if (TextUtils.isEmpty(selection)) {
val id = uri.lastPathSegment?.toInt()
?: throw IllegalArgumentException("Invalid ID")
rowsUpdated = sqlDB.update(TABLE_NAME,
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString()))
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID")
@ -130,7 +117,7 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
}
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
}
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return rowsUpdated
}
@ -165,13 +152,9 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
"$COLUMN_TIMES_USED INTEGER" +
");"
fun uriForId(id: Int): Uri {
return Uri.parse("${BASE_URI}/$id")
}
fun uriForId(id: Int): Uri = Uri.parse("${BASE_URI}/$id")
fun onCreate(db: SQLiteDatabase) {
db.execSQL(CREATE_TABLE_STATEMENT)
}
fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT)
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
@ -200,6 +183,6 @@ class CategoryContentProvider : CommonsDaggerContentProvider() {
private const val CATEGORIES = 1
private const val CATEGORIES_ID = 2
private const val BASE_PATH = "categories"
val BASE_URI: Uri = Uri.parse("content://${BuildConfig.CATEGORY_AUTHORITY}/${Companion.BASE_PATH}")
val BASE_URI: Uri = "content://${BuildConfig.CATEGORY_AUTHORITY}/${BASE_PATH}".toUri()
}
}

View file

@ -4,11 +4,10 @@ import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteException
import android.database.sqlite.SQLiteOpenHelper
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsTable
import fr.free.nrw.commons.bookmarks.pictures.BookmarksTable
import fr.free.nrw.commons.category.CategoryDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
@ -30,17 +29,17 @@ class DBOpenHelper(
*/
override fun onCreate(db: SQLiteDatabase) {
CategoryDao.Table.onCreate(db)
BookmarkPicturesDao.Table.onCreate(db)
BookmarkItemsDao.Table.onCreate(db)
RecentSearchesDao.Table.onCreate(db)
BookmarksTable.onCreate(db)
BookmarkItemsTable.onCreate(db)
RecentSearchesTable.onCreate(db)
RecentLanguagesDao.Table.onCreate(db)
}
override fun onUpgrade(db: SQLiteDatabase, from: Int, to: Int) {
CategoryDao.Table.onUpdate(db, from, to)
BookmarkPicturesDao.Table.onUpdate(db, from, to)
BookmarkItemsDao.Table.onUpdate(db, from, to)
RecentSearchesDao.Table.onUpdate(db, from, to)
BookmarksTable.onUpdate(db, from, to)
BookmarkItemsTable.onUpdate(db, from, to)
RecentSearchesTable.onUpdate(db, from, to)
RecentLanguagesDao.Table.onUpdate(db, from, to)
deleteTable(db, CONTRIBUTIONS_TABLE)
deleteTable(db, BOOKMARKS_LOCATIONS)

View file

@ -1,14 +1,25 @@
package fr.free.nrw.commons.di
import android.content.ContentProvider
import android.database.sqlite.SQLiteDatabase
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.ApplicationlessInjection.Companion.getInstance
import javax.inject.Inject
abstract class CommonsDaggerContentProvider : ContentProvider() {
@JvmField
@Inject
var dbOpenHelper: DBOpenHelper? = null
override fun onCreate(): Boolean {
inject()
return true
}
fun requireDbOpenHelper(): DBOpenHelper = dbOpenHelper!!
fun requireDb(): SQLiteDatabase = requireDbOpenHelper().writableDatabase!!
private fun inject() {
val injection = getInstance(context!!)

View file

@ -1,260 +0,0 @@
package fr.free.nrw.commons.explore;
import static androidx.viewpager.widget.ViewPager.SCROLL_STATE_IDLE;
import static fr.free.nrw.commons.ViewPagerAdapter.pairOf;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentPagerAdapter;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentExploreBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.ActivityUtils;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import javax.inject.Named;
import kotlin.Pair;
public class ExploreFragment extends CommonsDaggerSupportFragment {
private static final String FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons";
private static final String MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android";
private static final String EXPLORE_MAP = "Map";
private static final String MEDIA_DETAILS_FRAGMENT_TAG = "MediaDetailsFragment";
public FragmentExploreBinding binding;
ViewPagerAdapter viewPagerAdapter;
private ExploreListRootFragment featuredRootFragment;
private ExploreListRootFragment mobileRootFragment;
private ExploreMapRootFragment mapRootFragment;
@Inject
@Named("default_preferences")
public JsonKvStore applicationKvStore;
// Nearby map state (for if we came from Nearby fragment)
private double prevZoom;
private double prevLatitude;
private double prevLongitude;
public void setScroll(boolean canScroll) {
if (binding != null) {
binding.viewPager.setCanScroll(canScroll);
}
}
@NonNull
public static ExploreFragment newInstance() {
ExploreFragment fragment = new ExploreFragment();
fragment.setRetainInstance(true);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loadNearbyMapData();
binding = FragmentExploreBinding.inflate(inflater, container, false);
viewPagerAdapter = new ViewPagerAdapter(requireContext(), getChildFragmentManager(),
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setId(R.id.viewPager);
binding.tabLayout.setupWithViewPager(binding.viewPager);
binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
if (position == 2) {
binding.viewPager.setCanScroll(false);
} else {
binding.viewPager.setCanScroll(true);
}
}
@Override
public void onPageScrollStateChanged(int state) {
}
});
setTabs();
setHasOptionsMenu(true);
// if we came from 'Show in Explore' in Nearby, jump to Map tab
if (isCameFromNearbyMap()) {
binding.viewPager.setCurrentItem(2);
}
return binding.getRoot();
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
public void setTabs() {
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY);
Bundle mobileArguments = new Bundle();
mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY);
Bundle mapArguments = new Bundle();
mapArguments.putString("categoryName", EXPLORE_MAP);
// if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root
if (isCameFromNearbyMap()) {
mapArguments.putDouble("prev_zoom", prevZoom);
mapArguments.putDouble("prev_latitude", prevLatitude);
mapArguments.putDouble("prev_longitude", prevLongitude);
}
featuredRootFragment = new ExploreListRootFragment(featuredArguments);
mobileRootFragment = new ExploreListRootFragment(mobileArguments);
mapRootFragment = new ExploreMapRootFragment(mapArguments);
((MainActivity) getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
viewPagerAdapter.setTabs(
pairOf(R.string.explore_tab_title_featured, featuredRootFragment),
pairOf(R.string.explore_tab_title_mobile, mobileRootFragment),
pairOf(R.string.explore_tab_title_map, mapRootFragment)
);
viewPagerAdapter.notifyDataSetChanged();
}
/**
* Fetch Nearby map camera data from fragment arguments if any.
*/
public void loadNearbyMapData() {
// get fragment arguments
if (getArguments() != null) {
prevZoom = getArguments().getDouble("prev_zoom");
prevLatitude = getArguments().getDouble("prev_latitude");
prevLongitude = getArguments().getDouble("prev_longitude");
}
}
/**
* Checks if fragment arguments contain data from Nearby map. if present, then the user
* navigated from Nearby using 'Show in Explore'.
*
* @return true if user navigated from Nearby map
**/
public boolean isCameFromNearbyMap() {
return prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0;
}
public boolean onBackPressed() {
if (binding.tabLayout.getSelectedTabPosition() == 0) {
if (featuredRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
} else if (binding.tabLayout.getSelectedTabPosition() == 1) { //Mobile root fragment
if (mobileRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
} else { //explore map fragment
if (mapRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
}
return false;
}
/**
* This method inflates the menu in the toolbar
*/
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
// if logged in 'Show in Nearby' menu item is visible
if (applicationKvStore.getBoolean("login_skipped") == false) {
inflater.inflate(R.menu.explore_fragment_menu, menu);
MenuItem others = menu.findItem(R.id.list_item_show_in_nearby);
if (binding.viewPager.getCurrentItem() == 2) {
others.setVisible(true);
}
// if on Map tab, show all menu options, else only show search
binding.viewPager.addOnPageChangeListener(new OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset,
int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
others.setVisible((position == 2));
}
@Override
public void onPageScrollStateChanged(int state) {
if (state == SCROLL_STATE_IDLE && binding.viewPager.getCurrentItem() == 2) {
onPageSelected(2);
}
}
});
} else {
inflater.inflate(R.menu.menu_search, menu);
}
super.onCreateOptionsMenu(menu, inflater);
}
/**
* This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is
* available to open search page of the app
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle item selection
switch (item.getItemId()) {
case R.id.action_search:
ActivityUtils.startActivityWithFlags(getActivity(), SearchActivity.class);
return true;
case R.id.list_item_show_in_nearby:
mapRootFragment.loadNearbyMapFromExplore();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,229 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.FragmentPagerAdapter
import androidx.viewpager.widget.ViewPager
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentExploreBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.ActivityUtils.startActivityWithFlags
import javax.inject.Inject
import javax.inject.Named
class ExploreFragment : CommonsDaggerSupportFragment() {
@JvmField
@Inject
@Named("default_preferences")
var applicationKvStore: JsonKvStore? = null
private var featuredRootFragment: ExploreListRootFragment? = null
private var mobileRootFragment: ExploreListRootFragment? = null
private var mapRootFragment: ExploreMapRootFragment? = null
private var prevZoom = 0.0
private var prevLatitude = 0.0
private var prevLongitude = 0.0
private var viewPagerAdapter: ViewPagerAdapter? = null
var binding: FragmentExploreBinding? = null
fun setScroll(canScroll: Boolean) {
if (binding != null) {
binding!!.viewPager.canScroll = canScroll
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreate(savedInstanceState)
loadNearbyMapData()
binding = FragmentExploreBinding.inflate(inflater, container, false)
viewPagerAdapter = ViewPagerAdapter(
requireContext(), childFragmentManager,
FragmentPagerAdapter.BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT
)
binding!!.viewPager.adapter = viewPagerAdapter
binding!!.viewPager.id = R.id.viewPager
binding!!.tabLayout.setupWithViewPager(binding!!.viewPager)
binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
override fun onPageScrollStateChanged(state: Int) = Unit
override fun onPageSelected(position: Int) {
binding!!.viewPager.canScroll = position != 2
}
})
setTabs()
setHasOptionsMenu(true)
// if we came from 'Show in Explore' in Nearby, jump to Map tab
if (isCameFromNearbyMap) {
binding!!.viewPager.currentItem = 2
}
return binding!!.root
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
fun setTabs() {
val featuredArguments = Bundle()
featuredArguments.putString("categoryName", FEATURED_IMAGES_CATEGORY)
val mobileArguments = Bundle()
mobileArguments.putString("categoryName", MOBILE_UPLOADS_CATEGORY)
val mapArguments = Bundle()
mapArguments.putString("categoryName", EXPLORE_MAP)
// if we came from 'Show in Explore' in Nearby, pass on zoom and center to Explore map root
if (isCameFromNearbyMap) {
mapArguments.putDouble("prev_zoom", prevZoom)
mapArguments.putDouble("prev_latitude", prevLatitude)
mapArguments.putDouble("prev_longitude", prevLongitude)
}
featuredRootFragment = ExploreListRootFragment(featuredArguments)
mobileRootFragment = ExploreListRootFragment(mobileArguments)
mapRootFragment = ExploreMapRootFragment(mapArguments)
(activity as MainActivity).showTabs()
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
viewPagerAdapter!!.setTabs(
R.string.explore_tab_title_featured to featuredRootFragment!!,
R.string.explore_tab_title_mobile to mobileRootFragment!!,
R.string.explore_tab_title_map to mapRootFragment!!
)
viewPagerAdapter!!.notifyDataSetChanged()
}
/**
* Fetch Nearby map camera data from fragment arguments if any.
*/
private fun loadNearbyMapData() {
// get fragment arguments
if (arguments != null) {
with (requireArguments()) {
prevZoom = getDouble("prev_zoom")
prevLatitude = getDouble("prev_latitude")
prevLongitude = getDouble("prev_longitude")
}
}
}
/**
* Checks if fragment arguments contain data from Nearby map. if present, then the user
* navigated from Nearby using 'Show in Explore'.
*
* @return true if user navigated from Nearby map
*/
private val isCameFromNearbyMap: Boolean
get() = prevZoom != 0.0 || prevLatitude != 0.0 || prevLongitude != 0.0
fun onBackPressed(): Boolean {
if (binding!!.tabLayout.selectedTabPosition == 0) {
if (featuredRootFragment!!.backPressed()) {
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
return true
}
} else if (binding!!.tabLayout.selectedTabPosition == 1) { //Mobile root fragment
if (mobileRootFragment!!.backPressed()) {
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
return true
}
} else { //explore map fragment
if (mapRootFragment!!.backPressed()) {
(activity as BaseActivity).supportActionBar!!.setDisplayHomeAsUpEnabled(false)
return true
}
}
return false
}
/**
* This method inflates the menu in the toolbar
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// if logged in 'Show in Nearby' menu item is visible
if (applicationKvStore!!.getBoolean("login_skipped") == false) {
inflater.inflate(R.menu.explore_fragment_menu, menu)
val others = menu.findItem(R.id.list_item_show_in_nearby)
if (binding!!.viewPager.currentItem == 2) {
others.setVisible(true)
}
// if on Map tab, show all menu options, else only show search
binding!!.viewPager.addOnPageChangeListener(object : OnPageChangeListener {
override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
override fun onPageSelected(position: Int) {
others.setVisible((position == 2))
}
override fun onPageScrollStateChanged(state: Int) {
if (state == ViewPager.SCROLL_STATE_IDLE && binding!!.viewPager.currentItem == 2) {
onPageSelected(2)
}
}
})
} else {
inflater.inflate(R.menu.menu_search, menu)
}
super.onCreateOptionsMenu(menu, inflater)
}
/**
* This method handles the logic on ItemSelect in toolbar menu Currently only 1 choice is
* available to open search page of the app
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle item selection
when (item.itemId) {
R.id.action_search -> {
startActivityWithFlags(requireActivity(), SearchActivity::class.java)
return true
}
R.id.list_item_show_in_nearby -> {
mapRootFragment!!.loadNearbyMapFromExplore()
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
companion object {
private const val FEATURED_IMAGES_CATEGORY = "Featured_pictures_on_Wikimedia_Commons"
private const val MOBILE_UPLOADS_CATEGORY = "Uploaded_with_Mobile/Android"
private const val EXPLORE_MAP = "Map"
fun newInstance(): ExploreFragment = ExploreFragment().apply {
retainInstance = true
}
}
}

View file

@ -1,215 +0,0 @@
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailProvider;
import fr.free.nrw.commons.navtab.NavTab;
public class ExploreListRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails;
private CategoriesMediaFragment listFragment;
private FragmentFeaturedRootBinding binding;
public ExploreListRootFragment() {
//empty constructor necessary otherwise crashes on recreate
}
public ExploreListRootFragment(Bundle bundle) {
String title = bundle.getString("categoryName");
listFragment = new CategoriesMediaFragment();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
listFragment.setArguments(featuredArguments);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState == null) {
setFragment(listFragment, mediaDetails);
}
}
public void setFragment(Fragment fragment, Fragment otherFragment) {
if (fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
}
}
public void removeFragment(Fragment fragment) {
getChildFragmentManager()
.beginTransaction()
.remove(fragment)
.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public void onMediaClicked(int position) {
if (binding!=null) {
binding.exploreContainer.setVisibility(View.VISIBLE);
}
if (((ExploreFragment) getParentFragment()).binding!=null) {
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
}
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((ExploreFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, listFragment);
mediaDetails.showImage(position);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (listFragment != null) {
return listFragment.getMediaAtPosition(i);
} else {
return null;
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (listFragment != null) {
return listFragment.getTotalMediaCount();
} else {
return 0;
}
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (mediaDetails != null && !listFragment.isVisible()) {
removeFragment(mediaDetails);
onMediaClicked(index);
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
}
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
public boolean backPressed() {
if (null != mediaDetails && mediaDetails.isVisible()) {
if (((ExploreFragment) getParentFragment()).binding != null) {
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
}
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(listFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
} else {
if (((MainActivity) getActivity()) != null) {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
}
if (((MainActivity) getActivity()) != null) {
((MainActivity) getActivity()).showTabs();
}
return false;
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,182 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.navtab.NavTab
class ExploreListRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider,
CategoryImagesCallback {
private var mediaDetails: MediaDetailPagerFragment? = null
private var listFragment: CategoriesMediaFragment? = null
private var binding: FragmentFeaturedRootBinding? = null
constructor()
constructor(bundle: Bundle) {
listFragment = CategoriesMediaFragment().apply {
arguments = bundleOf(
"categoryName" to bundle.getString("categoryName")
)
}
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreate(savedInstanceState)
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) {
setFragment(listFragment!!, mediaDetails)
}
}
fun setFragment(fragment: Fragment, otherFragment: Fragment?) {
if (fragment.isAdded && otherFragment != null) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (fragment.isAdded && otherFragment == null) {
childFragmentManager
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded && otherFragment != null) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded) {
childFragmentManager
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
}
}
private fun removeFragment(fragment: Fragment) {
childFragmentManager
.beginTransaction()
.remove(fragment)
.commit()
childFragmentManager.executePendingTransactions()
}
override fun onMediaClicked(position: Int) {
if (binding != null) {
binding!!.exploreContainer.visibility = View.VISIBLE
}
if ((parentFragment as ExploreFragment).binding != null) {
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility =
View.GONE
}
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
(parentFragment as ExploreFragment).setScroll(false)
setFragment(mediaDetails!!, listFragment)
mediaDetails!!.showImage(position)
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
override fun getMediaAtPosition(i: Int): Media? = listFragment?.getMediaAtPosition(i)
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
override fun getTotalMediaCount(): Int = listFragment?.getTotalMediaCount() ?: 0
override fun getContributionStateAt(position: Int): Int? = null
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (mediaDetails != null && !listFragment!!.isVisible) {
removeFragment(mediaDetails!!)
onMediaClicked(index)
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
mediaDetails?.notifyDataSetChanged()
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
fun backPressed(): Boolean {
if (null != mediaDetails && mediaDetails!!.isVisible) {
if ((parentFragment as ExploreFragment).binding != null) {
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility =
View.VISIBLE
}
removeFragment(mediaDetails!!)
(parentFragment as ExploreFragment).setScroll(true)
setFragment(listFragment!!, mediaDetails)
(activity as MainActivity).showTabs()
return true
} else {
if ((activity as MainActivity?) != null) {
(activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code())
}
}
if ((activity as MainActivity?) != null) {
(activity as MainActivity).showTabs()
}
return false
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -1,239 +0,0 @@
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.map.ExploreMapFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailProvider;
import fr.free.nrw.commons.navtab.NavTab;
public class ExploreMapRootFragment extends CommonsDaggerSupportFragment implements
MediaDetailProvider, CategoryImagesCallback {
private MediaDetailPagerFragment mediaDetails;
private ExploreMapFragment mapFragment;
private FragmentFeaturedRootBinding binding;
public ExploreMapRootFragment() {
//empty constructor necessary otherwise crashes on recreate
}
@NonNull
public static ExploreMapRootFragment newInstance() {
ExploreMapRootFragment fragment = new ExploreMapRootFragment();
fragment.setRetainInstance(true);
return fragment;
}
public ExploreMapRootFragment(Bundle bundle) {
// get fragment arguments
String title = bundle.getString("categoryName");
double zoom = bundle.getDouble("prev_zoom");
double latitude = bundle.getDouble("prev_latitude");
double longitude = bundle.getDouble("prev_longitude");
mapFragment = new ExploreMapFragment();
Bundle featuredArguments = new Bundle();
featuredArguments.putString("categoryName", title);
// if we came from 'Show in Explore' in Nearby, pass on zoom and center
if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) {
featuredArguments.putDouble("prev_zoom", zoom);
featuredArguments.putDouble("prev_latitude", latitude);
featuredArguments.putDouble("prev_longitude", longitude);
}
mapFragment.setArguments(featuredArguments);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false);
return binding.getRoot();
}
@Override
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
if (savedInstanceState == null) {
setFragment(mapFragment, mediaDetails);
}
}
public void setFragment(Fragment fragment, Fragment otherFragment) {
if (fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (fragment.isAdded() && otherFragment == null) {
getChildFragmentManager()
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded() && otherFragment != null) {
getChildFragmentManager()
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
} else if (!fragment.isAdded()) {
getChildFragmentManager()
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit();
getChildFragmentManager().executePendingTransactions();
}
}
public void removeFragment(Fragment fragment) {
getChildFragmentManager()
.beginTransaction()
.remove(fragment)
.commit();
getChildFragmentManager().executePendingTransactions();
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
}
@Override
public void onMediaClicked(int position) {
binding.exploreContainer.setVisibility(View.VISIBLE);
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.GONE);
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
((ExploreFragment) getParentFragment()).setScroll(false);
setFragment(mediaDetails, mapFragment);
mediaDetails.showImage(position);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
if (mapFragment != null && mapFragment.mediaList != null) {
return mapFragment.mediaList.get(i);
} else {
return null;
}
}
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
if (mapFragment != null && mapFragment.mediaList != null) {
return mapFragment.mediaList.size();
} else {
return 0;
}
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (mediaDetails != null && !mapFragment.isVisible()) {
removeFragment(mediaDetails);
onMediaClicked(index);
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails != null) {
mediaDetails.notifyDataSetChanged();
}
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
public boolean backPressed() {
if (null != mediaDetails && mediaDetails.isVisible()) {
((ExploreFragment) getParentFragment()).binding.tabLayout.setVisibility(View.VISIBLE);
removeFragment(mediaDetails);
((ExploreFragment) getParentFragment()).setScroll(true);
setFragment(mapFragment, mediaDetails);
((MainActivity) getActivity()).showTabs();
return true;
}
if (mapFragment != null && mapFragment.isVisible()) {
if (mapFragment.backButtonClicked()) {
// Explore map fragment handled the event no further action required.
return true;
} else {
((MainActivity) getActivity()).showTabs();
return false;
}
} else {
((MainActivity) getActivity()).setSelectedItemId(NavTab.CONTRIBUTIONS.code());
}
((MainActivity) getActivity()).showTabs();
return false;
}
public void loadNearbyMapFromExplore() {
mapFragment.loadNearbyMapFromExplore();
}
@Override
public void onDestroy() {
super.onDestroy();
binding = null;
}
}

View file

@ -0,0 +1,201 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.contributions.MainActivity
import fr.free.nrw.commons.databinding.FragmentFeaturedRootBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.explore.map.ExploreMapFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.navtab.NavTab
class ExploreMapRootFragment : CommonsDaggerSupportFragment, MediaDetailProvider,
CategoryImagesCallback {
private var mediaDetails: MediaDetailPagerFragment? = null
private var mapFragment: ExploreMapFragment? = null
private var binding: FragmentFeaturedRootBinding? = null
constructor()
constructor(bundle: Bundle) {
// get fragment arguments
val title = bundle.getString("categoryName")
val zoom = bundle.getDouble("prev_zoom")
val latitude = bundle.getDouble("prev_latitude")
val longitude = bundle.getDouble("prev_longitude")
mapFragment = ExploreMapFragment()
val featuredArguments = bundleOf(
"categoryName" to title
)
// if we came from 'Show in Explore' in Nearby, pass on zoom and center
if (zoom != 0.0 || latitude != 0.0 || longitude != 0.0) {
featuredArguments.putDouble("prev_zoom", zoom)
featuredArguments.putDouble("prev_latitude", latitude)
featuredArguments.putDouble("prev_longitude", longitude)
}
mapFragment!!.arguments = featuredArguments
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
super.onCreate(savedInstanceState)
binding = FragmentFeaturedRootBinding.inflate(inflater, container, false)
return binding!!.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (savedInstanceState == null) {
setFragment(mapFragment!!, mediaDetails)
}
}
fun setFragment(fragment: Fragment, otherFragment: Fragment?) {
if (fragment.isAdded && otherFragment != null) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (fragment.isAdded && otherFragment == null) {
childFragmentManager
.beginTransaction()
.show(fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded && otherFragment != null) {
childFragmentManager
.beginTransaction()
.hide(otherFragment)
.add(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
} else if (!fragment.isAdded) {
childFragmentManager
.beginTransaction()
.replace(R.id.explore_container, fragment)
.addToBackStack("CONTRIBUTION_LIST_FRAGMENT_TAG")
.commit()
childFragmentManager.executePendingTransactions()
}
}
private fun removeFragment(fragment: Fragment) {
childFragmentManager
.beginTransaction()
.remove(fragment)
.commit()
childFragmentManager.executePendingTransactions()
}
override fun onMediaClicked(position: Int) {
binding!!.exploreContainer.visibility = View.VISIBLE
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.GONE
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
(parentFragment as ExploreFragment).setScroll(false)
setFragment(mediaDetails!!, mapFragment)
mediaDetails!!.showImage(position)
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
*
* @param i It is the index of which media object is to be returned which is same as current
* index of viewPager.
* @return Media Object
*/
override fun getMediaAtPosition(i: Int): Media? = mapFragment?.mediaList?.get(i)
/**
* This method is called on from getCount of MediaDetailPagerFragment The viewpager will contain
* same number of media items as that of media elements in adapter.
*
* @return Total Media count in the adapter
*/
override fun getTotalMediaCount(): Int = mapFragment?.mediaList?.size ?: 0
override fun getContributionStateAt(position: Int): Int? = null
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (mediaDetails != null && !mapFragment!!.isVisible) {
removeFragment(mediaDetails!!)
onMediaClicked(index)
}
}
/**
* This method is called on success of API call for featured images or mobile uploads. The
* viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
mediaDetails?.notifyDataSetChanged()
}
/**
* Performs back pressed action on the fragment. Return true if the event was handled by the
* mediaDetails otherwise returns false.
*
* @return
*/
fun backPressed(): Boolean {
if (null != mediaDetails && mediaDetails!!.isVisible) {
(parentFragment as ExploreFragment).binding!!.tabLayout.visibility = View.VISIBLE
removeFragment(mediaDetails!!)
(parentFragment as ExploreFragment).setScroll(true)
setFragment(mapFragment!!, mediaDetails)
(activity as MainActivity).showTabs()
return true
}
if (mapFragment != null && mapFragment!!.isVisible) {
if (mapFragment!!.backButtonClicked()) {
// Explore map fragment handled the event no further action required.
return true
} else {
(activity as MainActivity).showTabs()
return false
}
} else {
(activity as MainActivity).setSelectedItemId(NavTab.CONTRIBUTIONS.code())
}
(activity as MainActivity).showTabs()
return false
}
fun loadNearbyMapFromExplore() = mapFragment?.loadNearbyMapFromExplore()
override fun onDestroy() {
super.onDestroy()
binding = null
}
companion object {
fun newInstance(): ExploreMapRootFragment = ExploreMapRootFragment().apply {
retainInstance = true
}
}
}

View file

@ -1,66 +0,0 @@
package fr.free.nrw.commons.explore;
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import androidx.viewpager.widget.ViewPager;
/**
* ParentViewPager A custom viewPager whose scrolling can be enabled and disabled.
*/
public class ParentViewPager extends ViewPager {
/**
* Boolean variable that stores the current state of pager scroll i.e(enabled or disabled)
*/
private boolean canScroll = true;
/**
* Default constructors
*/
public ParentViewPager(Context context) {
super(context);
}
public ParentViewPager(Context context, AttributeSet attrs) {
super(context, attrs);
}
/**
* Setter method for canScroll.
*/
public void setCanScroll(boolean canScroll) {
this.canScroll = canScroll;
}
/**
* Getter method for canScroll.
*/
public boolean isCanScroll() {
return canScroll;
}
/**
* Method that prevents scrolling if canScroll is set to false.
*/
@Override
public boolean onTouchEvent(MotionEvent ev) {
return canScroll && super.onTouchEvent(ev);
}
/**
* A facilitator method that allows parent to intercept touch events before its children. thus
* making it possible to prevent swiping parent on child end.
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
return canScroll && super.onInterceptTouchEvent(ev);
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.explore
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.viewpager.widget.ViewPager
/**
* ParentViewPager A custom viewPager whose scrolling can be enabled and disabled.
*/
class ParentViewPager : ViewPager {
var canScroll: Boolean = true
constructor(context: Context) : super(context)
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
override fun onTouchEvent(ev: MotionEvent): Boolean {
return canScroll && super.onTouchEvent(ev)
}
override fun onInterceptTouchEvent(ev: MotionEvent): Boolean {
return canScroll && super.onInterceptTouchEvent(ev)
}
}

View file

@ -1,285 +0,0 @@
package fr.free.nrw.commons.explore;
import static fr.free.nrw.commons.ViewPagerAdapter.pairOf;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
import com.jakewharton.rxbinding2.view.RxView;
import com.jakewharton.rxbinding2.widget.RxSearchView;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.databinding.ActivitySearchBinding;
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment;
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment;
import fr.free.nrw.commons.explore.media.SearchMediaFragment;
import fr.free.nrw.commons.explore.models.RecentSearch;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailProvider;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.FragmentUtils;
import fr.free.nrw.commons.utils.ViewUtil;
import io.reactivex.android.schedulers.AndroidSchedulers;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import timber.log.Timber;
/**
* Represents search screen of this app
*/
public class SearchActivity extends BaseActivity
implements MediaDetailProvider, CategoryImagesCallback {
@Inject
RecentSearchesDao recentSearchesDao;
private SearchMediaFragment searchMediaFragment;
private SearchCategoryFragment searchCategoryFragment;
private SearchDepictionsFragment searchDepictionsFragment;
private RecentSearchesFragment recentSearchesFragment;
private FragmentManager supportFragmentManager;
private MediaDetailPagerFragment mediaDetails;
ViewPagerAdapter viewPagerAdapter;
private ActivitySearchBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivitySearchBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setTitle(getString(R.string.title_activity_search));
setSupportActionBar(binding.toolbarSearch);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
binding.toolbarSearch.setNavigationOnClickListener(v->onBackPressed());
supportFragmentManager = getSupportFragmentManager();
setSearchHistoryFragment();
viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
binding.tabLayout.setupWithViewPager(binding.viewPager);
setTabs();
binding.searchBox.setQueryHint(getString(R.string.search_commons));
binding.searchBox.onActionViewExpanded();
binding.searchBox.clearFocus();
}
/**
* This method sets the search history fragment.
* Search history fragment is displayed when query is empty.
*/
private void setSearchHistoryFragment() {
recentSearchesFragment = new RecentSearchesFragment();
FragmentTransaction transaction = supportFragmentManager.beginTransaction();
transaction.add(R.id.searchHistoryContainer, recentSearchesFragment).commit();
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
public void setTabs() {
searchMediaFragment = new SearchMediaFragment();
searchDepictionsFragment = new SearchDepictionsFragment();
searchCategoryFragment= new SearchCategoryFragment();
viewPagerAdapter.setTabs(
pairOf(R.string.search_tab_title_media, searchMediaFragment),
pairOf(R.string.search_tab_title_categories, searchCategoryFragment),
pairOf(R.string.search_tab_title_depictions, searchDepictionsFragment)
);
viewPagerAdapter.notifyDataSetChanged();
getCompositeDisposable().add(RxSearchView.queryTextChanges(binding.searchBox)
.takeUntil(RxView.detaches(binding.searchBox))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::handleSearch, Timber::e
));
}
private void handleSearch(final CharSequence query) {
if (!TextUtils.isEmpty(query)) {
saveRecentSearch(query.toString());
binding.viewPager.setVisibility(View.VISIBLE);
binding.tabLayout.setVisibility(View.VISIBLE);
binding.searchHistoryContainer.setVisibility(View.GONE);
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
searchDepictionsFragment.onQueryUpdated(query.toString());
}
if (FragmentUtils.isFragmentUIActive(searchMediaFragment)) {
searchMediaFragment.onQueryUpdated(query.toString());
}
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
searchCategoryFragment.onQueryUpdated(query.toString());
}
}
else {
//Open RecentSearchesFragment
recentSearchesFragment.updateRecentSearches();
binding.viewPager.setVisibility(View.GONE);
binding.tabLayout.setVisibility(View.GONE);
setSearchHistoryFragment();
binding.searchHistoryContainer.setVisibility(View.VISIBLE);
}
}
private void saveRecentSearch(@NonNull final String query) {
final RecentSearch recentSearch = recentSearchesDao.find(query);
// Newly searched query...
if (recentSearch == null) {
recentSearchesDao.save(new RecentSearch(null, query, new Date()));
} else {
recentSearch.setLastSearched(new Date());
recentSearchesDao.save(recentSearch);
}
}
/**
* returns Media Object at position
* @param i position of Media in the imagesRecyclerView adapter.
*/
@Override
public Media getMediaAtPosition(int i) {
return searchMediaFragment.getMediaAtPosition(i);
}
/**
* returns total number of images present in the imagesRecyclerView adapter.
*/
@Override
public int getTotalMediaCount() {
return searchMediaFragment.getTotalMediaCount();
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
onBackPressed();
onMediaClicked(index);
}
}
/**
* This method is called on success of API call for image Search.
* The viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetails!=null){
mediaDetails.notifyDataSetChanged();
}
}
/**
* Open media detail pager fragment on click of image in search results
* @param index item index that should be opened
*/
@Override
public void onMediaClicked(int index) {
ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox));
binding.tabLayout.setVisibility(View.GONE);
binding.viewPager.setVisibility(View.GONE);
binding.mediaContainer.setVisibility(View.VISIBLE);
binding.searchBox.setVisibility(View.GONE);// to remove searchview when mediaDetails fragment open
if (mediaDetails == null || !mediaDetails.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = MediaDetailPagerFragment.newInstance(false, true);
supportFragmentManager
.beginTransaction()
.hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount()))
.add(R.id.mediaContainer, mediaDetails)
.addToBackStack(null)
.commit();
// Reason for using hide, add instead of replace is to maintain scroll position after
// coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631
// https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550
supportFragmentManager.executePendingTransactions();
}
mediaDetails.showImage(index);
}
/**
* This method is called on Screen Rotation
*/
@Override
protected void onResume() {
if (supportFragmentManager.getBackStackEntryCount()==1){
//FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time.
//FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894
// This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing.
//
onBackPressed();
}
super.onResume();
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
//Remove the backstack entry that gets added when share button is clicked
//fixing:https://github.com/commons-app/apps-android-commons/issues/2296
if (getSupportFragmentManager().getBackStackEntryCount() == 2) {
supportFragmentManager
.beginTransaction()
.remove(mediaDetails)
.commit();
supportFragmentManager.popBackStack();
supportFragmentManager.executePendingTransactions();
}
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
// back to search so show search toolbar and hide navigation toolbar
binding.searchBox.setVisibility(View.VISIBLE);//set the searchview
binding.tabLayout.setVisibility(View.VISIBLE);
binding.viewPager.setVisibility(View.VISIBLE);
binding.mediaContainer.setVisibility(View.GONE);
} else {
binding.toolbarSearch.setVisibility(View.GONE);
}
super.onBackPressed();
}
/**
* This method is called on click of a recent search to update query in SearchView.
* @param query Recent Search Query
*/
public void updateText(String query) {
binding.searchBox.setQuery(query, true);
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
binding.viewPager.requestFocus();
}
@Override protected void onDestroy() {
super.onDestroy();
//Dispose the disposables when the activity is destroyed
getCompositeDisposable().dispose();
binding = null;
}
}

View file

@ -0,0 +1,252 @@
package fr.free.nrw.commons.explore
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import androidx.fragment.app.FragmentManager
import com.jakewharton.rxbinding2.view.RxView
import com.jakewharton.rxbinding2.widget.RxSearchView
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.databinding.ActivitySearchBinding
import fr.free.nrw.commons.explore.categories.search.SearchCategoryFragment
import fr.free.nrw.commons.explore.depictions.search.SearchDepictionsFragment
import fr.free.nrw.commons.explore.media.SearchMediaFragment
import fr.free.nrw.commons.explore.models.RecentSearch
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.utils.FragmentUtils.isFragmentUIActive
import fr.free.nrw.commons.utils.ViewUtil.hideKeyboard
import io.reactivex.android.schedulers.AndroidSchedulers
import timber.log.Timber
import java.util.Date
import java.util.concurrent.TimeUnit
import javax.inject.Inject
/**
* Represents search screen of this app
*/
class SearchActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback {
@JvmField
@Inject
var recentSearchesDao: RecentSearchesDao? = null
private var searchMediaFragment: SearchMediaFragment? = null
private var searchCategoryFragment: SearchCategoryFragment? = null
private var searchDepictionsFragment: SearchDepictionsFragment? = null
private var recentSearchesFragment: RecentSearchesFragment? = null
private var supportFragmentManager: FragmentManager? = null
private var mediaDetails: MediaDetailPagerFragment? = null
private var viewPagerAdapter: ViewPagerAdapter? = null
private var binding: ActivitySearchBinding? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivitySearchBinding.inflate(layoutInflater)
setContentView(binding!!.root)
title = getString(R.string.title_activity_search)
setSupportActionBar(binding!!.toolbarSearch)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
binding!!.toolbarSearch.setNavigationOnClickListener { onBackPressed() }
supportFragmentManager = getSupportFragmentManager()
setSearchHistoryFragment()
viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager())
binding!!.viewPager.adapter = viewPagerAdapter
binding!!.viewPager.offscreenPageLimit = 2 // Because we want all the fragments to be alive
binding!!.tabLayout.setupWithViewPager(binding!!.viewPager)
setTabs()
binding!!.searchBox.queryHint = getString(R.string.search_commons)
binding!!.searchBox.onActionViewExpanded()
binding!!.searchBox.clearFocus()
}
/**
* This method sets the search history fragment.
* Search history fragment is displayed when query is empty.
*/
private fun setSearchHistoryFragment() {
recentSearchesFragment = RecentSearchesFragment()
val transaction = supportFragmentManager!!.beginTransaction()
transaction.add(R.id.searchHistoryContainer, recentSearchesFragment!!).commit()
}
/**
* Sets the titles in the tabLayout and fragments in the viewPager
*/
fun setTabs() {
searchMediaFragment = SearchMediaFragment()
searchDepictionsFragment = SearchDepictionsFragment()
searchCategoryFragment = SearchCategoryFragment()
viewPagerAdapter!!.setTabs(
R.string.search_tab_title_media to searchMediaFragment!!,
R.string.search_tab_title_categories to searchCategoryFragment!!,
R.string.search_tab_title_depictions to searchDepictionsFragment!!
)
viewPagerAdapter!!.notifyDataSetChanged()
compositeDisposable.add(
RxSearchView.queryTextChanges(binding!!.searchBox)
.takeUntil(RxView.detaches(binding!!.searchBox))
.debounce(500, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(::handleSearch, Timber::e)
)
}
private fun handleSearch(query: CharSequence) {
if (!TextUtils.isEmpty(query)) {
saveRecentSearch(query.toString())
binding!!.viewPager.visibility = View.VISIBLE
binding!!.tabLayout.visibility = View.VISIBLE
binding!!.searchHistoryContainer.visibility = View.GONE
if (isFragmentUIActive(searchDepictionsFragment)) {
searchDepictionsFragment!!.onQueryUpdated(query.toString())
}
if (isFragmentUIActive(searchMediaFragment)) {
searchMediaFragment!!.onQueryUpdated(query.toString())
}
if (isFragmentUIActive(searchCategoryFragment)) {
searchCategoryFragment!!.onQueryUpdated(query.toString())
}
} else {
//Open RecentSearchesFragment
recentSearchesFragment!!.updateRecentSearches()
binding!!.viewPager.visibility = View.GONE
binding!!.tabLayout.visibility = View.GONE
setSearchHistoryFragment()
binding!!.searchHistoryContainer.visibility = View.VISIBLE
}
}
private fun saveRecentSearch(query: String) {
val recentSearch = recentSearchesDao!!.find(query)
// Newly searched query...
if (recentSearch == null) {
recentSearchesDao!!.save(RecentSearch(null, query, Date()))
} else {
recentSearch.lastSearched = Date()
recentSearchesDao!!.save(recentSearch)
}
}
override fun getMediaAtPosition(i: Int): Media? = searchMediaFragment!!.getMediaAtPosition(i)
override fun getTotalMediaCount(): Int = searchMediaFragment!!.getTotalMediaCount()
override fun getContributionStateAt(position: Int): Int? = null
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (getSupportFragmentManager().backStackEntryCount == 1) {
onBackPressed()
onMediaClicked(index)
}
}
/**
* This method is called on success of API call for image Search.
* The viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
mediaDetails?.notifyDataSetChanged()
}
/**
* Open media detail pager fragment on click of image in search results
* @param position item index that should be opened
*/
override fun onMediaClicked(position: Int) {
hideKeyboard(findViewById(R.id.searchBox))
binding!!.tabLayout.visibility = View.GONE
binding!!.viewPager.visibility = View.GONE
binding!!.mediaContainer.visibility = View.VISIBLE
binding!!.searchBox.visibility =
View.GONE // to remove searchview when mediaDetails fragment open
if (mediaDetails == null || !mediaDetails!!.isVisible) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetails = MediaDetailPagerFragment.newInstance(false, true)
supportFragmentManager!!
.beginTransaction()
.hide(supportFragmentManager!!.fragments[supportFragmentManager!!.backStackEntryCount])
.add(R.id.mediaContainer, mediaDetails!!)
.addToBackStack(null)
.commit()
// Reason for using hide, add instead of replace is to maintain scroll position after
// coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631
// https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550
supportFragmentManager!!.executePendingTransactions()
}
mediaDetails!!.showImage(position)
}
/**
* This method is called on Screen Rotation
*/
override fun onResume() {
if (supportFragmentManager!!.backStackEntryCount == 1) {
//FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time.
//FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894
// This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing.
onBackPressed()
}
super.onResume()
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
override fun onBackPressed() {
//Remove the backstack entry that gets added when share button is clicked
//fixing:https://github.com/commons-app/apps-android-commons/issues/2296
if (getSupportFragmentManager().backStackEntryCount == 2) {
supportFragmentManager!!
.beginTransaction()
.remove(mediaDetails!!)
.commit()
supportFragmentManager!!.popBackStack()
supportFragmentManager!!.executePendingTransactions()
}
if (getSupportFragmentManager().backStackEntryCount == 1) {
// back to search so show search toolbar and hide navigation toolbar
binding!!.searchBox.visibility = View.VISIBLE //set the searchview
binding!!.tabLayout.visibility = View.VISIBLE
binding!!.viewPager.visibility = View.VISIBLE
binding!!.mediaContainer.visibility = View.GONE
} else {
binding!!.toolbarSearch.visibility = View.GONE
}
super.onBackPressed()
}
/**
* This method is called on click of a recent search to update query in SearchView.
* @param query Recent Search Query
*/
fun updateText(query: String?) {
binding!!.searchBox.setQuery(query, true)
// Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details.
// https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511
binding!!.viewPager.requestFocus()
}
override fun onDestroy() {
super.onDestroy()
//Dispose the disposables when the activity is destroyed
compositeDisposable.dispose()
binding = null
}
}

View file

@ -7,6 +7,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() {
override val errorTextId: Int = R.string.error_loading_depictions
override val pagedListAdapter by lazy {
DepictionAdapter { WikidataItemDetailsActivity.startYourself(context, it) }
DepictionAdapter { WikidataItemDetailsActivity.startYourself(requireContext(), it) }
}
}

View file

@ -1,302 +0,0 @@
package fr.free.nrw.commons.explore.depictions;
import static fr.free.nrw.commons.ViewPagerAdapter.pairOf;
import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import androidx.fragment.app.FragmentManager;
import com.google.android.material.snackbar.Snackbar;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao;
import fr.free.nrw.commons.category.CategoryImagesCallback;
import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding;
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment;
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment;
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment;
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
import fr.free.nrw.commons.media.MediaDetailProvider;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
import fr.free.nrw.commons.wikidata.WikidataConstants;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.CompositeDisposable;
import io.reactivex.schedulers.Schedulers;
import javax.inject.Inject;
/**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
*/
public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailProvider,
CategoryImagesCallback {
private FragmentManager supportFragmentManager;
private DepictedImagesFragment depictionImagesListFragment;
private MediaDetailPagerFragment mediaDetailPagerFragment;
/**
* Name of the depicted item
* Ex: Rabbit
*/
@Inject BookmarkItemsDao bookmarkItemsDao;
private CompositeDisposable compositeDisposable;
@Inject
DepictModel depictModel;
private String wikidataItemName;
private ActivityWikidataItemDetailsBinding binding;
ViewPagerAdapter viewPagerAdapter;
private DepictedItem wikidataItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityWikidataItemDetailsBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
compositeDisposable = new CompositeDisposable();
supportFragmentManager = getSupportFragmentManager();
viewPagerAdapter = new ViewPagerAdapter(this, getSupportFragmentManager());
binding.viewPager.setAdapter(viewPagerAdapter);
binding.viewPager.setOffscreenPageLimit(2);
binding.tabLayout.setupWithViewPager(binding.viewPager);
final DepictedItem depictedItem = getIntent().getParcelableExtra(
WikidataConstants.BOOKMARKS_ITEMS);
wikidataItem = depictedItem;
setSupportActionBar(binding.toolbarBinding.toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setTabs();
setPageTitle();
}
/**
* Gets the passed wikidataItemName from the intents and displays it as the page title
*/
private void setPageTitle() {
if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) {
setTitle(getIntent().getStringExtra("wikidataItemName"));
}
}
/**
* This method is called on success of API call for featured Images.
* The viewpager will notified that number of items have changed.
*/
@Override
public void viewPagerNotifyDataSetChanged() {
if (mediaDetailPagerFragment !=null){
mediaDetailPagerFragment.notifyDataSetChanged();
}
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private void setTabs() {
depictionImagesListFragment = new DepictedImagesFragment();
ChildDepictionsFragment childDepictionsFragment = new ChildDepictionsFragment();
ParentDepictionsFragment parentDepictionsFragment = new ParentDepictionsFragment();
wikidataItemName = getIntent().getStringExtra("wikidataItemName");
String entityId = getIntent().getStringExtra("entityId");
if (getIntent() != null && wikidataItemName != null) {
Bundle arguments = new Bundle();
arguments.putString("wikidataItemName", wikidataItemName);
arguments.putString("entityId", entityId);
depictionImagesListFragment.setArguments(arguments);
parentDepictionsFragment.setArguments(arguments);
childDepictionsFragment.setArguments(arguments);
}
viewPagerAdapter.setTabs(
pairOf(R.string.title_for_media, depictionImagesListFragment),
pairOf(R.string.title_for_subcategories, childDepictionsFragment),
pairOf(R.string.title_for_parent_categories, parentDepictionsFragment)
);
binding.viewPager.setOffscreenPageLimit(2);
viewPagerAdapter.notifyDataSetChanged();
}
/**
* Shows media detail fragment when user clicks on any image in the list
*/
@Override
public void onMediaClicked(int position) {
binding.tabLayout.setVisibility(View.GONE);
binding.viewPager.setVisibility(View.GONE);
binding.mediaContainer.setVisibility(View.VISIBLE);
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment.isVisible()) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true);
FragmentManager supportFragmentManager = getSupportFragmentManager();
supportFragmentManager
.beginTransaction()
.replace(R.id.mediaContainer, mediaDetailPagerFragment)
.addToBackStack(null)
.commit();
supportFragmentManager.executePendingTransactions();
}
mediaDetailPagerFragment.showImage(position);
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* @param i It is the index of which media object is to be returned which is same as
* current index of viewPager.
* @return Media Object
*/
@Override
public Media getMediaAtPosition(int i) {
return depictionImagesListFragment.getMediaAtPosition(i);
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
@Override
public void onBackPressed() {
if (supportFragmentManager.getBackStackEntryCount() == 1){
binding.tabLayout.setVisibility(View.VISIBLE);
binding.viewPager.setVisibility(View.VISIBLE);
binding.mediaContainer.setVisibility(View.GONE);
}
super.onBackPressed();
}
/**
* This method is called on from getCount of MediaDetailPagerFragment
* The viewpager will contain same number of media items as that of media elements in adapter.
* @return Total Media count in the adapter
*/
@Override
public int getTotalMediaCount() {
return depictionImagesListFragment.getTotalMediaCount();
}
@Override
public Integer getContributionStateAt(int position) {
return null;
}
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
@Override
public void refreshNominatedMedia(int index) {
if (getSupportFragmentManager().getBackStackEntryCount() == 1) {
onBackPressed();
onMediaClicked(index);
}
}
/**
* Consumers should be simply using this method to use this activity.
*
* @param context A Context of the application package implementing this class.
* @param depictedItem Name of the depicts for displaying its details
*/
public static void startYourself(Context context, DepictedItem depictedItem) {
Intent intent = new Intent(context, WikidataItemDetailsActivity.class);
intent.putExtra("wikidataItemName", depictedItem.getName());
intent.putExtra("entityId", depictedItem.getId());
intent.putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem);
context.startActivity(intent);
}
/**
* This function inflates the menu
*/
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater=getMenuInflater();
menuInflater.inflate(R.menu.menu_wikidata_item,menu);
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item));
return super.onCreateOptionsMenu(menu);
}
/**
* This method handles the logic on item select in toolbar menu
* Currently only 1 choice is available to open Wikidata item details page in browser
*/
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()){
case R.id.browser_actions_menu_items:
String entityId=getIntent().getStringExtra("entityId");
Uri uri = Uri.parse("https://www.wikidata.org/wiki/" + entityId);
handleWebUrl(this, uri);
return true;
case R.id.menu_bookmark_current_item:
if(getIntent().getStringExtra("fragment") != null) {
compositeDisposable.add(depictModel.getDepictions(
getIntent().getStringExtra("entityId")
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(depictedItems -> {
final boolean bookmarkExists = bookmarkItemsDao.updateBookmarkItem(
depictedItems.get(0));
final Snackbar snackbar
= bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG)
: Snackbar.make(findViewById(R.id.toolbar_layout),
R.string.remove_bookmark,
Snackbar.LENGTH_LONG);
snackbar.show();
updateBookmarkState(item);
}));
} else {
final boolean bookmarkExists
= bookmarkItemsDao.updateBookmarkItem(wikidataItem);
final Snackbar snackbar
= bookmarkExists ? Snackbar.make(findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG)
: Snackbar.make(findViewById(R.id.toolbar_layout), R.string.remove_bookmark,
Snackbar.LENGTH_LONG);
snackbar.show();
updateBookmarkState(item);
}
return true;
case android.R.id.home:
onBackPressed();
return true;
default:
return super.onOptionsItemSelected(item);
}
}
private void updateBookmarkState(final MenuItem item) {
final boolean isBookmarked;
if(getIntent().getStringExtra("fragment") != null) {
isBookmarked
= bookmarkItemsDao.findBookmarkItem(getIntent().getStringExtra("entityId"));
} else {
isBookmarked = bookmarkItemsDao.findBookmarkItem(wikidataItem.getId());
}
final int icon
= isBookmarked ? R.drawable.menu_ic_round_star_filled_24px
: R.drawable.menu_ic_round_star_border_24px;
item.setIcon(icon);
}
}

View file

@ -0,0 +1,295 @@
package fr.free.nrw.commons.explore.depictions
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import com.google.android.material.snackbar.Snackbar
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.ViewPagerAdapter
import fr.free.nrw.commons.bookmarks.items.BookmarkItemsDao
import fr.free.nrw.commons.category.CategoryImagesCallback
import fr.free.nrw.commons.databinding.ActivityWikidataItemDetailsBinding
import fr.free.nrw.commons.explore.depictions.child.ChildDepictionsFragment
import fr.free.nrw.commons.explore.depictions.media.DepictedImagesFragment
import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsFragment
import fr.free.nrw.commons.media.MediaDetailPagerFragment
import fr.free.nrw.commons.media.MediaDetailProvider
import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.structure.depictions.DepictModel
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
import fr.free.nrw.commons.utils.handleWebUrl
import fr.free.nrw.commons.wikidata.WikidataConstants
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.functions.Consumer
import io.reactivex.schedulers.Schedulers
import javax.inject.Inject
/**
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
*/
class WikidataItemDetailsActivity : BaseActivity(), MediaDetailProvider, CategoryImagesCallback {
@JvmField
@Inject
var bookmarkItemsDao: BookmarkItemsDao? = null
@JvmField
@Inject
var depictModel: DepictModel? = null
private var supportFragmentManager: FragmentManager? = null
private var depictionImagesListFragment: DepictedImagesFragment? = null
private var mediaDetailPagerFragment: MediaDetailPagerFragment? = null
private var binding: ActivityWikidataItemDetailsBinding? = null
var viewPagerAdapter: ViewPagerAdapter? = null
private var wikidataItem: DepictedItem? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityWikidataItemDetailsBinding.inflate(layoutInflater)
setContentView(binding!!.root)
supportFragmentManager = getSupportFragmentManager()
viewPagerAdapter = ViewPagerAdapter(this, getSupportFragmentManager())
binding!!.viewPager.adapter = viewPagerAdapter
binding!!.viewPager.offscreenPageLimit = 2
binding!!.tabLayout.setupWithViewPager(binding!!.viewPager)
wikidataItem = intent.getParcelableExtra(WikidataConstants.BOOKMARKS_ITEMS)
setSupportActionBar(binding!!.toolbarBinding.toolbar)
supportActionBar!!.setDisplayHomeAsUpEnabled(true)
setTabs()
setPageTitle()
}
/**
* Gets the passed wikidataItemName from the intents and displays it as the page title
*/
private fun setPageTitle() {
if (intent != null && intent.getStringExtra("wikidataItemName") != null) {
title = intent.getStringExtra("wikidataItemName")
}
}
/**
* This method is called on success of API call for featured Images.
* The viewpager will notified that number of items have changed.
*/
override fun viewPagerNotifyDataSetChanged() {
if (mediaDetailPagerFragment != null) {
mediaDetailPagerFragment!!.notifyDataSetChanged()
}
}
/**
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
* Set the fragments according to the tab selected in the viewPager.
*/
private fun setTabs() {
depictionImagesListFragment = DepictedImagesFragment()
val childDepictionsFragment = ChildDepictionsFragment()
val parentDepictionsFragment = ParentDepictionsFragment()
val wikidataItemName = intent.getStringExtra("wikidataItemName")
val entityId = intent.getStringExtra("entityId")
if (intent != null && wikidataItemName != null) {
val arguments = bundleOf(
"wikidataItemName" to wikidataItemName,
"entityId" to entityId
)
depictionImagesListFragment!!.arguments = arguments
parentDepictionsFragment.arguments = arguments
childDepictionsFragment.arguments = arguments
}
viewPagerAdapter!!.setTabs(
R.string.title_for_media to depictionImagesListFragment!!,
R.string.title_for_subcategories to childDepictionsFragment,
R.string.title_for_parent_categories to parentDepictionsFragment
)
binding!!.viewPager.offscreenPageLimit = 2
viewPagerAdapter!!.notifyDataSetChanged()
}
/**
* Shows media detail fragment when user clicks on any image in the list
*/
override fun onMediaClicked(position: Int) {
binding!!.tabLayout.visibility = View.GONE
binding!!.viewPager.visibility = View.GONE
binding!!.mediaContainer.visibility = View.VISIBLE
if (mediaDetailPagerFragment == null || !mediaDetailPagerFragment!!.isVisible) {
// set isFeaturedImage true for featured images, to include author field on media detail
mediaDetailPagerFragment = MediaDetailPagerFragment.newInstance(false, true)
val supportFragmentManager = getSupportFragmentManager()
supportFragmentManager
.beginTransaction()
.replace(R.id.mediaContainer, mediaDetailPagerFragment!!)
.addToBackStack(null)
.commit()
supportFragmentManager.executePendingTransactions()
}
mediaDetailPagerFragment!!.showImage(position)
}
/**
* This method is called mediaDetailPagerFragment. It returns the Media Object at that Index
* @param i It is the index of which media object is to be returned which is same as
* current index of viewPager.
* @return Media Object
*/
override fun getMediaAtPosition(i: Int): Media? {
return depictionImagesListFragment!!.getMediaAtPosition(i)
}
/**
* This method is called on backPressed of anyFragment in the activity.
* If condition is called when mediaDetailFragment is opened.
*/
override fun onBackPressed() {
if (supportFragmentManager!!.backStackEntryCount == 1) {
binding!!.tabLayout.visibility = View.VISIBLE
binding!!.viewPager.visibility = View.VISIBLE
binding!!.mediaContainer.visibility = View.GONE
}
super.onBackPressed()
}
/**
* This method is called on from getCount of MediaDetailPagerFragment
* The viewpager will contain same number of media items as that of media elements in adapter.
* @return Total Media count in the adapter
*/
override fun getTotalMediaCount(): Int = depictionImagesListFragment!!.getTotalMediaCount()
override fun getContributionStateAt(position: Int): Int? = null
/**
* Reload media detail fragment once media is nominated
*
* @param index item position that has been nominated
*/
override fun refreshNominatedMedia(index: Int) {
if (getSupportFragmentManager().backStackEntryCount == 1) {
onBackPressed()
onMediaClicked(index)
}
}
/**
* This function inflates the menu
*/
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val menuInflater = menuInflater
menuInflater.inflate(R.menu.menu_wikidata_item, menu)
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_item))
return super.onCreateOptionsMenu(menu)
}
/**
* This method handles the logic on item select in toolbar menu
* Currently only 1 choice is available to open Wikidata item details page in browser
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.browser_actions_menu_items -> {
val entityId = intent.getStringExtra("entityId")
val uri = Uri.parse("https://www.wikidata.org/wiki/$entityId")
handleWebUrl(this, uri)
return true
}
R.id.menu_bookmark_current_item -> {
if (intent.getStringExtra("fragment") != null) {
compositeDisposable!!.add(
depictModel!!.getDepictions(
intent.getStringExtra("entityId")!!
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(Consumer<List<DepictedItem?>> { depictedItems: List<DepictedItem?> ->
val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(
depictedItems[0]!!
)
val snackbar = if (bookmarkExists)
Snackbar.make(
findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG
)
else
Snackbar.make(
findViewById(R.id.toolbar_layout),
R.string.remove_bookmark,
Snackbar.LENGTH_LONG
)
snackbar.show()
updateBookmarkState(item)
})
)
} else {
val bookmarkExists = bookmarkItemsDao!!.updateBookmarkItem(wikidataItem!!)
val snackbar = if (bookmarkExists)
Snackbar.make(
findViewById(R.id.toolbar_layout),
R.string.add_bookmark, Snackbar.LENGTH_LONG
)
else
Snackbar.make(
findViewById(R.id.toolbar_layout), R.string.remove_bookmark,
Snackbar.LENGTH_LONG
)
snackbar.show()
updateBookmarkState(item)
}
return true
}
android.R.id.home -> {
onBackPressed()
return true
}
else -> return super.onOptionsItemSelected(item)
}
}
private fun updateBookmarkState(item: MenuItem) {
val isBookmarked: Boolean = if (intent.getStringExtra("fragment") != null) {
bookmarkItemsDao!!.findBookmarkItem(intent.getStringExtra("entityId"))
} else {
bookmarkItemsDao!!.findBookmarkItem(wikidataItem!!.id)
}
item.setIcon(if (isBookmarked) {
R.drawable.menu_ic_round_star_filled_24px
} else {
R.drawable.menu_ic_round_star_border_24px
})
}
companion object {
/**
* Consumers should be simply using this method to use this activity.
*
* @param context A Context of the application package implementing this class.
* @param depictedItem Name of the depicts for displaying its details
*/
fun startYourself(context: Context, depictedItem: DepictedItem) {
val intent = Intent(context, WikidataItemDetailsActivity::class.java).apply {
putExtra("wikidataItemName", depictedItem.name)
putExtra("entityId", depictedItem.id)
putExtra(WikidataConstants.BOOKMARKS_ITEMS, depictedItem)
}
context.startActivity(intent)
}
}
}

View file

@ -1,34 +0,0 @@
package fr.free.nrw.commons.explore.map;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.media.MediaClient;
import java.util.Collections;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Singleton;
@Singleton
public class ExploreMapCalls {
@Inject
MediaClient mediaClient;
@Inject
public ExploreMapCalls() {
}
/**
* Calls method to query Commons for uploads around a location
*
* @param currentLatLng coordinates of search location
* @return list of places obtained
*/
@NonNull
List<Media> callCommonsQuery(final LatLng currentLatLng) {
String coordinates = currentLatLng.getLatitude() + "|" + currentLatLng.getLongitude();
return mediaClient.getMediaListFromGeoSearch(coordinates).blockingGet();
}
}

View file

@ -0,0 +1,25 @@
package fr.free.nrw.commons.explore.map
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.media.MediaClient
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class ExploreMapCalls @Inject constructor() {
@Inject
@JvmField
var mediaClient: MediaClient? = null
/**
* Calls method to query Commons for uploads around a location
*
* @param currentLatLng coordinates of search location
* @return list of places obtained
*/
fun callCommonsQuery(currentLatLng: LatLng): List<Media> {
val coordinates = currentLatLng.latitude.toString() + "|" + currentLatLng.longitude
return mediaClient!!.getMediaListFromGeoSearch(coordinates).blockingGet()
}
}

View file

@ -1,45 +0,0 @@
package fr.free.nrw.commons.explore.map;
import android.content.Context;
import fr.free.nrw.commons.BaseMarker;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager;
import java.util.List;
public class ExploreMapContract {
interface View {
boolean isNetworkConnectionEstablished();
void populatePlaces(LatLng curlatLng);
void askForLocationPermission();
void recenterMap(LatLng curLatLng);
void hideBottomDetailsSheet();
LatLng getMapCenter();
LatLng getMapFocus();
LatLng getLastMapFocus();
void addMarkersToMap(final List<BaseMarker> nearbyBaseMarkers);
void clearAllMarkers();
void addSearchThisAreaButtonAction();
void setSearchThisAreaButtonVisibility(boolean isVisible);
void setProgressBarVisibility(boolean isVisible);
boolean isDetailsBottomSheetVisible();
boolean isSearchThisAreaButtonVisible();
Context getContext();
LatLng getLastLocation();
void disableFABRecenter();
void enableFABRecenter();
void setFABRecenterAction(android.view.View.OnClickListener onClickListener);
boolean backButtonClicked();
}
interface UserActions {
void updateMap(LocationServiceManager.LocationChangeType locationChangeType);
void lockUnlockNearby(boolean isNearbyLocked);
void attachView(View view);
void detachView();
void setActionListeners(JsonKvStore applicationKvStore);
boolean backButtonClicked();
}
}

View file

@ -0,0 +1,43 @@
package fr.free.nrw.commons.explore.map
import android.content.Context
import android.view.View
import fr.free.nrw.commons.BaseMarker
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
class ExploreMapContract {
interface View {
fun isNetworkConnectionEstablished(): Boolean
fun populatePlaces(curlatLng: LatLng?)
fun askForLocationPermission()
fun recenterMap(curLatLng: LatLng?)
fun hideBottomDetailsSheet()
fun getMapCenter(): LatLng?
fun getMapFocus(): LatLng?
fun getLastMapFocus(): LatLng?
fun addMarkersToMap(nearbyBaseMarkers: List<BaseMarker?>?)
fun clearAllMarkers()
fun addSearchThisAreaButtonAction()
fun setSearchThisAreaButtonVisibility(isVisible: Boolean)
fun setProgressBarVisibility(isVisible: Boolean)
fun isDetailsBottomSheetVisible(): Boolean
fun isSearchThisAreaButtonVisible(): Boolean
fun getContext(): Context?
fun getLastLocation(): LatLng?
fun disableFABRecenter()
fun enableFABRecenter()
fun setFABRecenterAction(onClickListener: android.view.View.OnClickListener?)
fun backButtonClicked(): Boolean
}
interface UserActions {
fun updateMap(locationChangeType: LocationChangeType)
fun lockUnlockNearby(isNearbyLocked: Boolean)
fun attachView(view: View?)
fun detachView()
fun setActionListeners(applicationKvStore: JsonKvStore?)
fun backButtonClicked(): Boolean
}
}

View file

@ -1,213 +0,0 @@
package fr.free.nrw.commons.explore.map;
import static fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween;
import static fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition;
import fr.free.nrw.commons.BaseMarker;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.Media;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.LocationUtils;
import fr.free.nrw.commons.utils.PlaceUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import timber.log.Timber;
public class ExploreMapController extends MapController {
private final ExploreMapCalls exploreMapCalls;
public LatLng latestSearchLocation; // Can be current and camera target on search this area button is used
public LatLng currentLocation; // current location of user
public double latestSearchRadius = 0; // Any last search radius
public double currentLocationSearchRadius = 0; // Search radius of only searches around current location
@Inject
public ExploreMapController(ExploreMapCalls explorePlaces) {
this.exploreMapCalls = explorePlaces;
}
/**
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
* explorePlaceList and boundaryCoordinates
*
* @param currentLatLng is current geolocation
* @param searchLatLng is the location that we want to search around
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around
* current location, false if another location
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
* boundaryCoordinates
*/
public ExplorePlacesInfo loadAttractionsFromLocation(LatLng currentLatLng, LatLng searchLatLng,
boolean checkingAroundCurrentLocation) {
if (searchLatLng == null) {
Timber.d("Loading attractions explore map, but search is null");
return null;
}
ExplorePlacesInfo explorePlacesInfo = new ExplorePlacesInfo();
try {
explorePlacesInfo.currentLatLng = currentLatLng;
latestSearchLocation = searchLatLng;
List<Media> mediaList = exploreMapCalls.callCommonsQuery(searchLatLng);
LatLng[] boundaryCoordinates = {mediaList.get(0).getCoordinates(), // south
mediaList.get(0).getCoordinates(), // north
mediaList.get(0).getCoordinates(), // west
mediaList.get(0).getCoordinates()};// east, init with a random location
if (searchLatLng != null) {
Timber.d("Sorting places by distance...");
final Map<Media, Double> distances = new HashMap<>();
for (Media media : mediaList) {
distances.put(media,
computeDistanceBetween(media.getCoordinates(), searchLatLng));
// Find boundaries with basic find max approach
if (media.getCoordinates().getLatitude()
< boundaryCoordinates[0].getLatitude()) {
boundaryCoordinates[0] = media.getCoordinates();
}
if (media.getCoordinates().getLatitude()
> boundaryCoordinates[1].getLatitude()) {
boundaryCoordinates[1] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude()
< boundaryCoordinates[2].getLongitude()) {
boundaryCoordinates[2] = media.getCoordinates();
}
if (media.getCoordinates().getLongitude()
> boundaryCoordinates[3].getLongitude()) {
boundaryCoordinates[3] = media.getCoordinates();
}
}
}
explorePlacesInfo.mediaList = mediaList;
explorePlacesInfo.explorePlaceList = PlaceUtils.mediaToExplorePlace(mediaList);
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates;
// Sets latestSearchRadius to maximum distance among boundaries and search location
for (LatLng bound : boundaryCoordinates) {
double distance = LocationUtils.calculateDistance(bound.getLatitude(),
bound.getLongitude(), searchLatLng.getLatitude(), searchLatLng.getLongitude());
if (distance > latestSearchRadius) {
latestSearchRadius = distance;
}
}
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
if (checkingAroundCurrentLocation) {
currentLocationSearchRadius = latestSearchRadius;
currentLocation = currentLatLng;
}
} catch (Exception e) {
e.printStackTrace();
}
return explorePlacesInfo;
}
/**
* Loads attractions from location for map view, we need to return places in Place data type
*
* @return baseMarkerOptions list that holds nearby places with their icons
*/
public static List<BaseMarker> loadAttractionsFromLocationToBaseMarkerOptions(
LatLng currentLatLng,
final List<Place> placeList,
Context context,
NearbyBaseMarkerThumbCallback callback,
ExplorePlacesInfo explorePlacesInfo) {
List<BaseMarker> baseMarkerList = new ArrayList<>();
if (placeList == null) {
return baseMarkerList;
}
VectorDrawableCompat vectorDrawable = null;
try {
vectorDrawable = VectorDrawableCompat.create(
context.getResources(), R.drawable.ic_custom_map_marker_dark, context.getTheme());
} catch (Resources.NotFoundException e) {
// ignore when running tests.
}
if (vectorDrawable != null) {
for (Place explorePlace : placeList) {
final BaseMarker baseMarker = new BaseMarker();
String distance = formatDistanceBetween(currentLatLng, explorePlace.location);
explorePlace.setDistance(distance);
baseMarker.setTitle(
explorePlace.name.substring(5, explorePlace.name.lastIndexOf(".")));
baseMarker.setPosition(
new fr.free.nrw.commons.location.LatLng(
explorePlace.location.getLatitude(),
explorePlace.location.getLongitude(), 0));
baseMarker.setPlace(explorePlace);
Glide.with(context)
.asBitmap()
.load(explorePlace.getThumb())
.placeholder(R.drawable.image_placeholder_96)
.apply(new RequestOptions().override(96, 96).centerCrop())
.into(new CustomTarget<Bitmap>() {
// We add icons to markers when bitmaps are ready
@Override
public void onResourceReady(@NonNull Bitmap resource,
@Nullable Transition<? super Bitmap> transition) {
baseMarker.setIcon(
ImageUtils.addRedBorder(resource, 6, context));
baseMarkerList.add(baseMarker);
if (baseMarkerList.size()
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
explorePlacesInfo);
}
}
@Override
public void onLoadCleared(@Nullable Drawable placeholder) {
}
// We add thumbnail icon for images that couldn't be loaded
@Override
public void onLoadFailed(@Nullable final Drawable errorDrawable) {
super.onLoadFailed(errorDrawable);
baseMarker.fromResource(context, R.drawable.image_placeholder_96);
baseMarkerList.add(baseMarker);
if (baseMarkerList.size()
== placeList.size()) { // if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(baseMarkerList,
explorePlacesInfo);
}
}
});
}
}
return baseMarkerList;
}
interface NearbyBaseMarkerThumbCallback {
// Callback to notify thumbnails of explore markers are added as icons and ready
void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
ExplorePlacesInfo explorePlacesInfo);
}
}

View file

@ -0,0 +1,219 @@
package fr.free.nrw.commons.explore.map
import android.content.Context
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.CustomTarget
import com.bumptech.glide.request.transition.Transition
import fr.free.nrw.commons.BaseMarker
import fr.free.nrw.commons.MapController
import fr.free.nrw.commons.Media
import fr.free.nrw.commons.R
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.nearby.Place
import fr.free.nrw.commons.utils.ImageUtils.addRedBorder
import fr.free.nrw.commons.utils.LengthUtils.computeDistanceBetween
import fr.free.nrw.commons.utils.LengthUtils.formatDistanceBetween
import fr.free.nrw.commons.utils.LocationUtils.calculateDistance
import fr.free.nrw.commons.utils.PlaceUtils.mediaToExplorePlace
import timber.log.Timber
import javax.inject.Inject
class ExploreMapController @Inject constructor(
private val exploreMapCalls: ExploreMapCalls
) : MapController() {
// Can be current and camera target on search this area button is used
private var latestSearchLocation: LatLng? = null
// Any last search radius
private var latestSearchRadius: Double = 0.0
// Search radius of only searches around current location
private var currentLocationSearchRadius: Double = 0.0
@JvmField
// current location of user
var currentLocation: LatLng? = null
/**
* Takes location as parameter and returns ExplorePlaces info that holds currentLatLng, mediaList,
* explorePlaceList and boundaryCoordinates
*
* @param currentLatLng is current geolocation
* @param searchLatLng is the location that we want to search around
* @param checkingAroundCurrentLocation is a boolean flag. True if we want to check around
* current location, false if another location
* @return explorePlacesInfo info that holds currentLatLng, mediaList, explorePlaceList and
* boundaryCoordinates
*/
fun loadAttractionsFromLocation(
currentLatLng: LatLng?, searchLatLng: LatLng?,
checkingAroundCurrentLocation: Boolean
): ExplorePlacesInfo? {
if (searchLatLng == null) {
Timber.d("Loading attractions explore map, but search is null")
return null
}
val explorePlacesInfo = ExplorePlacesInfo()
try {
explorePlacesInfo.currentLatLng = currentLatLng
latestSearchLocation = searchLatLng
val mediaList = exploreMapCalls.callCommonsQuery(searchLatLng)
val boundaryCoordinates = arrayOf(
mediaList[0].coordinates!!, // south
mediaList[0].coordinates!!, // north
mediaList[0].coordinates!!, // west
mediaList[0].coordinates!!
) // east, init with a random location
Timber.d("Sorting places by distance...")
val distances: MutableMap<Media, Double> = HashMap()
for (media in mediaList) {
distances[media] = computeDistanceBetween(media.coordinates!!, searchLatLng)
// Find boundaries with basic find max approach
if (media.coordinates!!.latitude
< boundaryCoordinates[0]!!.latitude
) {
boundaryCoordinates[0] = media.coordinates!!
}
if (media.coordinates!!.latitude
> boundaryCoordinates[1]!!.latitude
) {
boundaryCoordinates[1] = media.coordinates!!
}
if (media.coordinates!!.longitude
< boundaryCoordinates[2]!!.longitude
) {
boundaryCoordinates[2] = media.coordinates!!
}
if (media.coordinates!!.longitude
> boundaryCoordinates[3]!!.longitude
) {
boundaryCoordinates[3] = media.coordinates!!
}
}
explorePlacesInfo.mediaList = mediaList
explorePlacesInfo.explorePlaceList = mediaToExplorePlace(mediaList)
explorePlacesInfo.boundaryCoordinates = boundaryCoordinates
// Sets latestSearchRadius to maximum distance among boundaries and search location
for ((latitude, longitude) in boundaryCoordinates) {
val distance = calculateDistance(
latitude,
longitude, searchLatLng.latitude, searchLatLng.longitude
)
if (distance > latestSearchRadius) {
latestSearchRadius = distance
}
}
// Our radius searched around us, will be used to understand when user search their own location, we will follow them
if (checkingAroundCurrentLocation) {
currentLocationSearchRadius = latestSearchRadius
currentLocation = currentLatLng
}
} catch (e: Exception) {
Timber.e(e)
}
return explorePlacesInfo
}
interface NearbyBaseMarkerThumbCallback {
// Callback to notify thumbnails of explore markers are added as icons and ready
fun onNearbyBaseMarkerThumbsReady(
baseMarkers: List<BaseMarker>?,
explorePlacesInfo: ExplorePlacesInfo?
)
}
companion object {
/**
* Loads attractions from location for map view, we need to return places in Place data type
*
* @return baseMarkerOptions list that holds nearby places with their icons
*/
fun loadAttractionsFromLocationToBaseMarkerOptions(
currentLatLng: LatLng?,
placeList: List<Place>?,
context: Context,
callback: NearbyBaseMarkerThumbCallback,
explorePlacesInfo: ExplorePlacesInfo?
): List<BaseMarker> {
val baseMarkerList: MutableList<BaseMarker> = ArrayList()
if (placeList == null) {
return baseMarkerList
}
var vectorDrawable: VectorDrawableCompat? = null
try {
vectorDrawable = VectorDrawableCompat.create(
context.resources, R.drawable.ic_custom_map_marker_dark, context.theme
)
} catch (e: Resources.NotFoundException) {
// ignore when running tests.
}
if (vectorDrawable != null) {
for (explorePlace in placeList) {
val baseMarker = BaseMarker()
val distance = formatDistanceBetween(currentLatLng, explorePlace.location)
explorePlace.setDistance(distance)
baseMarker.title =
explorePlace.name.substring(5, explorePlace.name.lastIndexOf("."))
baseMarker.position = LatLng(
explorePlace.location.latitude,
explorePlace.location.longitude, 0f
)
baseMarker.place = explorePlace
Glide.with(context)
.asBitmap()
.load(explorePlace.thumb)
.placeholder(R.drawable.image_placeholder_96)
.apply(RequestOptions().override(96, 96).centerCrop())
.into(object : CustomTarget<Bitmap>() {
// We add icons to markers when bitmaps are ready
override fun onResourceReady(
resource: Bitmap,
transition: Transition<in Bitmap>?
) {
baseMarker.icon = addRedBorder(resource, 6, context)
baseMarkerList.add(baseMarker)
if (baseMarkerList.size == placeList.size) {
// if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(
baseMarkerList,
explorePlacesInfo
)
}
}
override fun onLoadCleared(placeholder: Drawable?) = Unit
// We add thumbnail icon for images that couldn't be loaded
override fun onLoadFailed(errorDrawable: Drawable?) {
super.onLoadFailed(errorDrawable)
baseMarker.fromResource(context, R.drawable.image_placeholder_96)
baseMarkerList.add(baseMarker)
if (baseMarkerList.size == placeList.size) {
// if true, we added all markers to list and can trigger thumbs ready callback
callback.onNearbyBaseMarkerThumbsReady(
baseMarkerList,
explorePlacesInfo
)
}
}
})
}
}
return baseMarkerList
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,237 +0,0 @@
package fr.free.nrw.commons.explore.map;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED;
import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.SEARCH_CUSTOM_AREA;
import android.location.Location;
import android.view.View;
import fr.free.nrw.commons.BaseMarker;
import fr.free.nrw.commons.MapController;
import fr.free.nrw.commons.MapController.ExplorePlacesInfo;
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType;
import fr.free.nrw.commons.nearby.Place;
import io.reactivex.Observable;
import java.lang.reflect.Proxy;
import java.util.List;
import timber.log.Timber;
public class ExploreMapPresenter
implements ExploreMapContract.UserActions,
NearbyBaseMarkerThumbCallback {
BookmarkLocationsDao bookmarkLocationDao;
private boolean isNearbyLocked;
private LatLng currentLatLng;
private ExploreMapController exploreMapController;
private static final ExploreMapContract.View DUMMY = (ExploreMapContract.View) Proxy
.newProxyInstance(
ExploreMapContract.View.class.getClassLoader(),
new Class[]{ExploreMapContract.View.class}, (proxy, method, args) -> {
if (method.getName().equals("onMyEvent")) {
return null;
} else if (String.class == method.getReturnType()) {
return "";
} else if (Integer.class == method.getReturnType()) {
return Integer.valueOf(0);
} else if (int.class == method.getReturnType()) {
return 0;
} else if (Boolean.class == method.getReturnType()) {
return Boolean.FALSE;
} else if (boolean.class == method.getReturnType()) {
return false;
} else {
return null;
}
}
);
private ExploreMapContract.View exploreMapFragmentView = DUMMY;
public ExploreMapPresenter(BookmarkLocationsDao bookmarkLocationDao) {
this.bookmarkLocationDao = bookmarkLocationDao;
}
@Override
public void updateMap(LocationChangeType locationChangeType) {
Timber.d("Presenter updates map and list" + locationChangeType.toString());
if (isNearbyLocked) {
Timber.d("Nearby is locked, so updateMapAndList returns");
return;
}
if (!exploreMapFragmentView.isNetworkConnectionEstablished()) {
Timber.d("Network connection is not established");
return;
}
/**
* Significant changed - Markers and current location will be updated together
* Slightly changed - Only current position marker will be updated
*/
if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED)) {
Timber.d("LOCATION_SIGNIFICANTLY_CHANGED");
LatLng populateLatLng = exploreMapFragmentView.getMapCenter();
//If "Show in Explore" was selected in Nearby, use the previous LatLng
if (exploreMapFragmentView instanceof ExploreMapFragment) {
ExploreMapFragment exploreMapFragment = (ExploreMapFragment)exploreMapFragmentView;
if (exploreMapFragment.recentlyCameFromNearbyMap()) {
//Ensure this LatLng will not be used again if user searches their GPS location
exploreMapFragment.setRecentlyCameFromNearbyMap(false);
populateLatLng = exploreMapFragment.getPreviousLatLng();
}
}
lockUnlockNearby(true);
exploreMapFragmentView.setProgressBarVisibility(true);
exploreMapFragmentView.populatePlaces(populateLatLng);
} else if (locationChangeType.equals(SEARCH_CUSTOM_AREA)) {
Timber.d("SEARCH_CUSTOM_AREA");
lockUnlockNearby(true);
exploreMapFragmentView.setProgressBarVisibility(true);
exploreMapFragmentView.populatePlaces(exploreMapFragmentView.getMapFocus());
} else { // Means location changed slightly, ie user is walking or driving.
Timber.d("Means location changed slightly");
}
}
/**
* Nearby updates takes time, since they are network operations. During update time, we don't
* want to get any other calls from user. So locking nearby.
*
* @param isNearbyLocked true means lock, false means unlock
*/
@Override
public void lockUnlockNearby(boolean isNearbyLocked) {
this.isNearbyLocked = isNearbyLocked;
if (isNearbyLocked) {
exploreMapFragmentView.disableFABRecenter();
} else {
exploreMapFragmentView.enableFABRecenter();
}
}
@Override
public void attachView(ExploreMapContract.View view) {
exploreMapFragmentView = view;
}
@Override
public void detachView() {
exploreMapFragmentView = DUMMY;
}
/**
* Sets click listener of FAB
*/
@Override
public void setActionListeners(JsonKvStore applicationKvStore) {
exploreMapFragmentView.setFABRecenterAction(v -> {
exploreMapFragmentView.recenterMap(currentLatLng);
});
}
@Override
public boolean backButtonClicked() {
return exploreMapFragmentView.backButtonClicked();
}
public void onMapReady(ExploreMapController exploreMapController) {
this.exploreMapController = exploreMapController;
if (null != exploreMapFragmentView) {
exploreMapFragmentView.addSearchThisAreaButtonAction();
initializeMapOperations();
}
}
public void initializeMapOperations() {
lockUnlockNearby(false);
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
}
public Observable<ExplorePlacesInfo> loadAttractionsFromLocation(LatLng currentLatLng,
LatLng searchLatLng, boolean checkingAroundCurrent) {
return Observable
.fromCallable(() -> exploreMapController
.loadAttractionsFromLocation(currentLatLng, searchLatLng, checkingAroundCurrent));
}
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
*
* @param explorePlacesInfo This variable has placeToCenter list information and distances.
*/
public void updateMapMarkers(
MapController.ExplorePlacesInfo explorePlacesInfo) {
if (explorePlacesInfo.mediaList != null) {
prepareNearbyBaseMarkers(explorePlacesInfo);
} else {
lockUnlockNearby(false); // So that new location updates wont come
exploreMapFragmentView.setProgressBarVisibility(false);
}
}
void prepareNearbyBaseMarkers(MapController.ExplorePlacesInfo explorePlacesInfo) {
exploreMapController
.loadAttractionsFromLocationToBaseMarkerOptions(explorePlacesInfo.currentLatLng,
// Curlatlang will be used to calculate distances
(List<Place>) explorePlacesInfo.explorePlaceList,
exploreMapFragmentView.getContext(),
this,
explorePlacesInfo);
}
@Override
public void onNearbyBaseMarkerThumbsReady(List<BaseMarker> baseMarkers,
ExplorePlacesInfo explorePlacesInfo) {
if (null != exploreMapFragmentView) {
exploreMapFragmentView.addMarkersToMap(baseMarkers);
lockUnlockNearby(false); // So that new location updates wont come
exploreMapFragmentView.setProgressBarVisibility(false);
}
}
public View.OnClickListener onSearchThisAreaClicked() {
return v -> {
// Lock map operations during search this area operation
exploreMapFragmentView.setSearchThisAreaButtonVisibility(false);
if (searchCloseToCurrentLocation()) {
updateMap(LOCATION_SIGNIFICANTLY_CHANGED);
} else {
updateMap(SEARCH_CUSTOM_AREA);
}
};
}
/**
* Returns true if search this area button is used around our current location, so that we can
* continue following our current location again
*
* @return Returns true if search this area button is used around our current location
*/
public boolean searchCloseToCurrentLocation() {
if (null == exploreMapFragmentView.getLastMapFocus()) {
return true;
}
Location mylocation = new Location("");
Location dest_location = new Location("");
dest_location.setLatitude(exploreMapFragmentView.getMapFocus().getLatitude());
dest_location.setLongitude(exploreMapFragmentView.getMapFocus().getLongitude());
mylocation.setLatitude(exploreMapFragmentView.getLastMapFocus().getLatitude());
mylocation.setLongitude(exploreMapFragmentView.getLastMapFocus().getLongitude());
Float distance = mylocation.distanceTo(dest_location);
return !(distance > 2000.0 * 3 / 4);
}
}

View file

@ -0,0 +1,223 @@
package fr.free.nrw.commons.explore.map
import android.location.Location
import android.view.View
import fr.free.nrw.commons.BaseMarker
import fr.free.nrw.commons.MapController.ExplorePlacesInfo
import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao
import fr.free.nrw.commons.explore.map.ExploreMapController.Companion.loadAttractionsFromLocationToBaseMarkerOptions
import fr.free.nrw.commons.explore.map.ExploreMapController.NearbyBaseMarkerThumbCallback
import fr.free.nrw.commons.kvstore.JsonKvStore
import fr.free.nrw.commons.location.LatLng
import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
import fr.free.nrw.commons.nearby.Place
import io.reactivex.Observable
import timber.log.Timber
import java.lang.reflect.Method
import java.lang.reflect.Proxy
import java.util.concurrent.Callable
class ExploreMapPresenter(
var bookmarkLocationDao: BookmarkLocationsDao
) : ExploreMapContract.UserActions, NearbyBaseMarkerThumbCallback {
private var isNearbyLocked = false
private val currentLatLng: LatLng? = null
private var exploreMapController: ExploreMapController? = null
private var exploreMapFragmentView: ExploreMapContract.View? = DUMMY
override fun updateMap(locationChangeType: LocationChangeType) {
Timber.d("Presenter updates map and list$locationChangeType")
if (isNearbyLocked) {
Timber.d("Nearby is locked, so updateMapAndList returns")
return
}
if (!exploreMapFragmentView!!.isNetworkConnectionEstablished()) {
Timber.d("Network connection is not established")
return
}
/**
* Significant changed - Markers and current location will be updated together
* Slightly changed - Only current position marker will be updated
*/
if (locationChangeType == LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) {
Timber.d("LOCATION_SIGNIFICANTLY_CHANGED")
var populateLatLng = exploreMapFragmentView!!.getMapCenter()
//If "Show in Explore" was selected in Nearby, use the previous LatLng
if (exploreMapFragmentView is ExploreMapFragment) {
val exploreMapFragment = exploreMapFragmentView as ExploreMapFragment
if (exploreMapFragment.recentlyCameFromNearbyMap()) {
//Ensure this LatLng will not be used again if user searches their GPS location
exploreMapFragment.setRecentlyCameFromNearbyMap(false)
populateLatLng = exploreMapFragment.previousLatLng
}
}
lockUnlockNearby(true)
exploreMapFragmentView!!.setProgressBarVisibility(true)
exploreMapFragmentView!!.populatePlaces(populateLatLng)
} else if (locationChangeType == LocationChangeType.SEARCH_CUSTOM_AREA) {
Timber.d("SEARCH_CUSTOM_AREA")
lockUnlockNearby(true)
exploreMapFragmentView!!.setProgressBarVisibility(true)
exploreMapFragmentView!!.populatePlaces(exploreMapFragmentView!!.getMapFocus())
} else { // Means location changed slightly, ie user is walking or driving.
Timber.d("Means location changed slightly")
}
}
/**
* Nearby updates takes time, since they are network operations. During update time, we don't
* want to get any other calls from user. So locking nearby.
*
* @param isNearbyLocked true means lock, false means unlock
*/
override fun lockUnlockNearby(isNearbyLocked: Boolean) {
this.isNearbyLocked = isNearbyLocked
if (isNearbyLocked) {
exploreMapFragmentView!!.disableFABRecenter()
} else {
exploreMapFragmentView!!.enableFABRecenter()
}
}
override fun attachView(view: ExploreMapContract.View?) {
exploreMapFragmentView = view
}
override fun detachView() {
exploreMapFragmentView = DUMMY
}
/**
* Sets click listener of FAB
*/
override fun setActionListeners(applicationKvStore: JsonKvStore?) {
exploreMapFragmentView!!.setFABRecenterAction {
exploreMapFragmentView!!.recenterMap(currentLatLng)
}
}
override fun backButtonClicked(): Boolean =
exploreMapFragmentView!!.backButtonClicked()
fun onMapReady(exploreMapController: ExploreMapController?) {
this.exploreMapController = exploreMapController
if (null != exploreMapFragmentView) {
exploreMapFragmentView!!.addSearchThisAreaButtonAction()
initializeMapOperations()
}
}
fun initializeMapOperations() {
lockUnlockNearby(false)
updateMap(LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED)
}
fun loadAttractionsFromLocation(
currentLatLng: LatLng?,
searchLatLng: LatLng?, checkingAroundCurrent: Boolean
): Observable<ExplorePlacesInfo?> = Observable.fromCallable(Callable {
exploreMapController!!.loadAttractionsFromLocation(
currentLatLng,
searchLatLng,
checkingAroundCurrent
)
})
/**
* Populates places for custom location, should be used for finding nearby places around a
* location where you are not at.
*
* @param explorePlacesInfo This variable has placeToCenter list information and distances.
*/
fun updateMapMarkers(
explorePlacesInfo: ExplorePlacesInfo
) {
if (explorePlacesInfo.mediaList != null) {
prepareNearbyBaseMarkers(explorePlacesInfo)
} else {
lockUnlockNearby(false) // So that new location updates wont come
exploreMapFragmentView!!.setProgressBarVisibility(false)
}
}
private fun prepareNearbyBaseMarkers(explorePlacesInfo: ExplorePlacesInfo) {
loadAttractionsFromLocationToBaseMarkerOptions(
explorePlacesInfo.currentLatLng, // Curlatlang will be used to calculate distances
explorePlacesInfo.explorePlaceList,
exploreMapFragmentView!!.getContext()!!,
this,
explorePlacesInfo
)
}
override fun onNearbyBaseMarkerThumbsReady(
baseMarkers: List<BaseMarker>?,
explorePlacesInfo: ExplorePlacesInfo?
) {
if (null != exploreMapFragmentView) {
exploreMapFragmentView!!.addMarkersToMap(baseMarkers)
lockUnlockNearby(false) // So that new location updates wont come
exploreMapFragmentView!!.setProgressBarVisibility(false)
}
}
fun onSearchThisAreaClicked(): View.OnClickListener {
return View.OnClickListener {
// Lock map operations during search this area operation
exploreMapFragmentView!!.setSearchThisAreaButtonVisibility(false)
updateMap(if (searchCloseToCurrentLocation()) {
LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED
} else {
LocationChangeType.SEARCH_CUSTOM_AREA
})
}
}
/**
* Returns true if search this area button is used around our current location, so that we can
* continue following our current location again
*
* @return Returns true if search this area button is used around our current location
*/
private fun searchCloseToCurrentLocation(): Boolean {
if (null == exploreMapFragmentView!!.getLastMapFocus()) {
return true
}
val mylocation = Location("").apply {
latitude = exploreMapFragmentView!!.getLastMapFocus()!!.latitude
longitude = exploreMapFragmentView!!.getLastMapFocus()!!.longitude
}
val dest_location = Location("").apply {
latitude = exploreMapFragmentView!!.getMapFocus()!!.latitude
longitude = exploreMapFragmentView!!.getMapFocus()!!.longitude
}
val distance = mylocation.distanceTo(dest_location)
return !(distance > 2000.0 * 3 / 4)
}
companion object {
private val DUMMY = Proxy.newProxyInstance(
ExploreMapContract.View::class.java.classLoader,
arrayOf<Class<*>>(ExploreMapContract.View::class.java)
) { _: Any?, method: Method, _: Array<Any?>? ->
when {
method.name == "onMyEvent" -> null
String::class.java == method.returnType -> ""
Int::class.java == method.returnType -> 0
Int::class.javaPrimitiveType == method.returnType -> 0
Boolean::class.java == method.returnType -> java.lang.Boolean.FALSE
Boolean::class.javaPrimitiveType == method.returnType -> false
else -> null
}
} as ExploreMapContract.View
}
}

View file

@ -1,202 +0,0 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import javax.inject.Inject;
import fr.free.nrw.commons.BuildConfig;
import fr.free.nrw.commons.data.DBOpenHelper;
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
import timber.log.Timber;
import static android.content.UriMatcher.NO_MATCH;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID;
import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME;
/**
* This class contains functions for executing queries for
* inserting, searching, deleting, editing recent searches in SqLite DB
**/
public class RecentSearchesContentProvider extends CommonsDaggerContentProvider {
// For URI matcher
private static final int RECENT_SEARCHES = 1;
private static final int RECENT_SEARCHES_ID = 2;
private static final String BASE_PATH = "recent_searches";
public static final Uri BASE_URI = Uri.parse("content://" + BuildConfig.RECENT_SEARCH_AUTHORITY + "/" + BASE_PATH);
private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH);
static {
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES);
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH + "/#", RECENT_SEARCHES_ID);
}
public static Uri uriForId(int id) {
return Uri.parse(BASE_URI.toString() + "/" + id);
}
@Inject DBOpenHelper dbOpenHelper;
/**
* This functions executes query for searching recent searches in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
queryBuilder.setTables(TABLE_NAME);
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
Cursor cursor;
switch (uriType) {
case RECENT_SEARCHES:
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
break;
case RECENT_SEARCHES_ID:
cursor = queryBuilder.query(db,
ALL_FIELDS,
"_id = ?",
new String[]{uri.getLastPathSegment()},
null,
null,
sortOrder
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* This functions executes query for inserting a recentSearch object in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public Uri insert(@NonNull Uri uri, ContentValues contentValues) {
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
long id;
switch (uriType) {
case RECENT_SEARCHES:
id = sqlDB.insert(TABLE_NAME, null, contentValues);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return Uri.parse(BASE_URI + "/" + id);
}
/**
* This functions executes query for deleting a recentSearch object in SqLite DB
**/
@Override
public int delete(@NonNull Uri uri, String s, String[] strings) {
int rows;
int uriType = uriMatcher.match(uri);
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
switch (uriType) {
case RECENT_SEARCHES_ID:
Timber.d("Deleting recent searches id %s", uri.getLastPathSegment());
rows = db.delete(RecentSearchesDao.Table.TABLE_NAME,
"_id = ?",
new String[]{uri.getLastPathSegment()}
);
break;
default:
throw new IllegalArgumentException("Unknown URI" + uri);
}
getContext().getContentResolver().notifyChange(uri, null);
return rows;
}
/**
* This functions executes query for inserting multiple recentSearch objects in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) {
Timber.d("Hello, bulk insert! (RecentSearchesContentProvider)");
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
sqlDB.beginTransaction();
switch (uriType) {
case RECENT_SEARCHES:
for (ContentValues value : values) {
Timber.d("Inserting! %s", value);
sqlDB.insert(TABLE_NAME, null, value);
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
sqlDB.setTransactionSuccessful();
sqlDB.endTransaction();
getContext().getContentResolver().notifyChange(uri, null);
return values.length;
}
/**
* This functions executes query for updating a particular recentSearch object in SqLite DB
**/
@SuppressWarnings("ConstantConditions")
@Override
public int update(@NonNull Uri uri, ContentValues contentValues, String selection,
String[] selectionArgs) {
/*
SQL Injection warnings: First, note that we're not exposing this to the
outside world (exported="false"). Even then, we should make sure to sanitize
all user input appropriately. Input that passes through ContentValues
should be fine. So only issues are those that pass in via concating.
In here, the only concat created argument is for id. It is cast to an int,
and will error out otherwise.
*/
int uriType = uriMatcher.match(uri);
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
int rowsUpdated;
switch (uriType) {
case RECENT_SEARCHES_ID:
if (TextUtils.isEmpty(selection)) {
int id = Integer.valueOf(uri.getLastPathSegment());
rowsUpdated = sqlDB.update(TABLE_NAME,
contentValues,
COLUMN_ID + " = ?",
new String[]{String.valueOf(id)});
} else {
throw new IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID");
}
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
}
getContext().getContentResolver().notifyChange(uri, null);
return rowsUpdated;
}
}

View file

@ -0,0 +1,174 @@
package fr.free.nrw.commons.explore.recentsearches
import android.content.ContentValues
import android.content.UriMatcher
import android.database.Cursor
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import androidx.core.net.toUri
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.TABLE_NAME
/**
* This class contains functions for executing queries for
* inserting, searching, deleting, editing recent searches in SqLite DB
*/
class RecentSearchesContentProvider : CommonsDaggerContentProvider() {
/**
* This functions executes query for searching recent searches in SqLite DB
*/
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor {
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val uriType = uriMatcher.match(uri)
val cursor = when (uriType) {
RECENT_SEARCHES -> queryBuilder.query(
requireDb(), projection, selection, selectionArgs,
null, null, sortOrder
)
RECENT_SEARCHES_ID -> queryBuilder.query(
requireDb(),
ALL_FIELDS,
"$COLUMN_ID = ?",
arrayOf(uri.lastPathSegment),
null,
null,
sortOrder
)
else -> throw IllegalArgumentException("Unknown URI$uri")
}
cursor.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
override fun getType(uri: Uri): String? = null
/**
* This functions executes query for inserting a recentSearch object in SqLite DB
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
val uriType = uriMatcher.match(uri)
val id: Long = when (uriType) {
RECENT_SEARCHES -> requireDb().insert(TABLE_NAME, null, contentValues)
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
requireContext().contentResolver.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
/**
* This functions executes query for deleting a recentSearch object in SqLite DB
*/
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val rows: Int
val uriType = uriMatcher.match(uri)
when (uriType) {
RECENT_SEARCHES_ID -> {
rows = requireDb().delete(
TABLE_NAME,
"_id = ?",
arrayOf(uri.lastPathSegment)
)
}
else -> throw IllegalArgumentException("Unknown URI - $uri")
}
requireContext().contentResolver.notifyChange(uri, null)
return rows
}
/**
* This functions executes query for inserting multiple recentSearch objects in SqLite DB
*/
override fun bulkInsert(uri: Uri, values: Array<ContentValues>): Int {
val uriType = uriMatcher.match(uri)
val sqlDB = requireDb()
sqlDB.beginTransaction()
when (uriType) {
RECENT_SEARCHES -> for (value in values) {
sqlDB.insert(TABLE_NAME, null, value)
}
else -> throw IllegalArgumentException("Unknown URI: $uri")
}
sqlDB.setTransactionSuccessful()
sqlDB.endTransaction()
requireContext().contentResolver.notifyChange(uri, null)
return values.size
}
/**
* This functions executes query for updating a particular recentSearch object in SqLite DB
*/
override fun update(
uri: Uri, contentValues: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
/*
SQL Injection warnings: First, note that we're not exposing this to the
outside world (exported="false"). Even then, we should make sure to sanitize
all user input appropriately. Input that passes through ContentValues
should be fine. So only issues are those that pass in via concating.
In here, the only concat created argument is for id. It is cast to an int,
and will error out otherwise.
*/
val uriType = uriMatcher.match(uri)
val rowsUpdated: Int
when (uriType) {
RECENT_SEARCHES_ID -> if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment!!.toInt()
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_ID = ?",
arrayOf(id.toString())
)
} else {
throw IllegalArgumentException(
"Parameter `selection` should be empty when updating an ID"
)
}
else -> throw IllegalArgumentException("Unknown URI: $uri with type $uriType")
}
requireContext().contentResolver.notifyChange(uri, null)
return rowsUpdated
}
companion object {
// For URI matcher
private const val RECENT_SEARCHES = 1
private const val RECENT_SEARCHES_ID = 2
private const val BASE_PATH = "recent_searches"
@JvmField
val BASE_URI: Uri = "content://${BuildConfig.RECENT_SEARCH_AUTHORITY}/$BASE_PATH".toUri()
private val uriMatcher = UriMatcher(UriMatcher.NO_MATCH)
init {
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES)
uriMatcher.addURI(BuildConfig.RECENT_SEARCH_AUTHORITY, "$BASE_PATH/#", RECENT_SEARCHES_ID)
}
@JvmStatic
fun uriForId(id: Int): Uri = "$BASE_URI/$id".toUri()
}
}

View file

@ -1,275 +0,0 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.annotation.SuppressLint;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.RemoteException;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import fr.free.nrw.commons.explore.models.RecentSearch;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import timber.log.Timber;
/**
* This class doesn't execute queries in database directly instead it contains the logic behind
* inserting, deleting, searching data from recent searches database.
**/
public class RecentSearchesDao {
private final Provider<ContentProviderClient> clientProvider;
@Inject
public RecentSearchesDao(@Named("recentsearch") Provider<ContentProviderClient> clientProvider) {
this.clientProvider = clientProvider;
}
/**
* This method is called on click of media/ categories for storing them in recent searches
* @param recentSearch a recent searches object that is to be added in SqLite DB
*/
public void save(RecentSearch recentSearch) {
ContentProviderClient db = clientProvider.get();
try {
if (recentSearch.getContentUri() == null) {
recentSearch.setContentUri(db.insert(RecentSearchesContentProvider.BASE_URI, toContentValues(recentSearch)));
} else {
db.update(recentSearch.getContentUri(), toContentValues(recentSearch), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* This method is called on confirmation of delete recent searches.
* It deletes all recent searches from the database
*/
public void deleteAll() {
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
RecentSearchesContentProvider.BASE_URI,
Table.ALL_FIELDS,
null,
new String[]{},
Table.COLUMN_LAST_USED + " DESC"
);
while (cursor != null && cursor.moveToNext()) {
try {
RecentSearch recentSearch = find(fromCursor(cursor).getQuery());
if (recentSearch.getContentUri() == null) {
throw new RuntimeException("tried to delete item with no content URI");
} else {
Timber.d("QUERY_NAME %s - delete tried", recentSearch.getContentUri());
db.delete(recentSearch.getContentUri(), null, null);
Timber.d("QUERY_NAME %s - query deleted", recentSearch.getQuery());
}
} catch (RemoteException e) {
Timber.e(e, "query deleted");
throw new RuntimeException(e);
} finally {
db.release();
}
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
}
}
/**
* Deletes a recent search from the database
*/
public void delete(RecentSearch recentSearch) {
ContentProviderClient db = clientProvider.get();
try {
if (recentSearch.getContentUri() == null) {
throw new RuntimeException("tried to delete item with no content URI");
} else {
db.delete(recentSearch.getContentUri(), null, null);
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
db.release();
}
}
/**
* Find persisted search query in database, based on its name.
* @param name Search query Ex- "butterfly"
* @return recently searched query from database, or null if not found
*/
@Nullable
public RecentSearch find(String name) {
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query(
RecentSearchesContentProvider.BASE_URI,
Table.ALL_FIELDS,
Table.COLUMN_NAME + "=?",
new String[]{name},
null);
if (cursor != null && cursor.moveToFirst()) {
return fromCursor(cursor);
}
} catch (RemoteException e) {
// This feels lazy, but to hell with checked exceptions. :)
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return null;
}
/**
* Retrieve recently-searched queries, ordered by descending date.
* @return a list containing recent searches
*/
@NonNull
public List<String> recentSearches(int limit) {
List<String> items = new ArrayList<>();
Cursor cursor = null;
ContentProviderClient db = clientProvider.get();
try {
cursor = db.query( RecentSearchesContentProvider.BASE_URI, Table.ALL_FIELDS,
null, new String[]{}, 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) {
items.add(fromCursor(cursor).getQuery());
}
} catch (RemoteException e) {
throw new RuntimeException(e);
} finally {
if (cursor != null) {
cursor.close();
}
db.release();
}
return items;
}
/**
* It creates an Recent Searches object from data stored in the SQLite DB by using cursor
* @param cursor
* @return RecentSearch object
*/
@NonNull
@SuppressLint("Range")
RecentSearch fromCursor(Cursor cursor) {
// Hardcoding column positions!
return new RecentSearch(
RecentSearchesContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))),
cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)),
new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED)))
);
}
/**
* This class contains the database table architechture for recent searches,
* It also contains queries and logic necessary to the create, update, delete this table.
*/
private ContentValues toContentValues(RecentSearch recentSearch) {
ContentValues cv = new ContentValues();
cv.put(RecentSearchesDao.Table.COLUMN_NAME, recentSearch.getQuery());
cv.put(RecentSearchesDao.Table.COLUMN_LAST_USED, recentSearch.getLastSearched().getTime());
return cv;
}
/**
* This class contains the database table architechture for recent searches,
* It also contains queries and logic necessary to the create, update, delete this table.
*/
public static class Table {
public static final String TABLE_NAME = "recent_searches";
public static final String COLUMN_ID = "_id";
static final String COLUMN_NAME = "name";
static final String COLUMN_LAST_USED = "last_used";
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
public static final String[] ALL_FIELDS = {
COLUMN_ID,
COLUMN_NAME,
COLUMN_LAST_USED,
};
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
+ COLUMN_NAME + " STRING,"
+ COLUMN_LAST_USED + " INTEGER"
+ ");";
/**
* This method creates a RecentSearchesTable in SQLiteDatabase
* @param db SQLiteDatabase
*/
public static void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE_STATEMENT);
}
/**
* This method deletes RecentSearchesTable from SQLiteDatabase
* @param db SQLiteDatabase
*/
public static void onDelete(SQLiteDatabase db) {
db.execSQL(DROP_TABLE_STATEMENT);
onCreate(db);
}
/**
* This method is called on migrating from a older version to a newer version
* @param db SQLiteDatabase
* @param from Version from which we are migrating
* @param to Version to which we are migrating
*/
public static void onUpdate(SQLiteDatabase db, int from, int to) {
if (from == to) {
return;
}
if (from < 6) {
// doesn't exist yet
from++;
onUpdate(db, from, to);
return;
}
if (from == 6) {
// table added in version 7
onCreate(db);
from++;
onUpdate(db, from, to);
return;
}
if (from == 7) {
from++;
onUpdate(db, from, to);
return;
}
}
}
}

View file

@ -0,0 +1,180 @@
package fr.free.nrw.commons.explore.recentsearches
import android.annotation.SuppressLint
import android.content.ContentProviderClient
import android.content.ContentValues
import android.database.Cursor
import android.os.RemoteException
import androidx.core.content.contentValuesOf
import fr.free.nrw.commons.explore.models.RecentSearch
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.BASE_URI
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.Companion.uriForId
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.ALL_FIELDS
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_ID
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_LAST_USED
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesTable.COLUMN_NAME
import fr.free.nrw.commons.utils.getInt
import fr.free.nrw.commons.utils.getLong
import fr.free.nrw.commons.utils.getString
import java.util.Date
import javax.inject.Inject
import javax.inject.Named
import javax.inject.Provider
/**
* This class doesn't execute queries in database directly instead it contains the logic behind
* inserting, deleting, searching data from recent searches database.
*/
class RecentSearchesDao @Inject constructor(
@param:Named("recentsearch") private val clientProvider: Provider<ContentProviderClient>
) {
/**
* This method is called on click of media/ categories for storing them in recent searches
* @param recentSearch a recent searches object that is to be added in SqLite DB
*/
fun save(recentSearch: RecentSearch) {
val db = clientProvider.get()
try {
val contentValues = toContentValues(recentSearch)
if (recentSearch.contentUri == null) {
recentSearch.contentUri = db.insert(BASE_URI, contentValues)
} else {
db.update(recentSearch.contentUri!!, contentValues, null, null)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* This method is called on confirmation of delete recent searches.
* It deletes all recent searches from the database
*/
fun deleteAll() {
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
BASE_URI,
ALL_FIELDS,
null,
arrayOf(),
"$COLUMN_LAST_USED DESC"
)
while (cursor != null && cursor.moveToNext()) {
try {
val recentSearch = find(fromCursor(cursor).query)
if (recentSearch!!.contentUri == null) {
throw RuntimeException("tried to delete item with no content URI")
} else {
db.delete(recentSearch.contentUri!!, null, null)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
}
}
/**
* Deletes a recent search from the database
*/
fun delete(recentSearch: RecentSearch) {
val db = clientProvider.get()
try {
if (recentSearch.contentUri == null) {
throw RuntimeException("tried to delete item with no content URI")
} else {
db.delete(recentSearch.contentUri!!, null, null)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
db.release()
}
}
/**
* Find persisted search query in database, based on its name.
* @param name Search query Ex- "butterfly"
* @return recently searched query from database, or null if not found
*/
fun find(name: String): RecentSearch? {
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
BASE_URI,
ALL_FIELDS,
"$COLUMN_NAME=?",
arrayOf(name),
null
)
if (cursor != null && cursor.moveToFirst()) {
return fromCursor(cursor)
}
} catch (e: RemoteException) {
// This feels lazy, but to hell with checked exceptions. :)
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return null
}
/**
* Retrieve recently-searched queries, ordered by descending date.
* @return a list containing recent searches
*/
fun recentSearches(limit: Int): List<String> {
val items: MutableList<String> = mutableListOf()
var cursor: Cursor? = null
val db = clientProvider.get()
try {
cursor = db.query(
BASE_URI, ALL_FIELDS,
null, arrayOf(), "$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.position < limit) {
items.add(fromCursor(cursor).query)
}
} catch (e: RemoteException) {
throw RuntimeException(e)
} finally {
cursor?.close()
db.release()
}
return items
}
/**
* It creates an Recent Searches object from data stored in the SQLite DB by using cursor
* @param cursor
* @return RecentSearch object
*/
fun fromCursor(cursor: Cursor): RecentSearch = RecentSearch(
uriForId(cursor.getInt(COLUMN_ID)),
cursor.getString(COLUMN_NAME),
Date(cursor.getLong(COLUMN_LAST_USED))
)
/**
* This class contains the database table architechture for recent searches,
* It also contains queries and logic necessary to the create, update, delete this table.
*/
private fun toContentValues(recentSearch: RecentSearch): ContentValues = contentValuesOf(
COLUMN_NAME to recentSearch.query,
COLUMN_LAST_USED to recentSearch.lastSearched.time
)
}

View file

@ -1,149 +0,0 @@
package fr.free.nrw.commons.explore.recentsearches;
import android.content.Context;
import android.content.DialogInterface;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.explore.SearchActivity;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
/**
* Displays the recent searches screen.
*/
public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
@Inject
RecentSearchesDao recentSearchesDao;
List<String> recentSearches;
ArrayAdapter adapter;
private FragmentSearchHistoryBinding binding;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
binding = FragmentSearchHistoryBinding.inflate(inflater, container, false);
recentSearches = recentSearchesDao.recentSearches(10);
if (recentSearches.isEmpty()) {
binding.recentSearchesDeleteButton.setVisibility(View.GONE);
binding.recentSearchesTextView.setText(R.string.no_recent_searches);
}
binding.recentSearchesDeleteButton.setOnClickListener(v -> {
showDeleteRecentAlertDialog(requireContext());
});
adapter = new ArrayAdapter<>(requireContext(), R.layout.item_recent_searches,
recentSearches);
binding.recentSearchesList.setAdapter(adapter);
binding.recentSearchesList.setOnItemClickListener((parent, view, position, id) -> (
(SearchActivity) getContext()).updateText(recentSearches.get(position)));
binding.recentSearchesList.setOnItemLongClickListener((parent, view, position, id) -> {
showDeleteAlertDialog(requireContext(), position);
return true;
});
updateRecentSearches();
return binding.getRoot();
}
private void showDeleteRecentAlertDialog(@NonNull final Context context) {
new AlertDialog.Builder(context)
.setMessage(getString(R.string.delete_recent_searches_dialog))
.setPositiveButton(android.R.string.yes,
(dialog, which) -> setDeleteRecentPositiveButton(context, dialog))
.setNegativeButton(android.R.string.no, null)
.setCancelable(false)
.create()
.show();
}
private void setDeleteRecentPositiveButton(@NonNull final Context context,
final DialogInterface dialog) {
recentSearchesDao.deleteAll();
if (binding != null) {
binding.recentSearchesDeleteButton.setVisibility(View.GONE);
binding.recentSearchesTextView.setText(R.string.no_recent_searches);
Toast.makeText(getContext(), getString(R.string.search_history_deleted),
Toast.LENGTH_SHORT).show();
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<>(context, R.layout.item_recent_searches,
recentSearches);
binding.recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
dialog.dismiss();
}
private void showDeleteAlertDialog(@NonNull final Context context, final int position) {
new AlertDialog.Builder(context)
.setMessage(R.string.delete_search_dialog)
.setPositiveButton(getString(R.string.delete).toUpperCase(Locale.ROOT),
((dialog, which) -> setDeletePositiveButton(context, dialog, position)))
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.create()
.show();
}
private void setDeletePositiveButton(@NonNull final Context context,
final DialogInterface dialog, final int position) {
recentSearchesDao.delete(recentSearchesDao.find(recentSearches.get(position)));
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<>(context, R.layout.item_recent_searches,
recentSearches);
if (binding != null){
binding.recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
dialog.dismiss();
}
/**
* This method is called on back press of activity so we are updating the list from database to
* refresh the recent searches list.
*/
@Override
public void onResume() {
updateRecentSearches();
super.onResume();
}
/**
* This method is called when search query is null to update Recent Searches
*/
public void updateRecentSearches() {
recentSearches = recentSearchesDao.recentSearches(10);
adapter.notifyDataSetChanged();
if (!recentSearches.isEmpty()) {
if (binding!= null) {
binding.recentSearchesDeleteButton.setVisibility(View.VISIBLE);
binding.recentSearchesTextView.setText(R.string.search_recent_header);
}
}
}
@Override
public void onDestroy() {
super.onDestroy();
if (binding != null) {
binding = null;
}
}
}

View file

@ -0,0 +1,153 @@
package fr.free.nrw.commons.explore.recentsearches
import android.content.Context
import android.content.DialogInterface
import android.content.DialogInterface.OnClickListener
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.AdapterView.OnItemClickListener
import android.widget.AdapterView.OnItemLongClickListener
import android.widget.ArrayAdapter
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.FragmentSearchHistoryBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
import fr.free.nrw.commons.explore.SearchActivity
import javax.inject.Inject
/**
* Displays the recent searches screen.
*/
class RecentSearchesFragment : CommonsDaggerSupportFragment() {
@JvmField
@Inject
var recentSearchesDao: RecentSearchesDao? = null
private var recentSearches: List<String> = emptyList()
private lateinit var adapter: ArrayAdapter<String>
private var binding: FragmentSearchHistoryBinding? = null
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
binding = FragmentSearchHistoryBinding.inflate(inflater, container, false)
recentSearches = recentSearchesDao!!.recentSearches(10)
if (recentSearches.isEmpty()) {
binding!!.recentSearchesDeleteButton.visibility = View.GONE
binding!!.recentSearchesTextView.setText(R.string.no_recent_searches)
}
binding!!.recentSearchesDeleteButton.setOnClickListener { v: View? ->
showDeleteRecentAlertDialog(requireContext())
}
adapter = ArrayAdapter(requireContext(), R.layout.item_recent_searches, recentSearches)
binding!!.recentSearchesList.adapter = adapter
binding!!.recentSearchesList.onItemClickListener =
OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
(context as SearchActivity).updateText(recentSearches[position])
}
binding!!.recentSearchesList.onItemLongClickListener =
OnItemLongClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
showDeleteAlertDialog(requireContext(), position)
true
}
updateRecentSearches()
return binding!!.root
}
private fun showDeleteRecentAlertDialog(context: Context) {
AlertDialog.Builder(context)
.setMessage(getString(R.string.delete_recent_searches_dialog))
.setPositiveButton(android.R.string.yes) { dialog: DialogInterface, _: Int ->
setDeleteRecentPositiveButton(context, dialog)
}
.setNegativeButton(android.R.string.no, null)
.setCancelable(false)
.create()
.show()
}
private fun setDeleteRecentPositiveButton(context: Context, dialog: DialogInterface) {
recentSearchesDao!!.deleteAll()
if (binding != null) {
binding!!.recentSearchesDeleteButton.visibility = View.GONE
binding!!.recentSearchesTextView.setText(R.string.no_recent_searches)
Toast.makeText(
getContext(), getString(R.string.search_history_deleted),
Toast.LENGTH_SHORT
).show()
recentSearches = recentSearchesDao!!.recentSearches(10)
adapter = ArrayAdapter(context, R.layout.item_recent_searches, recentSearches)
binding!!.recentSearchesList.adapter = adapter
adapter.notifyDataSetChanged()
}
dialog.dismiss()
}
private fun showDeleteAlertDialog(context: Context, position: Int) {
AlertDialog.Builder(context)
.setMessage(R.string.delete_search_dialog)
.setPositiveButton(
getString(R.string.delete).uppercase(),
{ dialog: DialogInterface, _: Int ->
setDeletePositiveButton(context, dialog, position)
}
)
.setNegativeButton(android.R.string.cancel, null)
.setCancelable(false)
.create()
.show()
}
private fun setDeletePositiveButton(context: Context, dialog: DialogInterface, position: Int) {
recentSearchesDao!!.delete(recentSearchesDao!!.find(recentSearches[position])!!)
recentSearches = recentSearchesDao!!.recentSearches(10)
adapter = ArrayAdapter(
context, R.layout.item_recent_searches,
recentSearches
)
if (binding != null) {
binding!!.recentSearchesList.adapter = adapter
adapter.notifyDataSetChanged()
}
dialog.dismiss()
}
/**
* This method is called on back press of activity so we are updating the list from database to
* refresh the recent searches list.
*/
override fun onResume() {
updateRecentSearches()
super.onResume()
}
/**
* This method is called when search query is null to update Recent Searches
*/
fun updateRecentSearches() {
recentSearches = recentSearchesDao!!.recentSearches(10)
adapter.notifyDataSetChanged()
if (recentSearches.isNotEmpty()) {
if (binding != null) {
binding!!.recentSearchesDeleteButton.visibility = View.VISIBLE
binding!!.recentSearchesTextView.setText(R.string.search_recent_header)
}
}
}
override fun onDestroy() {
super.onDestroy()
binding = null
}
}

View file

@ -0,0 +1,71 @@
package fr.free.nrw.commons.explore.recentsearches
import android.database.sqlite.SQLiteDatabase
/**
* This class contains the database table architechture for recent searches, It also contains
* queries and logic necessary to the create, update, delete this table.
*/
object RecentSearchesTable {
const val TABLE_NAME: String = "recent_searches"
const val COLUMN_ID: String = "_id"
const val COLUMN_NAME: String = "name"
const val COLUMN_LAST_USED: String = "last_used"
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
@JvmField
val ALL_FIELDS = arrayOf(
COLUMN_ID,
COLUMN_NAME,
COLUMN_LAST_USED,
)
const val DROP_TABLE_STATEMENT: String = "DROP TABLE IF EXISTS $TABLE_NAME"
const val CREATE_TABLE_STATEMENT: String = ("CREATE TABLE $TABLE_NAME ($COLUMN_ID INTEGER PRIMARY KEY,$COLUMN_NAME STRING,$COLUMN_LAST_USED INTEGER);")
/**
* This method creates a RecentSearchesTable in SQLiteDatabase
*
* @param db SQLiteDatabase
*/
fun onCreate(db: SQLiteDatabase) = db.execSQL(CREATE_TABLE_STATEMENT)
/**
* This method deletes RecentSearchesTable from SQLiteDatabase
*
* @param db SQLiteDatabase
*/
fun onDelete(db: SQLiteDatabase) {
db.execSQL(DROP_TABLE_STATEMENT)
onCreate(db)
}
/**
* This method is called on migrating from a older version to a newer version
*
* @param db SQLiteDatabase
* @param from Version from which we are migrating
* @param to Version to which we are migrating
*/
fun onUpdate(db: SQLiteDatabase, from: Int, to: Int) {
if (from == to) {
return
}
if (from < 6) {
// doesn't exist yet
onUpdate(db, from + 1, to)
return
}
if (from == 6) {
// table added in version 7
onCreate(db)
onUpdate(db, from + 1, to)
return
}
if (from == 7) {
onUpdate(db, from + 1, to)
return
}
}
}

View file

@ -320,12 +320,6 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
binding.seeMore.setUnderlinedText(R.string.nominated_see_more)
if (isCategoryImage) {
binding.authorLinearLayout.visibility = View.VISIBLE
} else {
binding.authorLinearLayout.visibility = View.GONE
}
if (!sessionManager.isUserLoggedIn) {
binding.categoryEditButton.visibility = View.GONE
binding.descriptionEdit.visibility = View.GONE
@ -814,10 +808,27 @@ class MediaDetailFragment : CommonsDaggerSupportFragment(), CategoryEditHelper.C
categoryNames.clear()
categoryNames.addAll(media.categories!!)
if (media.author == null || media.author == "") {
binding.authorLinearLayout.visibility = View.GONE
} else {
binding.mediaDetailAuthor.text = media.author
// Show author or uploader information for licensing compliance
val authorName = media.getAttributedAuthor()
val uploaderName = media.user
when {
!authorName.isNullOrEmpty() -> {
// Show author if available
binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_author)
binding.mediaDetailAuthor.text = authorName
binding.authorLinearLayout.visibility = View.VISIBLE
}
!uploaderName.isNullOrEmpty() -> {
// Show uploader as fallback
binding.mediaDetailAuthorLabel.text = getString(R.string.media_detail_uploader)
binding.mediaDetailAuthor.text = uploaderName
binding.authorLinearLayout.visibility = View.VISIBLE
}
else -> {
// Hide if neither author nor uploader is available
binding.authorLinearLayout.visibility = View.GONE
}
}
}

View file

@ -166,7 +166,7 @@ class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeLis
val mediaDetailFragment = adapter!!.currentMediaDetailFragment
when (item.itemId) {
R.id.menu_bookmark_current_image -> {
val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark)
val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark!!)
val snackbar = if (bookmarkExists) Snackbar.make(
requireView(),
R.string.add_bookmark,
@ -436,7 +436,7 @@ ${m.pageTitle.canonicalUri}"""
bookmark = Bookmark(
m.filename,
m.getAuthorOrUser(),
BookmarkPicturesContentProvider.uriForName(m.filename)
BookmarkPicturesContentProvider.uriForName(m.filename!!)
)
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image))
val contributionState = provider.getContributionStateAt(position)

View file

@ -114,13 +114,13 @@ class MoreBottomSheetFragment : BottomSheetDialogFragment() {
val level = store.getString("userAchievementsLevel", "0")
if (level == "0"){
binding?.moreProfile?.text = getString(
R.string.profileLevel,
R.string.profile_withoutLevel,
getUserName(),
getString(R.string.see_your_achievements) // Second argument
)
} else {
binding?.moreProfile?.text = getString(
R.string.profileLevel,
R.string.profile_withLevel,
getUserName(),
level
)

View file

@ -7,7 +7,8 @@ class NearbyResultItem(
private val wikipediaArticle: ResultTuple?,
private val commonsArticle: ResultTuple?,
private val location: ResultTuple?,
private val label: ResultTuple?,
@field:SerializedName("label") private val label: ResultTuple?,
@field:SerializedName("itemLabel") private val itemLabel: ResultTuple?,
@field:SerializedName("streetAddress") private val address: ResultTuple?,
private val icon: ResultTuple?,
@field:SerializedName("class") private val className: ResultTuple?,
@ -29,7 +30,15 @@ class NearbyResultItem(
fun getLocation(): ResultTuple = location ?: ResultTuple()
fun getLabel(): ResultTuple = label ?: ResultTuple()
/**
* Returns label for display (pins, popup), using fallback to itemLabel if needed.
*/
fun getLabel(): ResultTuple = label ?: itemLabel ?: ResultTuple()
/**
* Returns only the original label field, for Wikidata edits.
*/
fun getOriginalLabel(): ResultTuple = label ?: ResultTuple()
fun getIcon(): ResultTuple = icon ?: ResultTuple()

View file

@ -46,7 +46,7 @@ class ProfileActivity : BaseActivity() {
private var contributionsFragment: ContributionsFragment? = null
fun setScroll(canScroll: Boolean) {
binding.viewPager.setCanScroll(canScroll)
binding.viewPager.canScroll = canScroll
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {

View file

@ -311,7 +311,7 @@ class LeaderboardFragment : CommonsDaggerSupportFragment() {
}
private class SelectionListener(private val handler: () -> Unit): AdapterView.OnItemSelectedListener {
override fun onItemSelected(adapterView: AdapterView<*>?, view: View, i: Int, l: Long) =
override fun onItemSelected(adapterView: AdapterView<*>?, view: View?, i: Int, l: Long) =
handler()
override fun onNothingSelected(p0: AdapterView<*>?) = Unit

View file

@ -3,17 +3,13 @@ package fr.free.nrw.commons.recentlanguages
import android.content.ContentValues
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteQueryBuilder
import android.net.Uri
import android.text.TextUtils
import fr.free.nrw.commons.BuildConfig
import fr.free.nrw.commons.data.DBOpenHelper
import fr.free.nrw.commons.di.CommonsDaggerContentProvider
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.COLUMN_NAME
import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao.Table.TABLE_NAME
import javax.inject.Inject
import timber.log.Timber
import androidx.core.net.toUri
/**
@ -23,27 +19,17 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
companion object {
private const val BASE_PATH = "recent_languages"
val BASE_URI: Uri =
Uri.parse(
"content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH"
)
val BASE_URI: Uri = "content://${BuildConfig.RECENT_LANGUAGE_AUTHORITY}/$BASE_PATH".toUri()
/**
* Append language code to the base URI
* @param languageCode Code of a language
*/
@JvmStatic
fun uriForCode(languageCode: String): Uri {
return Uri.parse("$BASE_URI/$languageCode")
}
fun uriForCode(languageCode: String): Uri = "$BASE_URI/$languageCode".toUri()
}
@Inject
lateinit var dbOpenHelper: DBOpenHelper
override fun getType(uri: Uri): String? {
return null
}
override fun getType(uri: Uri): String? = null
/**
* Queries the SQLite database for the recently used languages
@ -60,11 +46,12 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
selectionArgs: Array<String>?,
sortOrder: String?
): Cursor? {
val queryBuilder = SQLiteQueryBuilder()
queryBuilder.tables = TABLE_NAME
val db = dbOpenHelper.readableDatabase
val queryBuilder = SQLiteQueryBuilder().apply {
tables = TABLE_NAME
}
val cursor = queryBuilder.query(
db,
requireDb(),
projection,
selection,
selectionArgs,
@ -72,7 +59,7 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
null,
sortOrder
)
cursor.setNotificationUri(context?.contentResolver, uri)
cursor.setNotificationUri(requireContext().contentResolver, uri)
return cursor
}
@ -89,12 +76,11 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
selection: String?,
selectionArgs: Array<String>?
): Int {
val sqlDB = dbOpenHelper.writableDatabase
val rowsUpdated: Int
if (selection.isNullOrEmpty()) {
val id = uri.lastPathSegment?.toInt()
?: throw IllegalArgumentException("Invalid URI: $uri")
rowsUpdated = sqlDB.update(
rowsUpdated = requireDb().update(
TABLE_NAME,
contentValues,
"$COLUMN_NAME = ?",
@ -104,7 +90,7 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
throw IllegalArgumentException("Parameter `selection` should be empty when updating an ID")
}
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return rowsUpdated
}
@ -114,14 +100,13 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
* @param contentValues : new values to be entered to the database
*/
override fun insert(uri: Uri, contentValues: ContentValues?): Uri? {
val sqlDB = dbOpenHelper.writableDatabase
val id = sqlDB.insert(
val id = requireDb().insert(
TABLE_NAME,
null,
contentValues
)
context?.contentResolver?.notifyChange(uri, null)
return Uri.parse("$BASE_URI/$id")
requireContext().contentResolver?.notifyChange(uri, null)
return "$BASE_URI/$id".toUri()
}
/**
@ -129,14 +114,12 @@ class RecentLanguagesContentProvider : CommonsDaggerContentProvider() {
* @param uri : contains the URI for recently used languages
*/
override fun delete(uri: Uri, s: String?, strings: Array<String>?): Int {
val db = dbOpenHelper.readableDatabase
Timber.d("Deleting recently used language %s", uri.lastPathSegment)
val rows = db.delete(
val rows = requireDb().delete(
TABLE_NAME,
"language_code = ?",
arrayOf(uri.lastPathSegment)
)
context?.contentResolver?.notifyChange(uri, null)
requireContext().contentResolver?.notifyChange(uri, null)
return rows
}
}

View file

@ -56,11 +56,7 @@ class UploadProgressActivity : BaseActivity() {
override fun onPageSelected(position: Int) {
updateMenuItems(position)
if (position == 2) {
binding.uploadProgressViewPager.setCanScroll(false)
} else {
binding.uploadProgressViewPager.setCanScroll(true)
}
binding.uploadProgressViewPager.canScroll = (position != 2)
}
override fun onPageScrollStateChanged(state: Int) {

View file

@ -821,6 +821,7 @@ class UploadMediaDetailFragment : UploadBaseFragment(), UploadMediaDetailsContra
{
showProgress(false)
uploadItem.imageQuality = IMAGE_OK
uploadItem.hasInvalidLocation = false // Reset invalid location flag when user confirms upload
},
{
presenterCallback!!.deletePictureAtIndex(index)

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.utils
import android.annotation.SuppressLint
import android.database.Cursor
fun Cursor.getStringArray(name: String): List<String> =
stringToArray(getString(name))
@SuppressLint("Range")
fun Cursor.getString(name: String): String =
getString(getColumnIndex(name))
@SuppressLint("Range")
fun Cursor.getInt(name: String): Int =
getInt(getColumnIndex(name))
@SuppressLint("Range")
fun Cursor.getLong(name: String): Long =
getLong(getColumnIndex(name))
/**
* Converts string to List
* @param listString comma separated single string from of list items
* @return List of string
*/
fun stringToArray(listString: String?): List<String> {
if (listString.isNullOrEmpty()) return emptyList();
val elements = listString.split(",".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
return listOf(*elements)
}
/**
* Converts string to List
* @param list list of items
* @return string comma separated single string of items
*/
fun arrayToString(list: List<String?>?): String? {
return list?.joinToString(",")
}

View file

@ -125,7 +125,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:imeOptions="flagNoExtractUi"
android:imeOptions="actionNext"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
@ -148,9 +148,9 @@
android:id="@+id/login_two_factor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLines="1"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -128,7 +128,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:imeOptions="flagNoExtractUi"
android:imeOptions="actionNext"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
@ -151,9 +151,9 @@
android:id="@+id/login_two_factor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/_2fa_code"
android:imeOptions="flagNoExtractUi"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLines="1"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -131,7 +131,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/password"
android:imeOptions="flagNoExtractUi"
android:imeOptions="actionNext"
android:inputType="textPassword" />
</com.google.android.material.textfield.TextInputLayout>
@ -155,7 +155,9 @@
android:id="@+id/login_two_factor"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="flagNoExtractUi"
android:imeOptions="actionDone"
android:inputType="number"
android:maxLines="1"
android:visibility="gone"
tools:visibility="visible" />

View file

@ -125,27 +125,6 @@
</LinearLayout>
<LinearLayout
style="@style/MediaDetailContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/authorLinearLayout"
android:orientation="horizontal">
<TextView
style="@style/MediaDetailTextLabelGeneric"
android:layout_width="@dimen/widget_margin"
android:layout_height="match_parent"
android:text="@string/media_detail_author" />
<TextView
style="@style/MediaDetailTextBody"
android:id="@+id/mediaDetailAuthor"
android:layout_width="@dimen/widget_margin"
android:layout_height="match_parent"
tools:text="Media author user name goes here." />
</LinearLayout>
<LinearLayout
android:id="@+id/caption_layout"
style="@style/MediaDetailContainer"
@ -263,6 +242,28 @@
tools:text="License link" />
</LinearLayout>
<LinearLayout
style="@style/MediaDetailContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/authorLinearLayout"
android:orientation="horizontal">
<TextView
android:id="@+id/mediaDetailAuthorLabel"
style="@style/MediaDetailTextLabelGeneric"
android:layout_width="@dimen/widget_margin"
android:layout_height="match_parent"
android:text="@string/media_detail_author" />
<TextView
style="@style/MediaDetailTextBody"
android:id="@+id/mediaDetailAuthor"
android:layout_width="@dimen/widget_margin"
android:layout_height="match_parent"
tools:text="Media author user name goes here." />
</LinearLayout>
<LinearLayout
style="@style/MediaDetailContainer"
android:layout_width="match_parent"

View file

@ -268,6 +268,7 @@
<string name="media_detail_description">الوصف</string>
<string name="media_detail_discussion">نقاش</string>
<string name="media_detail_author">المؤلف</string>
<string name="media_detail_uploader">الرافع</string>
<string name="media_detail_uploaded_date">تاريخ الرفع</string>
<string name="media_detail_license">الترخيص</string>
<string name="media_detail_coordinates">الإحداثيات</string>
@ -426,7 +427,7 @@
<string name="statistics_featured">الصور المختارة</string>
<string name="statistics_wikidata_edits">صور عبر \"الأماكن المجاورة\"</string>
<string name="level">المستوى %d</string>
<string name="profileLevel">%s (المستوى %s)</string>
<string name="profile_withLevel">%s (المستوى %s)</string>
<string name="images_uploaded">الصور المرفوعة</string>
<string name="image_reverts">لم يتم إرجاع الصور</string>
<string name="images_used_by_wiki">الصور المستخدمة</string>
@ -822,7 +823,7 @@
<string name="permissions_are_required_for_functionality">الإذن مطلوب لهذه الوظيفة</string>
<string name="learn_how_to_write_a_useful_description">تعلم كيفية كتابة وصف مفيد</string>
<string name="learn_how_to_write_a_useful_caption">تعلم كيفية كتابة تعليق مفيد</string>
<string name="see_your_achievements">شاهد إنجازاتك</string>
<string name="see_your_achievements">عرض إنجازاتك</string>
<string name="edit_image">تعديل الصورة</string>
<string name="edit_location">تعديل الموقع</string>
<string name="location_updated">تم تحديث الموقع!</string>

View file

@ -370,7 +370,7 @@
<string name="statistics_featured">Seçilmiş şəkillər</string>
<string name="statistics_wikidata_edits">\"Yaxınlıqdakı yerlər\" vasitəsilə şəkillər</string>
<string name="level">Səviyyə %d</string>
<string name="profileLevel">%s (Səviyyə %s)</string>
<string name="profile_withLevel">%s (Səviyyə %s)</string>
<string name="images_uploaded">Yüklənən şəkillər</string>
<string name="image_reverts">Geri qaytarılan şəkillər</string>
<string name="images_used_by_wiki">İstifadə olunan şəkillər</string>

View file

@ -0,0 +1,233 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Blackisnewyellow
* Mansur
* Ерней
* Ильгиз
* Ильнар
-->
<resources>
<string name="commons_facebook">Викиҗыентыкның Facebook бите</string>
<string name="commons_github">Викиҗыентыкның гитхабтагы башлангыч кодлары</string>
<string name="commons_logo">Викиҗыентык логотипы</string>
<string name="commons_website">Викиҗыентыкның веб-сайты</string>
<string name="exit_location_picker">Урынны сайлау тәрәзәсеннән чыгарга</string>
<string name="submit">Сакларга</string>
<string name="add_another_description">Башка тасвирлама өстәргә</string>
<string name="add_new_contribution">Яңа кертем өстәргә</string>
<string name="add_contribution_from_camera">Камерадан яңа кертем өстәргә</string>
<string name="add_contribution_from_photos">Камерадан яңа кертем өстәргә</string>
<string name="add_contribution_from_contributions_gallery">Алдагы кертемнәр галереясыннан фото өстәргә</string>
<string name="show_captions">Язмалар</string>
<string name="row_item_language_description">Тел тасвирламасы</string>
<string name="row_item_caption">Язма</string>
<string name="show_captions_description">Тасвирлама</string>
<string name="nearby_row_image">Сурәт</string>
<string name="nearby_all">Барысы</string>
<string name="nearby_filter_toggle">Күчертергә</string>
<string name="nearby_filter_search">Күренешне эзлә</string>
<string name="nearby_filter_state">Урын халәте</string>
<string name="appwidget_img">Көн сурәте</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">%1$d файл йөкләнә</item>
<item quantity="few">%1$d файл йөкләнә</item>
<item quantity="many">%1$d файл йөкләнә</item>
<item quantity="other">%1$d файл йөкләнә</item>
</plurals>
<plurals name="contributions_subtitle">
<item quantity="one">(%1$d)</item>
<item quantity="few">(%1$d)</item>
<item quantity="many">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<string name="starting_uploads">Йөкләү башлана</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">%d йөкләүне эшкәртү</item>
<item quantity="few">%d йөкләүне эшкәртү</item>
<item quantity="many">%d йөкләүне эшкәртү</item>
<item quantity="other">%d йөкләүне эшкәртү</item>
</plurals>
<plurals name="multiple_uploads_title">
<item quantity="one">%d йөкләү</item>
<item quantity="few">%d йөкләү</item>
<item quantity="many">%d йөкләү</item>
<item quantity="other">%d йөкләү</item>
</plurals>
<string name="navigation_item_explore">Тикшерергә</string>
<string name="preference_category_appearance">Күренеш</string>
<string name="preference_category_general">Гомуми</string>
<string name="preference_category_feedback">Кире элемтә</string>
<string name="preference_category_privacy">Шәхсилек</string>
<string name="app_name">Викиҗыентык</string>
<string name="menu_settings">Көйләнмәләр</string>
<string name="intent_share_upload_label">Викиҗыентыкка йөкләргә</string>
<string name="upload_in_progress">Йөкләү бара...</string>
<string name="username">Кулланучы исеме</string>
<string name="password">Серсүз</string>
<string name="login_credential">Commons Beta хисапъязмагызга керегез</string>
<string name="login">Керү</string>
<string name="forgot_password">Серсүзне оныттыгызмы?</string>
<string name="signup">Теркәлү</string>
<string name="logging_in_title">Керү бара…</string>
<string name="logging_in_message">Бераз көтегезче...</string>
<string name="updating_caption_title">Язмалар һәм тасвирламалар яңартыла</string>
<string name="updating_caption_message">Бераз көтегезче...</string>
<string name="login_success">Керү уңышлы башкарылды!</string>
<string name="login_failed">Системага кереп булмады!</string>
<string name="upload_failed">Файл табылмады. Башка файлны кулланып карагызчы.</string>
<string name="retry_limit_reached">Кабатлаулар саны нык артып китте! Йөкләүне кире кагыгыз яки яңадан кабатлагыз.</string>
<string name="unrestricted_battery_mode">Батәринең оптималь кулланылышын сүндерергәме?</string>
<string name="uploading_started">Йөкләү башланды!</string>
<string name="upload_completed_notification_title">%1$s төялде!</string>
<string name="upload_completed_notification_text">Төялгән файлыгызны карау өчен басыгыз</string>
<string name="upload_progress_notification_title_start">Файлны төяү: %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="upload_failed_notification_title">%1$s йөкләнә алмады</string>
<string name="upload_paused_notification_title">%1$s йөкләнүе туктатылып калды</string>
<string name="upload_failed_notification_subtitle">Карау өчен басыгыз</string>
<string name="upload_paused_notification_subtitle">Карау өчен басыгыз</string>
<string name="title_activity_contributions">Минем соңгы төяүләрем</string>
<string name="contribution_state_failed">Төяү хатасы</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_copy_link">Сылтаманы күчереп ал</string>
<string name="menu_link_copied">Сылтама алмашу буферына күчереп алынды</string>
<string name="menu_share">Уртаклашырга</string>
<string name="menu_view_file_page">Файл битен күрсәтергә</string>
<string name="share_title_hint">Язма (Зарур)</string>
<string name="add_caption_toast">Бу файлның исемен билгеләгезче</string>
<string name="share_description_hint">Тасвирлама</string>
<string name="share_caption_hint">Язма</string>
<string name="login_failed_network">Кереп булмый - челтәр хатасы</string>
<string name="login_failed_blocked">Гафу итегез, мондый исемле кулланучы Викиҗыентыкта блокланган булган.</string>
<string name="login_failed_generic">Системага кереп булмады!</string>
<string name="share_upload_button">Төяү</string>
<string name="multiple_share_base_title">Бу файллар төркеме өчен исемне кертегез</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="display_list_button">Исемлек</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="title_activity_featured_images">Сакланган сурәтләр</string>
<string name="title_activity_custom_selector">Кулланучы селекторы</string>
<string name="title_activity_category_details">Төркем</string>
<string name="title_activity_review">Тикшер</string>
<string name="menu_about">Кушымта турында</string>
<string name="about_privacy_policy">Яшеренлек сәясәте</string>
<string name="about_credits">Төзүчеләр</string>
<string name="title_activity_about">Кушымта турында</string>
<string name="no_email_client">Почта клиенты урнаштырылмаган</string>
<string name="provider_categories">Күптән түгел кулланылган төркемнәр</string>
<string name="waiting_first_sync">Беренче синхронлаштыруны көтү...</string>
<string name="no_uploads_yet">Сез әле бер сурәтне дә төямәдегез.</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="use_previous">Алдагы исемне һәм тамвирламаны куллан</string>
<string name="preference_theme">Күренеш</string>
<string name="license_name_cc_by_sa_four"> Attribution-ShareAlike 4.0</string>
<string name="license_name_cc_by_four"> Attribution 4.0</string>
<string name="license_name_cc_by_sa"> Attribution-ShareAlike 3.0</string>
<string name="license_name_cc_by"> Attribution 3.0</string>
<string name="tutorial_1_text">Викиҗыентыктагы сурәтләр Википедиянең күпчелек күләмендә кулланыла.</string>
<string name="tutorial_1_subtext">Төягән сурәтләрегез бөтен дөньядагы кешеләргә белем алырга ярдәм итә ала!</string>
<string name="tutorial_2_text">Зинһар, бары тик үзегез ясаган яки төшергән сурәтләрне генә төягез:</string>
<string name="tutorial_2_subtext_1">Табигать объектлары (мәсәлән, чәчәкләр, хайваннар, таулар)</string>
<string name="tutorial_2_subtext_2">Файдалы җисемнәр (мәсәлән, велосипедлар, вокзаллар)</string>
<string name="tutorial_2_subtext_3">Билгеле кешеләр (мәсәлән, мэрыгыз, сез очраткан олимпияче-спортсменнар)</string>
<string name="tutorial_3_text">Зинһар, боларны ТӨЯМӘГЕЗ:</string>
<string name="tutorial_3_subtext_1">Селфилар яки дусларыгызның фотолары</string>
<string name="tutorial_3_subtext_2">Интернеттан иңләгән фотолар</string>
<string name="tutorial_3_subtext_3">Ирекле булмаган программаларның скриншотлары</string>
<string name="tutorial_4_text">Төяү мисалы:</string>
<string name="tutorial_4_subtext_1">Атамасы: Сидней опера театры</string>
<string name="welcome_wikipedia_text">Сурәтләрегезне төягез. Википедия мәкаләләрен кызыклырак ясарга булышыгыз!</string>
<string name="welcome_wikipedia_subtext">Википедиядә кулланылучы сурәтләр Викиҗыентыкта саклана.</string>
<string name="welcome_copyright_text">Төягән сурәтләрегез бөтен дөньядагы кешеләргә белем алырга ярдәм итә ала.</string>
<string name="welcome_copyright_subtext">Авторлык хокуклары белән сакланган материалларны, мәсәлән, Интернетта табылган плакатлар, китап тышлары һ. б. ш. сурәтләрне кулланмаска тырышыгыз.</string>
<string name="welcome_final_text">Бу сезгә аңлашыламы?</string>
<string name="welcome_final_button_text">Әйе!</string>
<string name="welcome_help_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_caption_empty">Язмасыз</string>
<string name="detail_description_empty">Тасвирламасыз</string>
<string name="detail_discussion_empty">Фикерләшү юк</string>
<string name="detail_license_empty">Билгесез лицензия</string>
<string name="menu_refresh">Яңарту</string>
<string name="storage_permission_title">Тышкы саклагычны куллану рөхсәтен сорау</string>
<string name="location_permission_title">Локациягезне табуны сорау</string>
<string name="in_app_camera_location_permission_title">Кушымтада ясалган фотолар өчен локацияне яздыр</string>
<string name="ok">Ярар</string>
<string name="warning">Игътибар</string>
<string name="duplicate_file_name">Кабатлана торган файл исеме табылды</string>
<string name="upload">Төя</string>
<string name="yes">Әйе</string>
<string name="no">Юк</string>
<string name="media_detail_caption">Язма</string>
<string name="media_detail_title">Исем</string>
<string name="media_detail_depiction">Тасвирланган феномен</string>
<string name="media_detail_description">Тасвирлама</string>
<string name="media_detail_discussion">Фикер алышу</string>
<string name="media_detail_author">Автор</string>
<string name="media_detail_uploader">Төяп куючы</string>
<string name="media_detail_uploaded_date">Төяү вакыты</string>
<string name="media_detail_license">Лицензия</string>
<string name="media_detail_coordinates">Координатлар</string>
<string name="media_detail_coordinates_empty">Билгеләнмәгән</string>
<string name="become_a_tester_title">Бета-тестерга әйләнергә</string>
<string name="logout_verification">Сез чыннан да чыгарга телисезме?</string>
<string name="no_subcategory_found">Астөркемнәр табылмады.</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_feedback_github">GitHub аркылы кире элемтә</string>
<string name="navigation_item_logout">Чыгарга</string>
<string name="navigation_item_info">Кулланма</string>
<string name="navigation_item_notification">Белдермәләр</string>
<string name="navigation_item_review">Тикшерү</string>
<string name="no_description_found">тасвирлама табылмады</string>
<string name="nearby_info_menu_commons_article">Файлның Викиҗыентыктагы бите</string>
<string name="nearby_info_menu_wikidata_article">Викимәгълүмат элементы</string>
<string name="nearby_info_menu_wikipedia_article">Википедия мәкаләсе</string>
<string name="upload_problem_image_dark">Сурәт артык караңгы.</string>
<string name="upload_problem_image_duplicate">Бу сурәт Викиҗыентыкта бар инде.</string>
<string name="upload_problem_different_geolocation">Бу сурәт башка урында ясалган булган.</string>
<string name="upload_problem_do_you_continue">Барыбер бу сурәтне төяргә телисезме?</string>
<string name="upload_connection_error_alert_title">Тоташтыру хатасы</string>
<string name="upload_problem_image">Сурәттә читенлекләр табылды</string>
<string name="use_external_storage">Кушымтада ясалган фотоларны сакла</string>
<string name="use_external_storage_summary">Җайланма камерасы ярдәмендә эшләнгән фотоларны җайланмада сакла</string>
<string name="null_url">Хата! Сылтама табылмады</string>
<string name="nominate_deletion">Бетерергә тәкъдим ит</string>
<string name="nominated_for_deletion">Бу сурәтне бетерергә тәкъдим ителде.</string>
<string name="nominated_see_more">Күбрәк мәгълүмат өчен битне карагыз</string>
<string name="skip_login">Калдырып үт</string>
<string name="navigation_item_login">Керү</string>
<string name="wikicode_copied">Викитекст алмашу буферына күчереп алынды</string>
<string name="nearby_directions">Юнәлешләр</string>
<string name="nearby_wikidata">Викимәгълүмат</string>
<string name="nearby_wikipedia">Википедия</string>
<string name="nearby_commons">Викиҗыентык</string>
<string name="about_rate_us">Безне бәяләгез</string>
<string name="about_faq">Еш бирелгән сораулар (ЕБС, ЧаВо)</string>
<string name="rotate">Бору</string>
<string name="storage_permissions_denied">Саклауга рөхсәтләр кире кагылды</string>
<string name="unable_to_share_upload_item">Бу объект белән уртаклашу мөмкин түгел</string>
</resources>

View file

@ -23,7 +23,7 @@
<string name="nearby_all">Makejang</string>
<string name="nearby_filter_toggle">Alih Duur</string>
<string name="nearby_filter_search">Cingakan Panyelehan</string>
<string name="nearby_filter_state">Genah Negara</string>
<string name="nearby_filter_state">Genah Pernyataan</string>
<string name="appwidget_img">Gambar rahina mangkin</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">%1$d berkas kaunggah</item>
@ -270,7 +270,7 @@
<string name="statistics">Statistik</string>
<string name="statistics_thanks">Haturan Suksma Katampi</string>
<string name="statistics_featured">Gambar Pilihan</string>
<string name="level" fuzzy="true">Tingkat</string>
<string name="level">Tingkat %d</string>
<string name="images_uploaded">Gambar Kaupload</string>
<string name="images_used_by_wiki">Gambar Kaanggén</string>
<string name="contributions_fragment">Pituut</string>

View file

@ -271,6 +271,7 @@
<string name="preference_author_name_toggle_summary">При качването използвайте персонализирано авторско име вместо потребителското си име</string>
<string name="preference_author_name">Персонализирано авторско име</string>
<string name="nearby_fragment">Наблизо</string>
<string name="notifications">Известия</string>
<string name="read_notifications">Известия (прочетени)</string>
<string name="list_sheet">Списък</string>
<string name="next">Следваща</string>

View file

@ -372,7 +372,7 @@
<string name="statistics_featured">নির্বাচিত ছবি</string>
<string name="statistics_wikidata_edits">\"কাছাকাছি স্থান\" এর মাধ্যমে ছবি</string>
<string name="level">স্তর %d</string>
<string name="profileLevel">%s (স্তর %s )</string>
<string name="profile_withLevel">%s (স্তর %s )</string>
<string name="images_uploaded">আপলোডকৃত চিত্র</string>
<string name="image_reverts">ছবিগুলো প্রত্যাবর্তন করা হয়নি</string>
<string name="images_used_by_wiki">ব্যবহৃত ছবি</string>

View file

@ -446,7 +446,7 @@
<string name="android_version">Stumm Android</string>
<string name="network_type">Seurt rouedad</string>
<string name="report_user">Disklêriañ an implijer/ez-mañ</string>
<string name="see_your_achievements">Gwelet ho taolioù-kaer</string>
<string name="see_your_achievements" fuzzy="true">Gwelet ho taolioù-kaer</string>
<string name="edit_image">Kemmañ ar skeudenn</string>
<string name="edit_location">Kemmañ al lec\'hiadur</string>
<string name="location_updated">Lec\'hiadur hizivaet!</string>

View file

@ -114,7 +114,7 @@
<string name="login_failed_2fa_needed">Ахь шинафакторийн аутентификацин код йазо йеза</string>
<string name="login_failed_generic">Системин довзийтарца гӀалат!</string>
<string name="share_upload_button">Чуйолуш йу</string>
<string name="multiple_share_base_title">ДӀайазйе хӀокху файлийн тобан цӀе</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>
@ -379,7 +379,7 @@
<string name="wikipedia_instructions_step_3">3. Хьайн суьртана догӀу йаззаман дакъа лаха</string>
<string name="wikipedia_instructions_step_4">4. «Хийца» иконкин тӀетаӀайе (къоламах тера йу) хӀокху декъана</string>
<string name="wikipedia_instructions_step_5">5. Вики-код йогӀучу метте дӀайазйе</string>
<string name="wikipedia_instructions_step_7">7. Йаззам дӀайазбе</string>
<string name="wikipedia_instructions_step_7">7. Йаззам дӀайазбан</string>
<string name="copy_wikicode_to_clipboard">Вики-код буфер чу копийе</string>
<string name="pause">пауза</string>
<string name="resume">кхидӀа</string>

View file

@ -19,6 +19,7 @@
* Patriccck
* Patrik L.
* Robins7
* Segoulas
* Spotter
* The astrea
* Vlad5250
@ -247,6 +248,7 @@
<string name="media_detail_description">Popis</string>
<string name="media_detail_discussion">Diskuse</string>
<string name="media_detail_author">Autor</string>
<string name="media_detail_uploader">Nahrávač</string>
<string name="media_detail_uploaded_date">Datum nahrání souboru</string>
<string name="media_detail_license">Licence</string>
<string name="media_detail_coordinates">Souřadnice</string>
@ -405,7 +407,7 @@
<string name="statistics_featured">Nejlepší obrázky</string>
<string name="statistics_wikidata_edits">Obrázky přes „Místa v okolí“</string>
<string name="level">Úroveň %d</string>
<string name="profileLevel">%s (úroveň %s)</string>
<string name="profile_withLevel">%s (úroveň %s)</string>
<string name="images_uploaded">Nahrané obrázky</string>
<string name="image_reverts">Nerevertované obrázky</string>
<string name="images_used_by_wiki">Použitých obrázků</string>
@ -448,7 +450,7 @@
<string name="deletion_reason_bad_for_my_privacy">Uvědomil/a jsem si, že je to špatné pro mé soukromí</string>
<string name="deletion_reason_no_longer_want_public">Změnil/a jsem názor, nechci, aby to bylo veřejně viditelné</string>
<string name="deletion_reason_not_interesting">Omlouváme se, že tento obrázek není zajímavý pro encyklopedii</string>
<string name="uploaded_by_myself" fuzzy="true">Náhráno mnou %1$s, použito v(e) %2$d článku/článcích.</string>
<string name="uploaded_by_myself">Nahráno mnou %1$s, použito v alespoň %2$d článku/článcích.</string>
<string name="no_uploads">Vítejte na Commons!\n\nNahrajte svá první média klepnutím na tlačítko přidat.</string>
<string name="no_categories_selected">Nebyly vybrány žádné kategorie</string>
<string name="no_categories_selected_warning_desc">Obrázky bez kategorií jsou používány jen zřídka. Opravdu chcete nahrát obrázek bez výběru kategorií?</string>
@ -617,6 +619,8 @@
<string name="title_for_media">MÉDIA</string>
<string name="title_for_child_classes">PODŘAZENÉ TŘÍDY</string>
<string name="title_for_parent_classes">NADŘAZENÉ TŘÍDY</string>
<string name="title_for_subcategories">PODKATEGORIE</string>
<string name="title_for_parent_categories">NADŘAZENÉ KATEGORIE</string>
<string name="upload_nearby_place_found_title">Místo v okolí nalezeno</string>
<string name="upload_nearby_place_found_description_plural">Je na těchto obrázcích %1$s?</string>
<string name="upload_nearby_place_found_description_singular">Je toto obrázek místa %1$s?</string>
@ -792,7 +796,7 @@
<string name="permissions_are_required_for_functionality">Pro funkčnost jsou vyžadována oprávnění</string>
<string name="learn_how_to_write_a_useful_description">Naučte se, jak psát užitečný popis</string>
<string name="learn_how_to_write_a_useful_caption">Naučte se, jak psát užitečný popis</string>
<string name="see_your_achievements">Podívejte se na své úspěchy</string>
<string name="see_your_achievements">Zhlédněte své úspěchy</string>
<string name="edit_image">Upravit obrázek</string>
<string name="edit_location">Upravit polohu</string>
<string name="location_updated">Poloha aktualizována!</string>
@ -824,6 +828,7 @@
<string name="talk">Diskuze</string>
<string name="write_something_about_the_item">Napište komentář o položce „%1$s“. Bude veřejně viditelný.</string>
<string name="does_not_exist_anymore_no_picture_can_ever_be_taken_of_it">„%1$s“ již neexistuje, nelze z něj již tedy pořídit obrázek.</string>
<string name="is_at_a_different_place_wikidata">\'%1$s\' je na odlišném místě.</string>
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">„%1$s“ je na jiném místě. Zadejte prosím správné místo, a pokud je to možné, napište správnou zeměpisnou šířku a délku.</string>
<string name="other_problem_or_information_please_explain_below">Jiný problém nebo informace (vysvětlete prosím níže).</string>
<string name="feedback_destination_note">Vaše zpětná vazba bude zveřejněna na následující stránce wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
@ -862,4 +867,5 @@
<string name="show_in_nearby">Zobrazit v kartě Poblíž</string>
<string name="image_tag_line_created_and_uploaded_by">Vytvořil/a a nahrál/a: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Vytvořeno uživatelem %1$s a nahráno uživatelem %2$s</string>
<string name="nominated_for_deletion_btn">Nominováno na smazání</string>
</resources>

View file

@ -218,6 +218,7 @@
<string name="media_detail_description">Beskrivelse</string>
<string name="media_detail_discussion">Diskussion</string>
<string name="media_detail_author">Forfatter</string>
<string name="media_detail_uploader">Uploader</string>
<string name="media_detail_uploaded_date">Upload-dato</string>
<string name="media_detail_license">Licens</string>
<string name="media_detail_coordinates">Koordinater</string>
@ -376,7 +377,7 @@
<string name="statistics_featured">Udvalgte billeder</string>
<string name="statistics_wikidata_edits">Billeder via \"Steder i nærheden\"</string>
<string name="level">Niveau %d</string>
<string name="profileLevel">%s (Niveau %s)</string>
<string name="profile_withLevel">%s (Niveau %s)</string>
<string name="images_uploaded">Uploadede billeder</string>
<string name="image_reverts">Billeder, som ikke er blevet trukket tilbage</string>
<string name="images_used_by_wiki">Billeder brugt</string>

View file

@ -411,7 +411,7 @@
<string name="statistics_featured">Vorgestellte Bilder</string>
<string name="statistics_wikidata_edits">Bilder über „Orte in der Nähe“</string>
<string name="level">Level %d</string>
<string name="profileLevel">%s (Level %s)</string>
<string name="profile_withLevel">%s (Level %s)</string>
<string name="images_uploaded">Hochgeladene Bilder</string>
<string name="image_reverts">Bilder nicht zurückgesetzt</string>
<string name="images_used_by_wiki">Verwendete Bilder</string>
@ -793,7 +793,7 @@
<string name="permissions_are_required_for_functionality">Für die Funktionalität sind Berechtigungen erforderlich</string>
<string name="learn_how_to_write_a_useful_description">Erfahre, wie man eine nützliche Beschreibung schreibt</string>
<string name="learn_how_to_write_a_useful_caption">Erfahre, wie man eine nützliche Überschrift schreibt</string>
<string name="see_your_achievements">Deine Erfolge ansehen</string>
<string name="see_your_achievements" fuzzy="true">Deine Erfolge ansehen</string>
<string name="edit_image">Bild bearbeiten</string>
<string name="edit_location">Standort bearbeiten</string>
<string name="location_updated">Standort aktualisiert!</string>

View file

@ -390,7 +390,7 @@
<string name="statistics_featured">Προβεβλημμένες εικόνες</string>
<string name="statistics_wikidata_edits">Εικόνες μέσω «Κοντινά μέρη»</string>
<string name="level">Επίπεδο %d</string>
<string name="profileLevel">%s (Επίπεδο %s)</string>
<string name="profile_withLevel">%s (Επίπεδο %s)</string>
<string name="images_uploaded">Εικόνες που μεταφορτώθηκαν</string>
<string name="image_reverts">Εικόνες που δεν ανεστράφησαν</string>
<string name="images_used_by_wiki">Εικόνες που χρησιμοποιήθηκαν</string>
@ -773,7 +773,7 @@
<string name="permissions_are_required_for_functionality">Απαιτούνται δικαιώματα για τη λειτουργικότητα</string>
<string name="learn_how_to_write_a_useful_description">Μάθετε πώς να γράψετε μια χρήσιμη περιγραφή</string>
<string name="learn_how_to_write_a_useful_caption">Μάθετε πώς να γράψετε μια χρήσιμη λεζάντα</string>
<string name="see_your_achievements">Δείτε τα επιτεύγματά σας</string>
<string name="see_your_achievements" fuzzy="true">Δείτε τα επιτεύγματά σας</string>
<string name="edit_image">Επεξεργασία εικόνας</string>
<string name="edit_location">Επεξεργασία τοποθεσίας</string>
<string name="location_updated">Η τοποθεσία ενημερώθηκε!</string>

View file

@ -263,6 +263,7 @@
<string name="media_detail_description">Descripción</string>
<string name="media_detail_discussion">Discusión</string>
<string name="media_detail_author">Autor</string>
<string name="media_detail_uploader">Subidor</string>
<string name="media_detail_uploaded_date">Fecha de subida</string>
<string name="media_detail_license">Licencia</string>
<string name="media_detail_coordinates">Coordenadas</string>
@ -421,7 +422,7 @@
<string name="statistics_featured">Imágenes destacadas</string>
<string name="statistics_wikidata_edits">Imágenes vía \"Sitios Cercanos\"</string>
<string name="level">Nivel %d</string>
<string name="profileLevel">%s (Nivel %s)</string>
<string name="profile_withLevel">%s (Nivel %s)</string>
<string name="images_uploaded">Imágenes subidas</string>
<string name="image_reverts">Imágenes no revertidas</string>
<string name="images_used_by_wiki">Imágenes utilizadas</string>
@ -464,7 +465,7 @@
<string name="deletion_reason_bad_for_my_privacy">Me di cuenta que es malo para mi privacidad</string>
<string name="deletion_reason_no_longer_want_public">Cambié de opinión, no quiero que siga siendo visible públicamente</string>
<string name="deletion_reason_not_interesting">Lo sentimos, esta imagen no es interesante para una enciclopedia</string>
<string name="uploaded_by_myself" fuzzy="true">Subida por mí mismo el %1$s, usado en %2$d artículo(s).</string>
<string name="uploaded_by_myself">Subido por mí en %1$s, usado en %2$d artículo(s) al menos.</string>
<string name="no_uploads">Te damos la bienvenida a Commons.\n\nCarga tu primer archivo mediante el botón Añadir.</string>
<string name="no_categories_selected">No hay categorías seleccionadas</string>
<string name="no_categories_selected_warning_desc">Las imágenes sin categorías raramente se pueden usar. ¿Seguro que quieres continuar sin seleccionar ninguna categoría?</string>
@ -629,6 +630,8 @@
<string name="title_for_media">MULTIMEDIA</string>
<string name="title_for_child_classes">CLASES HIJAS</string>
<string name="title_for_parent_classes">CLASES PADRES</string>
<string name="title_for_subcategories">SUBCATEGORÍAS</string>
<string name="title_for_parent_categories">CATEGORÍAS DE PADRES</string>
<string name="upload_nearby_place_found_title">Lugar cercano encontrado</string>
<string name="upload_nearby_place_found_description_plural">¿Son estas imágenes de %1$s?</string>
<string name="upload_nearby_place_found_description_singular">¿Es esta una imagen de %1$s?</string>
@ -804,7 +807,7 @@
<string name="permissions_are_required_for_functionality">Se requieren permisos para la funcionalidad</string>
<string name="learn_how_to_write_a_useful_description">Aprenda a escribir una descripción útil</string>
<string name="learn_how_to_write_a_useful_caption">Aprenda a escribir una leyenda útil</string>
<string name="see_your_achievements">Ver sus logros</string>
<string name="see_your_achievements">Ver tus logros</string>
<string name="edit_image">Editar Imagen</string>
<string name="edit_location">Editar Ubicación</string>
<string name="location_updated">¡Ubicación actualizada!</string>
@ -873,4 +876,5 @@
<string name="show_in_nearby">Mostrar en las cercanías</string>
<string name="image_tag_line_created_and_uploaded_by">Creado y cargado por: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Creado por %1$s y cargado por %2$s</string>
<string name="nominated_for_deletion_btn">Nominar para borrado</string>
</resources>

View file

@ -30,6 +30,9 @@
<string name="exit_location_picker">Poistu sijainnin valitsimesta</string>
<string name="submit">Lähetä</string>
<string name="add_another_description">Lisää toinen kuvaus</string>
<string name="add_new_contribution">Lisää uusi tiedosto</string>
<string name="add_contribution_from_camera">Lisää uusi tiedosto kameralla</string>
<string name="add_contribution_from_photos">Lisää uusi tiedosto kuvista</string>
<string name="show_captions">Kuvatekstit</string>
<string name="row_item_language_description">Kielen kuvaus</string>
<string name="row_item_caption">Kuvateksti</string>
@ -45,7 +48,7 @@
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<string name="starting_uploads">Aloitetaan latauksia</string>
<string name="starting_uploads">Aloitetaan tallennuksia</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">Käsitellään %d tallennus</item>
<item quantity="other">Käsitellään %d tallennusta</item>
@ -87,7 +90,7 @@
<string name="unrestricted_battery_mode">Poistetaanko akun optimointi käytöstä?</string>
<string name="authentication_failed">Tunnistautuminen epäonnistui, kirjaudu uudelleen sisään</string>
<string name="uploading_started">Tallentaminen aloitettiin!</string>
<string name="uploading_queued">Lataus jonossa (rajoitettu yhteystila käytössä)</string>
<string name="uploading_queued">Tallennus on jonossa (rajoitettu yhteystila käytössä)</string>
<string name="upload_completed_notification_title">%1$s tallennettiin!</string>
<string name="upload_completed_notification_text">Napauta katsoaksesi tallennusta</string>
<string name="upload_progress_notification_title_start">Kopioidaan palvelimelle: %s</string>
@ -106,6 +109,8 @@
<string name="menu_from_camera">Ota kuva</string>
<string name="menu_nearby">Lähistöllä</string>
<string name="provider_contributions">Omat tallennukset</string>
<string name="menu_copy_link">Kopioi linkki</string>
<string name="menu_link_copied">Linkki on kopioitu leikepöydälle.</string>
<string name="menu_share">Jaa</string>
<string name="menu_view_file_page">Näytä tiedostosivu</string>
<string name="share_title_hint">Kuvateksti (vaaditaan)</string>
@ -122,13 +127,14 @@
<string name="provider_modifications">Muutokset</string>
<string name="menu_upload_single">Tallenna</string>
<string name="categories_search_text_hint">Etsi luokkia</string>
<string name="depicts_search_text_hint">Hae kohteita, joita mediasi kuvaa (vuori, Taj Mahal jne.)</string>
<string name="depicts_search_text_hint">Hae kohteita, joita mediasi esittää (vuori, Taj Mahal jne.)</string>
<string name="menu_save_categories">Tallenna</string>
<string name="menu_overflow_desc">Ylivuotovalikko</string>
<string name="refresh_button">Päivitä</string>
<string name="display_list_button">Lista</string>
<string name="contributions_subtitle_zero">(Ei vielä tallennuksia)</string>
<string name="categories_not_found">Luokkaa %1$s ei löytynyt</string>
<string name="depictions_not_found">Wikidata-kohteita ei löytynyt</string>
<string name="depictions_not_found">Hakusanaa %1$s vastaavia Wikidata-kohteita ei löytynyt</string>
<string name="no_child_classes">%1$s ei ole lapsiluokkia</string>
<string name="no_parent_classes">%1$s ei ole vanhempia luokkia</string>
<string name="categories_skip_explanation">Lisää luokkia tehdäksesi kuvistasi enemmän löydettäviä Wikimedia Commonssissa.\nAloita kirjoittaminen lisätäksesi luokkia.</string>
@ -136,6 +142,7 @@
<string name="title_activity_settings">Asetukset</string>
<string name="title_activity_signup">Rekisteröidy</string>
<string name="title_activity_featured_images">Suositellut kuvat</string>
<string name="title_activity_custom_selector">Mukautettu valitsin</string>
<string name="title_activity_category_details">Luokka</string>
<string name="title_activity_review">Vertaisarviointi</string>
<string name="menu_about">Tietoja</string>
@ -151,7 +158,7 @@
<string name="no_uploads_yet">Et ole vielä tallentanut kuvia.</string>
<string name="menu_retry_upload">Yritä uudelleen</string>
<string name="menu_cancel_upload">Peruuta</string>
<string name="media_upload_policy">Lisäämällä kuvan, ilmoitan tämän olevan oma työ ja että se ei sisällä tekijänoikeuden alaista materiaalia tai selfietä ja muuten noudattaa &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;Wikimedia Commons policies&lt;/a&gt;.</string>
<string name="media_upload_policy">Lisäämällä kuvan, ilmoitan tämän olevan oma työ ja että se ei sisällä tekijänoikeuden alaista materiaalia tai selfietä ja muuten noudattaa &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\"&gt;Wikimedia Commonsin käytäntöjä&lt;/a&gt;.</string>
<string name="menu_download">Lataa</string>
<string name="preference_license">Oletuslisenssi</string>
<string name="use_previous">Käytä edellistä otsikkoa ja kuvausta</string>
@ -174,7 +181,7 @@
<string name="tutorial_3_text">ÄLÄ tallenna seuraavia:</string>
<string name="tutorial_3_subtext_1">Selfiet tai kuvat ystävistäsi</string>
<string name="tutorial_3_subtext_2">Kuvia, jotka olet ladannut Internetistä</string>
<string name="tutorial_3_subtext_3">Kuvakaappaukset omistamistasi sovelluksista</string>
<string name="tutorial_3_subtext_3">Kuvakaappaukset muiden omistamista sovelluksista</string>
<string name="tutorial_4_text">Tallennusesimerkki:</string>
<string name="tutorial_4_subtext_1">Otsikko: Sydneyn oopperatalo</string>
<string name="tutorial_4_subtext_2">Kuvaus: Sydneyn oopperatalo lahden toiselta puolelta katsottuna</string>
@ -200,15 +207,17 @@
<string name="location_permission_title">Pyydetään sijaintilupaa</string>
<string name="ok">OK</string>
<string name="warning">Varoitus</string>
<string name="duplicate_file_name">Vastaava tiedostonimi löytyi</string>
<string name="upload">Tallenna</string>
<string name="yes">Kyllä</string>
<string name="no">Ei</string>
<string name="media_detail_caption">Kuvateksti</string>
<string name="media_detail_title">Otsikko</string>
<string name="media_detail_depiction">Kuvaukset</string>
<string name="media_detail_depiction">Esittää-tunnisteet</string>
<string name="media_detail_description">Kuvaus</string>
<string name="media_detail_discussion">Keskustelu</string>
<string name="media_detail_author">Tekijä</string>
<string name="media_detail_uploader">Tallentaja</string>
<string name="media_detail_uploaded_date">Tallennuspäivämäärä</string>
<string name="media_detail_license">Lisenssi</string>
<string name="media_detail_coordinates">Koordinaatit</string>
@ -216,6 +225,7 @@
<string name="become_a_tester_title">Ryhdy beetatestaajaksi</string>
<string name="become_a_tester_description">Valitse beeta-kanavamme Google Playssa ja hanki varhainen pääsy uusiin ominaisuuksiin ja virheenkorjauksiin</string>
<string name="_2fa_code">Kaksivaiheisen tunnistautumisen koodi</string>
<string name="email_auth_code">Sähköpostivahvistuskoodi</string>
<string name="logout_verification">Haluatko varmasti kirjautua ulos?</string>
<string name="mediaimage_failed">Mediakuva epäonnistui</string>
<string name="no_subcategory_found">Alaluokkia ei löytynyt</string>
@ -236,6 +246,7 @@
<string name="navigation_item_about">Tietoja</string>
<string name="navigation_item_settings">Asetukset</string>
<string name="navigation_item_feedback">Palaute</string>
<string name="navigation_item_feedback_github">Palaute GitHubissa</string>
<string name="navigation_item_logout">Kirjaudu ulos</string>
<string name="navigation_item_info">Opas</string>
<string name="navigation_item_notification">Ilmoitukset</string>
@ -270,14 +281,14 @@
<string name="skip_login">Ohita</string>
<string name="navigation_item_login">Kirjaudu sisään</string>
<string name="skip_login_title">Haluatko todella ohittaa kirjautumisen?</string>
<string name="skip_login_message" fuzzy="true">Sinun täytyy kirjautua sisään tallentaaksesi kuvia tulevaisuudessa.</string>
<string name="skip_login_message">Sinun täytyy kirjautua sisään tallentaaksesi kuvia tulevaisuudessa.</string>
<string name="login_alert_message">Kirjaudu sisään käyttääksesi tätä ominaisuutta</string>
<string name="copy_wikicode">Kopioi wikiteksti leikepöydälle</string>
<string name="wikicode_copied">Wikiteksti kopioitiin leikepöydälle</string>
<string name="nearby_location_not_available">Nearby ei välttämättä toimi, sillä sijainti ei käytettävissä.</string>
<string name="nearby_location_not_available">Lähistöllä-toiminto ei välttämättä toimi kunnolla, sillä sijainti ei käytettävissä.</string>
<string name="upload_location_access_denied">Sijainnin käyttö kielletty. Aseta sijaintisi manuaalisesti käyttääksesi tätä ominaisuutta.</string>
<string name="location_permission_rationale_nearby">Lupa vaaditaan läheisten paikkojen luettelon näyttämiseen</string>
<string name="location_permission_rationale_explore">Lupa vaaditaan läheltä otettujen kuvien luettelon näyttämiseen</string>
<string name="location_permission_rationale_nearby">Lupa vaaditaan lähellä olevien paikkojen luettelon näyttämiseen</string>
<string name="location_permission_rationale_explore">Lupa vaaditaan lähellä otettujen kuvien luettelon näyttämiseen</string>
<string name="nearby_directions">Reitit</string>
<string name="nearby_wikidata">Wikidata</string>
<string name="nearby_wikipedia">Wikipedia</string>
@ -312,7 +323,7 @@
<string name="search_recent_header">Äskettäiset haut:</string>
<string name="provider_searches">Äskettäin haetut kyselyt</string>
<string name="error_loading_categories">Luokkia ladattaessa tapahtui virhe.</string>
<string name="error_loading_depictions">Virhe ladattaessa kuvauksia.</string>
<string name="error_loading_depictions">Virhe ladattaessa esittää-tunnisteita.</string>
<string name="search_tab_title_media">Media</string>
<string name="search_tab_title_categories">Luokat</string>
<string name="search_tab_title_depictions">Kohteet</string>
@ -327,8 +338,8 @@
<string name="quiz_question_string">Onko tämä kuva OK tallennettavaksi?</string>
<string name="question">Kysymys</string>
<string name="result">Tulos</string>
<string name="quiz_back_button">Jos jatkat poistettavien kuvien lataamista, tilisi todennäköisesti kielletään. Haluatko varmasti lopettaa tietokilpailun?</string>
<string name="quiz_alert_message">Yli %1$s tallentamistasi kuvista on poistettu. Mikäli jatkat poistamista vaativien kuvien lataamista, tilisi todennäköisesti estetään.\n\nHaluatko tutustua oppaaseen uudelleen ja tehdä sen jälkeen tietovisan oppiaksesi minkälaisia kuvia saa ja ei saa tallentaa?</string>
<string name="quiz_back_button">Jos jatkat poistettavien kuvien tallentamista, tunnuksesi tullaan todennäköisesti estämään. Haluatko varmasti lopettaa tietokilpailun?</string>
<string name="quiz_alert_message">Yli %1$s tallentamistasi kuvista on poistettu. Mikäli jatkat poistamista vaativien kuvien tallentamista, tunnuksesi tullaan todennäköisesti estämään.\n\nHaluatko tutustua oppaaseen uudelleen ja tehdä sen jälkeen tietovisan oppiaksesi, minkälaisia kuvia saa ja ei saa tallentaa?</string>
<string name="selfie_answer">Selfieillä ei ole paljoa arvoa tietosanakirjassa. Älä tallenna kuvaa itsestäsi, ellei sinusta jo ole Wikipedia-artikkelia.</string>
<string name="taj_mahal_answer">Monumenteista ja maisemista otetut kuvat ovat hyväksyttäviä tallennettavaksi useimmissa maissa. Huomaa kuitenkin että ulkotiloihin sijoitetut väliaikaiset tilataideteokset ovat usein suojattu tekijänoikeudella ja niistä otettuja kuvia ei usein saa tallentaa.</string>
<string name="screenshot_answer">Kuvakaappauksia sivustoista tulkitaan jäljennöksiksi ja ovat täten sivuston kopiosuojan piirissä. Kuvia voidaan käyttää asianmukaisella tekijältä saadulta luvalla. Ilman kyseistä lupaa, mitä tahansa heidän materiaalistaan tuottamaa tuotosta tulkitaan alkuperäisen tekijän näkökulmasta luvattomaksi kopioksi.</string>
@ -350,28 +361,29 @@
<string name="error_fetching_nearby_monuments">Virhe läheisiä monumentteja haettaessa.</string>
<string name="no_recent_searches">Ei viimeaikaisia hakuja</string>
<string name="delete_recent_searches_dialog">Haluatko varmasti tyhjentää hakuhistoriasi?</string>
<string name="cancel_upload_dialog">Haluatko varmasti peruuttaa tämän latauksen?</string>
<string name="cancel_upload_dialog">Haluatko varmasti peruuttaa tämän tallennuksen?</string>
<string name="delete_search_dialog">Haluatko poistaa tämän haun?</string>
<string name="search_history_deleted">Hakuhistoria poistettu</string>
<string name="nominate_delete">Ehdota poistettavaksi</string>
<string name="delete">Poista</string>
<string name="Achievements">Saavutukset</string>
<string name="Profile">Profiili</string>
<string name="badges">Merkit</string>
<string name="statistics">Tilastot</string>
<string name="statistics_thanks">Kiitos vastaanotettu</string>
<string name="statistics_featured">Suositellut kuvat</string>
<string name="statistics_wikidata_edits">Kuvia läheltä</string>
<string name="statistics_wikidata_edits">\"Lähistöllä\"-kuvat</string>
<string name="level">Taso %d</string>
<string name="profileLevel">%s (taso %s)</string>
<string name="profile_withLevel">%s (taso %s)</string>
<string name="images_uploaded">Kuvia tallennettu</string>
<string name="image_reverts">Kuvia ei palautettu</string>
<string name="images_used_by_wiki">Kuvia käytetty</string>
<string name="achievements_share_message">Jaa saavutuksesi ystäviesi kanssa!</string>
<string name="achievements_info_message">Tasosi nousee, kun täytät nämä vaatimukset. Tilastot-osion kohteita ei lasketa tasoosi.</string>
<string name="achievements_revert_limit_message">vähimmäisvaatimus:</string>
<string name="images_uploaded_explanation">Lähetettyjen kuvien määrä Commonsiin minkä tahansa latausohjelmiston kautta</string>
<string name="images_uploaded_explanation">Lähetettyjen kuvien määrä Commonsiin minkä tahansa tallennusohjelmiston kautta</string>
<string name="images_reverted_explanation">Niiden kuvien prosenttiosuus, jotka olet ladannut Commonsiin ja joita ei poistettu</string>
<string name="images_used_explanation">Wikimedia-artikkeleissa käytettyjen Commonsiin lataamiesi kuvien määrä</string>
<string name="images_used_explanation">Wikimedia-artikkeleissa käytettyjen Commonsiin tallentamiesi kuvien määrä</string>
<string name="error_occurred">Tapahtui virhe!</string>
<string name="notifications_channel_name_all">Commons-ilmoitus</string>
<string name="preference_author_name_toggle">Käytä mukautettua tekijän nimeä</string>
@ -385,7 +397,7 @@
<string name="display_nearby_notification_summary">Näytä sovelluksen sisäinen ilmoitus lähinnä kuvia tarvitsevasta paikasta</string>
<string name="list_sheet">Lista</string>
<string name="storage_permission">Tallennuslupa</string>
<string name="write_storage_permission_rationale_for_image_share">Tarvitsemme luvan käyttääksesi laitteen ulkoista tallennustilaa kuvien lataamista varten.</string>
<string name="write_storage_permission_rationale_for_image_share">Tarvitsemme luvan käyttääksesi laitteen ulkoista tallennustilaa kuvien tallentamista varten.</string>
<string name="nearby_notification_dismiss_message">Et enää näe lähellä olevia paikkoja, jotka tarvitsevat kuvia. Voit kuitenkin halutessasi ottaa tämän ilmoituksen uudelleen käyttöön asetuksissa.</string>
<string name="step_count">Vaihe %1$d %2$d: %3$s</string>
<string name="next">Seuraava</string>
@ -394,6 +406,7 @@
<string name="map_application_missing">Laitteestasi ei löydy yhteensopivaa karttasovellusta. Asenna karttasovellus käyttääksesi tätä toimintoa.</string>
<string name="title_page_bookmarks_pictures">Kuvat</string>
<string name="title_page_bookmarks_locations">Sijainnit</string>
<string name="title_page_bookmarks_categories">Luokat</string>
<string name="menu_bookmark">Lisää kirjanmerkkeihin/Poista kirjanmerkeistä</string>
<string name="provider_bookmarks">Kirjanmerkit</string>
<string name="bookmark_empty">Et ole lisännyt yhtään kirjanmerkkejä</string>
@ -408,8 +421,8 @@
<string name="no_uploads">Tervetuloa Commonsiin!\n\nTallenna ensimmäinen mediasi koskettamalla lisäyspainiketta.</string>
<string name="no_categories_selected">Luokkia ei valittu</string>
<string name="no_categories_selected_warning_desc">Kuvat, jotka eivät ole luokissa, ovat harvoin käyttökelpoisia. Haluatko varmasti jatkaa valitsematta luokkia?</string>
<string name="no_depictions_selected">Kuvauksia ei valittu</string>
<string name="no_depictions_selected_warning_desc">Kuvat, joissa on kuvatekstejä, löytyvät helpommin ja todennäköisemmin niitä käytetään. Haluatko varmasti jatkaa valitsematta kuvatekstejä?</string>
<string name="no_depictions_selected">Esittää-tunnisteita ei valittu</string>
<string name="no_depictions_selected_warning_desc">Kuvat, joissa on esittää-tunnisteita, löytyvät helpommin ja niitä käytetään todennäköisemmin. Haluatko varmasti jatkaa valitsematta esittää-tunnisteita?</string>
<string name="back_button_warning">Peruuta tallennus</string>
<string name="back_button_warning_desc">Takaisin-napin painaminen peruuttaa tämän tallennuksen ja poistaa tallentamasi tiedot</string>
<string name="back_button_continue">Jatka tallennusta</string>
@ -467,6 +480,7 @@
<string name="no_notification">Sinulla ei ole lukemattomia ilmoituksia</string>
<string name="no_read_notification">Sinulla ei ole luettuja ilmoituksia</string>
<string name="share_logs_using">Jaa lokit käyttämällä</string>
<string name="check_your_email_inbox">Tarkista sähköpostilaatikkosi</string>
<string name="menu_option_read">Näytä luetut</string>
<string name="menu_option_unread">Näytä lukemattomat</string>
<string name="error_occurred_in_picking_images">Kuvien valinnassa tapahtui virhe</string>
@ -485,11 +499,11 @@
<string name="exif_tag_name_lensModel">Linssin malli</string>
<string name="exif_tag_name_serialNumbers">Sarjanumerot</string>
<string name="exif_tag_name_software">Ohjelmisto</string>
<string name="share_text">Lähetä valokuvia suoraan Wikimedia Commonsiin puhelimestasi. Lataa Commons-appi nyt: %1$s</string>
<string name="share_text">Tallenna kuvia Wikimedia Commonsiin suoraan puhelimeltasi. Lataa Commons-sovellus nyt: %1$s</string>
<string name="share_via">Jaa sovellus...</string>
<string name="image_info">Kuvan tiedot</string>
<string name="no_categories_found">Luokkia ei löytynyt</string>
<string name="no_depiction_found">Kuvauksia ei löytynyt</string>
<string name="no_depiction_found">Esittää-tunnisteita ei löytynyt</string>
<string name="upload_cancelled">Peruutettu tallennus</string>
<string name="dialog_box_text_nomination">Miksi %1$s tulisi poistaa?</string>
<string name="review_is_uploaded_by">%1$s oli lähettänyt: %2$s</string>
@ -505,6 +519,7 @@
<string name="delete_helper_ask_reason_copyright_press_photo">Lehdistökuva</string>
<string name="delete_helper_ask_reason_copyright_internet_photo">Satunnainen kuva internetistä</string>
<string name="delete_helper_ask_reason_copyright_logo">Logo</string>
<string name="delete_helper_ask_reason_copyright_no_freedom_of_panorama">Panoraamavapauden rikkomus</string>
<string name="delete_helper_ask_alert_set_positive_button_reason">Koska se on</string>
<string name="category_edit_helper_make_edit_toast">Yritetään päivittää luokkia.</string>
<string name="category_edit_helper_show_edit_title">Luokan päivitys</string>
@ -515,6 +530,9 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Ei voitu lisätä luokkia.</string>
<string name="category_edit_button_text">Päivitetään luokkia</string>
<string name="depictions_edit_helper_make_edit_toast">Yritetään päivittää esittää-tunnisteita.</string>
<string name="depictions_edit_helper_show_edit_title">Muokkaa esittää-tunnisteita</string>
<string name="depictions_edit_helper_edit_message_else">Esittää-tunnisteita ei voitu lisätä.</string>
<string name="coordinates_edit_helper_make_edit_toast">Yritetään päivittää koordinaatit.</string>
<string name="coordinates_edit_helper_show_edit_title">Koordinaattien päivitys</string>
<string name="description_edit_helper_show_edit_title">Kuvaus päivitetty</string>
@ -526,7 +544,7 @@
<string name="coordinates_edit_helper_edit_message_else">Koordinaatteja ei voitu lisätä.</string>
<string name="description_edit_helper_edit_message_else">Kuvauksia ei voitu lisätä.</string>
<string name="caption_edit_helper_edit_message_else">Kuvatekstiä ei voitu lisätä.</string>
<string name="coordinates_picking_unsuccessful" fuzzy="true">Koordinaattien haku epäonnistui.</string>
<string name="coordinates_picking_unsuccessful">Kuvan koordinaatteja ei tallennettu</string>
<string name="descriptions_picking_unsuccessful">Kuvauksia ei voitu hakea.</string>
<string name="description_activity_title">Muokkaa kuvauksia ja kuvatekstejä</string>
<string name="share_image_via">Jaa kuva</string>
@ -543,8 +561,13 @@
<string name="nearby_search_hint">Silta, museo, hotelli jne.</string>
<string name="you_must_reset_your_passsword">Jokin meni pieleen kirjautumisessa. Sinun on nollattava salasanasi!</string>
<string name="title_for_media">MEDIA</string>
<string name="upload_nearby_place_found_title">Lähipaikka löytyi</string>
<string name="upload_nearby_place_found_description_singular" fuzzy="true">Onko tämä kuva paikasta %1$s?</string>
<string name="title_for_child_classes">ALALUOKAT</string>
<string name="title_for_parent_classes">YLÄLUOKAT</string>
<string name="title_for_subcategories">ALALUOKAT</string>
<string name="title_for_parent_categories">YLÄLUOKAT</string>
<string name="upload_nearby_place_found_title">Lähistöllä-paikka löytyi</string>
<string name="upload_nearby_place_found_description_plural">Ovatko nämä kuvia paikasta %1$s?</string>
<string name="upload_nearby_place_found_description_singular">Onko tämä kuva paikasta %1$s?</string>
<string name="title_app_shortcut_bookmark">Kirjanmerkit</string>
<string name="title_app_shortcut_setting">Asetukset</string>
<string name="remove_bookmark">Poistettu kirjanmerkeistä</string>
@ -592,7 +615,7 @@
<string name="leaderboard_weekly">Viikoittain</string>
<string name="leaderboard_all_time">Koko ajalta</string>
<string name="leaderboard_upload">Lähetä</string>
<string name="leaderboard_nearby">Lähistöltä</string>
<string name="leaderboard_nearby">Lähistöllä</string>
<string name="leaderboard_used">Käyttöjä</string>
<string name="leaderboard_my_rank_button_text">Sijani</string>
<string name="limited_connection_enabled">Rajoitettu yhteystila päällä!</string>
@ -605,9 +628,9 @@
<string name="cancel_upload">Peruuta tallennus</string>
<string name="limited_connection_is_on">Rajoitettu yhteystila on päällä.</string>
<string name="media_details_tooltip">Kirjoita lyhyt kuvateksti. Kerro miksi kuva on kiinnostava, tyypillinen tai harvinainen ja selitä asiayhteys, näkyy se kuvassa tai ei. Käytä mahdollisimman tarkkaa terminologiaa.</string>
<string name="depicts_tooltip">Etsi ja valitse kaikki tämän kuvan kuvaamat käsitteet. Ole mahdollisimman tarkka. Mikäli kuvattuna on monta kohdetta, valitse ne kaikki kohtuullisuuden rajoissa. Älä valitse yleisiä tunnisteita mikäli tarkempia on saatavilla.</string>
<string name="categories_tooltip">Valitse sopivat luokat. Toisin kuin kuvaukset, luokkien nimet ovat vain englanniksi.</string>
<string name="license_tooltip">Kuka tahansa saa käyttää ja muokata Commonsiin lataamiasi kuvia. Haluatko luovuttaa kuviesi kaikki oikeudet? Haluatko tulla nimetyksi kuvien tekijänä? Haluatko kuviesi muokattujen versioiden julkaistavan samalla lisenssillä?</string>
<string name="depicts_tooltip">Etsi ja valitse kaikki käsitteet, joita tämä kuva esittää. Ole mahdollisimman tarkka. Mikäli kuvattuna on monta kohdetta, valitse ne kaikki kohtuullisuuden rajoissa. Älä valitse yleisiä tunnisteita, mikäli tarkempia on saatavilla.</string>
<string name="categories_tooltip">Valitse sopivat luokat. Toisin kuin esittää-tunnisteet, luokkien nimet ovat vain englanniksi.</string>
<string name="license_tooltip">Kuka tahansa saa käyttää ja muokata Commonsiin tallentamiasi kuvia. Haluatko luovuttaa kuviesi kaikki oikeudet? Haluatko tulla nimetyksi kuvien tekijänä? Haluatko kuviesi muokattujen versioiden julkaistavan samalla lisenssillä?</string>
<string name="depicts_step_title">Esittää</string>
<string name="license_step_title">Median lisenssi</string>
<string name="media_detail_step_title">Median tiedot</string>
@ -629,18 +652,19 @@
<string name="custom_selector_empty_text">Ei kuvia</string>
<string name="done">Valmis</string>
<string name="back">Takaisin</string>
<string name="welcome_custom_selector_ok">Mahtava</string>
<string name="welcome_custom_selector_ok">Mahtavaa</string>
<string name="custom_selector_already_uploaded_image_text">Tämä kuva on jo ladattu Commonsiin.</string>
<string name="learn_more">LUE LISÄÄ</string>
<string name="need_permission">Tarvitaan käyttöoikeus</string>
<string name="contributions_of_user">Käyttäjän muokkaukset: %s</string>
<string name="achievements_of_user">Käyttäjän saavutukset: %s</string>
<string name="menu_view_user_page" fuzzy="true">Näytä käyttäjäsivu</string>
<string name="menu_view_user_page">Näytä käyttäjäprofiili</string>
<string name="edit_depictions">Muokkaa esittää-tunnisteita</string>
<string name="edit_categories">Muokkaa luokkia</string>
<string name="advanced_options">Lisäasetukset</string>
<string name="apply">Käytä</string>
<string name="reset">Nollaa</string>
<string name="location_message">Sijaintitiedot auttavat wikin muokkaajia löytämään kuvasi, mikä tekee siitä paljon hyödyllisemmän.\nViimeaikaisissa tallennuksissasi ei ole sijaintia.\nSuosittelemme, että otat sijainnin käyttöön kamerasovelluksesi asetuksista.\nKiitos latauksesta!</string>
<string name="location_message">Sijaintitiedot auttavat wikin muokkaajia löytämään kuvasi, mikä tekee siitä paljon hyödyllisemmän.\nViimeaikaisissa tallennuksissasi ei ole sijaintia.\nSuosittelemme, että otat sijainnin käyttöön kamerasovelluksesi asetuksista.\nKiitos kuvien tallentamista!</string>
<string name="no_location_found_title">Paikkaa ei löytynyt</string>
<string name="add_location">Lisää paikka</string>
<string name="explore_map_details">Tiedot</string>
@ -666,9 +690,10 @@
<string name="welcome_to_full_screen_mode_text">Tervetuloa koko näytön valintatilaan</string>
<string name="full_screen_mode_zoom_info">Käytä kahta sormea lähentääksesi ja loitontaaksesi.</string>
<string name="full_screen_mode_features_info">Pyyhkäise nopeasti ja pitkään suorittaaksesi nämä toiminnot: \n- Vasen/Oikea: Siirry edelliseen/seuraavaan \n- Ylös: Valitse\n- Alas: Merkitse ei-tallennettavaksi.</string>
<string name="see_your_achievements">Näytä omat saavutukset</string>
<string name="see_your_achievements" fuzzy="true">Näytä omat saavutukset</string>
<string name="edit_image">Muokkaa kuvaa</string>
<string name="edit_location">Muokkaa sijaintia</string>
<string name="location_updated">Sijainti päivitetty!</string>
<string name="remove_location">Poista sijainti</string>
<string name="location_removed">Sijainti poistettu!</string>
<string name="send_thanks_to_author">Kiitä tekijää</string>
@ -681,4 +706,34 @@
<item quantity="one">%d kuva valittu</item>
<item quantity="other">%d kuvaa valittu</item>
</plurals>
<string name="talk">Keskustelu</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Oletko varma, että haluat peruuttaa kaikki tallennukset?</string>
<string name="cancelling_all_the_uploads">Peruutetaan kaikki tallennukset...</string>
<string name="uploads">Tallennukset</string>
<string name="pending">Odottavat</string>
<string name="failed">Epäonnistuneet</string>
<string name="custom_selector_delete_folder">Poista kansio</string>
<string name="custom_selector_confirm_deletion_title">Vahvista poisto</string>
<string name="custom_selector_confirm_deletion_message">Oletko varma, että haluat poistaa kansion %1$s, jossa on %2$d kohdetta?</string>
<string name="custom_selector_delete">Poista</string>
<string name="custom_selector_cancel">Peruuta</string>
<string name="custom_selector_folder_deleted_success">Kansio %1$s poistettu</string>
<string name="custom_selector_folder_deleted_failure">Kansion %1$s poistaminen epäonnistui</string>
<string name="red_pin">Tästä paikasta ei ole vielä kuvaa. Ota ihmeessä kuva!</string>
<string name="green_pin">Tästä paikasta on jo kuva.</string>
<string name="grey_pin">Tarkistetaan, onko tästä paikasta kuvaa.</string>
<string name="error_while_loading">Virhe ladattaessa</string>
<string name="usages_on_commons_heading">Commons</string>
<string name="usages_on_other_wikis_heading">Muut wikit</string>
<string name="file_usages_container_heading">Tiedoston käyttö</string>
<string name="account">Tunnus</string>
<string name="vanish_account">Hävitä käyttäjätunnus</string>
<string name="account_vanish_request_confirm_title">Varoitus hävittämisestä</string>
<string name="account_vanish_request_confirm">Hävittäminen on &lt;b&gt;viimeinen keino&lt;/b&gt; ja sitä tulee &lt;b&gt;käyttää vain, jos haluat lopettaa muokkaamisen lopullisesti&lt;/b&gt; ja samalla myös piilottaa mahdollisimman paljon aiemmista toimistasi.&lt;br/&gt;&lt;br/&gt;Käyttäjätunnuksen poistaminen Wikimedia Commonsissa tapahtuu muuttamalla tunnuksesi nimeä niin, etteivät muut pysty tunnistamaan muokkauksiasi. Tätä toimea kutsutaan käyttäjätunnuksen hävittämiseksi. &lt;b&gt;Hävittäminen ei takaa täyttä anonyymiyttä, eikä se poista hankkeisiin tekemiäsi muokkauksia&lt;/b&gt;.</string>
<string name="caption">Kuvateksti</string>
<string name="caption_copied_to_clipboard">Kuvateksti kopioitu leikepöydälle</string>
<string name="show_in_explore">Näytä Tutki-välilehdellä</string>
<string name="image_tag_line_created_and_uploaded_by">Luonut ja tallentanut: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Luonut %1$s ja tallentanut %2$s</string>
<string name="nominated_for_deletion_btn">Ehdotettu poistettavaksi</string>
</resources>

View file

@ -6,6 +6,7 @@
* Assorted-Interests
* BaRaN6161 TURK
* Bananax47
* Billibilbi
* BlueCamille
* Cigaryno
* Cyclicus
@ -15,6 +16,7 @@
* Fitoschido
* Friday83260
* Gomoko
* Goombiis
* GrandEscogriffe
* Happy13241
* Hecatonchire
@ -50,7 +52,7 @@
-->
<resources>
<string name="commons_facebook">Page Facebook de Commons</string>
<string name="commons_github">Code source Github de Commons</string>
<string name="commons_github">Code source de Commons sur Github</string>
<string name="commons_logo">Logo de Commons</string>
<string name="commons_website">Site web de Commons</string>
<string name="exit_location_picker">Sélecteur d\'emplacement de sortie</string>
@ -256,6 +258,7 @@
<string name="media_detail_description">Description</string>
<string name="media_detail_discussion">Discussion</string>
<string name="media_detail_author">Auteur</string>
<string name="media_detail_uploader">Téléverseur</string>
<string name="media_detail_uploaded_date">Date de téléversement</string>
<string name="media_detail_license">Licence</string>
<string name="media_detail_coordinates">Coordonnées</string>
@ -414,7 +417,7 @@
<string name="statistics_featured">Images remarquables</string>
<string name="statistics_wikidata_edits">Images par «Lieux à proximité»</string>
<string name="level">Niveau %d</string>
<string name="profileLevel">%s (niveau %s)</string>
<string name="profile_withLevel">%s (niveau %s)</string>
<string name="images_uploaded">Images téléversées</string>
<string name="image_reverts">Images non annulées</string>
<string name="images_used_by_wiki">Images utilisées</string>
@ -801,7 +804,7 @@
<string name="permissions_are_required_for_functionality">Des autorisations sont nécessaires pour la fonctionnalité</string>
<string name="learn_how_to_write_a_useful_description">Apprendre à écrire une description utile</string>
<string name="learn_how_to_write_a_useful_caption">Apprendre à écrire une légende utile</string>
<string name="see_your_achievements">Voir vos réalisations</string>
<string name="see_your_achievements">Consultez vos réalisations</string>
<string name="edit_image">Modifier limage</string>
<string name="edit_location">Modifier lemplacement</string>
<string name="location_updated">Emplacement mis à jour!</string>

View file

@ -232,8 +232,9 @@
<string name="navigation_item_about">Acerca de</string>
<string name="navigation_item_settings">Configuración</string>
<string name="navigation_item_feedback">Comentarios</string>
<string name="navigation_item_feedback_github">Comentarios a través de GitHub</string>
<string name="navigation_item_logout">Saír</string>
<string name="navigation_item_info">Titorial</string>
<string name="navigation_item_info">Guía</string>
<string name="navigation_item_notification">Notificacións</string>
<string name="navigation_item_review">Revisar</string>
<string name="no_description_found">non se atopou descrición</string>
@ -272,12 +273,12 @@
<string name="location_permission_rationale_nearby">Precísase permiso para amosar unha lista de lugares preto de aquí</string>
<string name="nearby_directions">COMO CHEGAR</string>
<string name="nearby_wikidata">WIKIDATA</string>
<string name="nearby_wikipedia">WIKIPEDIA</string>
<string name="nearby_wikipedia">Wikipedia</string>
<string name="nearby_commons">COMMONS</string>
<string name="about_rate_us">Avalíenos</string>
<string name="about_faq">FAQ</string>
<string name="user_guide">Guía de uso</string>
<string name="welcome_skip_button">Saltar titorial</string>
<string name="welcome_skip_button">Saltar a guía</string>
<string name="no_internet">Internet non dispoñible</string>
<string name="error_notifications">Erro ó recuperar as notificacións</string>
<string name="error_review">Houbo un erro ó recuperar a imaxe a revisar. Prema en refrescar para tentalo de novo.</string>
@ -319,7 +320,7 @@
<string name="question">Pregunta</string>
<string name="result">Resultado</string>
<string name="quiz_back_button">Se continúa cargando imaxes que requiran ser eliminadas, a súa conta probablemente sexa bloqueada. Está seguro de que quere rematar o cuestionario?</string>
<string name="quiz_alert_message">Máis do %1$s das imaxes que cargou foron eliminadas. Se continúa cargando imaxes que requiran ser borradas, probablemente a súa conta sexa bloqueada.\n\nGustaríalle ver de novo o titorial e facer un pequeno cuestionario que axuda a entender que tipo de imaxes se deben ou non se deben cargar?</string>
<string name="quiz_alert_message">Máis do %1$s das imaxes que cargou foron eliminadas. Se continúa cargando imaxes que requiran ser borradas, probablemente a súa conta sexa bloqueada.\n\nGustaríalle ver de novo a guía e facer un pequeno cuestionario que axuda a entender que tipo de imaxes se deben ou non se deben cargar?</string>
<string name="selfie_answer">Os autorretratos non teñen valor enciclopédico abondo. Por favor no cargue imaxes de vostede mesmo salvo que haxa un artigo de Wikipedia sobre vostede.</string>
<string name="taj_mahal_answer">As fotografías de monumentos e paisaxes de exterior poden ser cargadas na maioría dos países. Teña en conta, por favor, que as instalacións de arte temporais en exteriores normalmente teñen dereitos de autor protexidos e non poden ser cargadas.</string>
<string name="screenshot_answer">As capturas de pantalla de sitios web son consideradas traballos derivados e están suxeitas a dereitos de autor. Poden ser usadas despois de obter a autorización do autor. Sen ese permiso, calquera obra que cree baseada no seu traballo está considerada legalmente como unha copia sen licenza, cuxa propiedade é mantida polo autor orixinal.</string>
@ -394,7 +395,7 @@
<string name="deletion_reason_bad_for_my_privacy">Decateime de que prexudica a miña privacidade</string>
<string name="deletion_reason_no_longer_want_public">Cambiei de idea, non quero que siga sendo visible de forma pública</string>
<string name="deletion_reason_not_interesting">Desculpas, esta imaxe non é interesante para unha enciclopedia</string>
<string name="uploaded_by_myself" fuzzy="true">Cargada por min o %1$s, usada en %2$d artigo(s).</string>
<string name="uploaded_by_myself">Cargada por min o %1$s, usada polo menos en %2$d artigo(s).</string>
<string name="no_uploads">Dámoslle a benvida ó Commonsǃ\n\nCargue o seu primeiro ficheiro premendo no botón Engadir.</string>
<string name="no_categories_selected">Non hai categorías seleccionadas</string>
<string name="no_categories_selected_warning_desc">As imaxes sen categorías só son utilizables en contadas ocasións. Está seguro de que quere continuar sen seleccionar categorías?</string>

View file

@ -322,7 +322,7 @@
<string name="statistics_thanks">धन्यवाद प्राप्त किया</string>
<string name="statistics_featured">निर्वाचित चित्र</string>
<string name="level">स्तर %d</string>
<string name="profileLevel">%s (स्तर %s )</string>
<string name="profile_withLevel">%s (स्तर %s )</string>
<string name="images_uploaded">चित्र अपलोड हुआ</string>
<string name="image_reverts">चित्रों को वापस नहीं किया गया</string>
<string name="images_used_by_wiki">उपयोग हुए चित्र</string>

View file

@ -206,6 +206,7 @@
<string name="media_detail_description">Description</string>
<string name="media_detail_discussion">Discussion</string>
<string name="media_detail_author">Autor</string>
<string name="media_detail_uploader">Incargator</string>
<string name="media_detail_uploaded_date">Data de incargamento</string>
<string name="media_detail_license">Licentia</string>
<string name="media_detail_coordinates">Coordinatas</string>
@ -234,7 +235,7 @@
<string name="navigation_item_about">A proposito</string>
<string name="navigation_item_settings">Parametros</string>
<string name="navigation_item_feedback">Commentario</string>
<string name="navigation_item_feedback_github">Retroaction per GitHub</string>
<string name="navigation_item_feedback_github">Commentarios per GitHub</string>
<string name="navigation_item_logout">Clauder session</string>
<string name="navigation_item_info">Tutorial</string>
<string name="navigation_item_notification">Notificationes</string>
@ -364,7 +365,7 @@
<string name="statistics_featured">Imagines eminente</string>
<string name="statistics_wikidata_edits">Imagines via “Locos a proximitate”</string>
<string name="level">Nivello %d</string>
<string name="profileLevel">%s (Nivello %s)</string>
<string name="profile_withLevel">%s (Nivello %s)</string>
<string name="images_uploaded">Imagines incargate</string>
<string name="image_reverts">Imagines non revertite</string>
<string name="images_used_by_wiki">Imagines usate</string>
@ -717,8 +718,8 @@
<string name="device_model">Modello del apparato</string>
<string name="device_name">Nomine del apparato</string>
<string name="network_type">Typo de rete</string>
<string name="thanks_feedback">Gratias pro dar retroaction</string>
<string name="error_feedback">Error durante le invio del retroaction</string>
<string name="thanks_feedback">Gratias pro dar commentario</string>
<string name="error_feedback">Error durante le invio del commentario</string>
<string name="enter_description">Que es tu commentario?</string>
<string name="your_feedback">Tu commentario</string>
<string name="mark_as_not_for_upload">Marcar como non a incargar</string>
@ -749,7 +750,7 @@
<string name="permissions_are_required_for_functionality">Permissiones es necessari pro functionalitate</string>
<string name="learn_how_to_write_a_useful_description">Apprende como scriber un description utile</string>
<string name="learn_how_to_write_a_useful_caption">Appende como scriber un legenda utile</string>
<string name="see_your_achievements">Vider tu realisationes</string>
<string name="see_your_achievements">Examinar tu realisationes</string>
<string name="edit_image">Modificar imagine</string>
<string name="edit_location">Modificar position</string>
<string name="location_updated">Position actualisate!</string>
@ -782,7 +783,7 @@
<string name="is_at_a_different_place_wikidata">%1$s se trova in un altere loco.</string>
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">%1$s es in un altere loco. Per favor specifica le loco correcte hic infra, e si possibile, indica le latitude e longitude correcte.</string>
<string name="other_problem_or_information_please_explain_below">Altere problema o information (per favor explica hic infra).</string>
<string name="feedback_destination_note">Tu retroaction apparera sur le sequente pagina wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="feedback_destination_note">Tu commentario apparera sur le sequente pagina wiki: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:Mobile app/Feedback&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">Es tu secur de voler cancellar tote le incargamentos?</string>
<string name="cancelling_all_the_uploads">Cancella tote le incargamentos…</string>
<string name="uploads">Incargamentos</string>

View file

@ -110,17 +110,20 @@
<string name="menu_from_camera">Ambil foto</string>
<string name="menu_nearby">Sekitar</string>
<string name="provider_contributions">Unggahan saya</string>
<string name="menu_copy_link">Salin pranala</string>
<string name="menu_link_copied">Pranala telah disalin ke papan klip</string>
<string name="menu_share">Bagikan</string>
<string name="menu_view_file_page">Lihat halaman berkas</string>
<string name="share_title_hint">Takarir (Wajib)</string>
<string name="add_caption_toast">Berikan takarir untuk berkas ini</string>
<string name="share_description_hint">Deskripsi</string>
<string name="share_caption_hint">Takarir</string>
<string name="login_failed_network" fuzzy="true">Tidak dapat login - kesalahan pada jaringan</string>
<string name="login_failed_network">Tidak dapat login - kesalahan pada jaringan</string>
<string name="login_failed_throttled">Terlalu banyak percobaan masuk yang gagal. Harap coba lagi dalam beberapa menit</string>
<string name="login_failed_blocked">Maaf, pengguna ini telah diblokir di Commons</string>
<string name="login_failed_2fa_needed">Anda harus memberikan kode otentikasi dua faktor milik Anda</string>
<string name="login_failed_generic" fuzzy="true">Gagal masuk log</string>
<string name="login_failed_email_auth_needed">Kode verifikasi masuk log telah dikirim ke alamat surel Anda. Mohon berikan kode untuk masuk log.</string>
<string name="login_failed_generic">Gagal masuk log</string>
<string name="share_upload_button">Unggah</string>
<string name="multiple_share_base_title">Beri nama set ini</string>
<string name="provider_modifications">Modifikasi</string>
@ -179,7 +182,7 @@
<string name="tutorial_3_text">Mohon JANGAN diunggah:</string>
<string name="tutorial_3_subtext_1">Foto narsis atau foto teman Anda</string>
<string name="tutorial_3_subtext_2">Gambar yang Anda unduh dari Internet</string>
<string name="tutorial_3_subtext_3">Cuplikan layar suatu aplikasi</string>
<string name="tutorial_3_subtext_3">Cuplikan layar suatu aplikasi tidak bebas</string>
<string name="tutorial_4_text">Contoh unggahan:</string>
<string name="tutorial_4_subtext_1">Judul: Gedung Opera Sydney</string>
<string name="tutorial_4_subtext_2">Deskripsi: Gedung Opera Sydney yang dilihat dari seberang teluk</string>
@ -200,7 +203,7 @@
<string name="detail_license_empty">Lisensi tidak diketahui</string>
<string name="menu_refresh">Segarkan</string>
<string name="storage_permission_title">Meminta Izin Akses Penyimpanan</string>
<string name="read_storage_permission_rationale">Perlu Izin: Membaca penyimpanan eksternal. Tanpa hal ini aplikasi tidak dapat mengakses galeri Anda.</string>
<string name="read_storage_permission_rationale">Perlu Izin: Membaca penyimpanan eksternal. Tanpa izin ini aplikasi tidak dapat mengakses galeri Anda.</string>
<string name="write_storage_permission_rationale">Perlu Izin: Menulis penyimpanan eksternal. Tanpa hal ini aplikasi tidak dapat mengakses kamera/galeri.</string>
<string name="location_permission_title">Meminta Izin Lokasi</string>
<string name="ok">OKE</string>
@ -351,7 +354,7 @@
<string name="quiz_screenshot_question">Apakah tangkapan layar ini OKE untuk diunggah?</string>
<string name="share_app_title">Bagikan Aplikasi</string>
<string name="rotate">Putar</string>
<string name="error_fetching_nearby_places" fuzzy="true">Galat saat mengambil tempat terdekat.</string>
<string name="error_fetching_nearby_places">Tidak dapat memuat tempat-tempat terdekat.</string>
<string name="no_pictures_in_this_area">Tidak ada gambar di area ini</string>
<string name="no_nearby_places_around">Tidak ditemukan tempat yang dekat</string>
<string name="error_fetching_nearby_monuments">Galat saat mengambil monumen terdekat.</string>
@ -368,7 +371,7 @@
<string name="statistics_thanks">Ucapan terima kasih diterima</string>
<string name="statistics_featured">Gambar Pilihan</string>
<string name="statistics_wikidata_edits">Gambar via \"Tempat di Sekitar\"</string>
<string name="level" fuzzy="true">Tingkat</string>
<string name="level">Tingkat %d</string>
<string name="images_uploaded">Gambar Diunggah</string>
<string name="image_reverts">Gambar Tidak Dikembalikan</string>
<string name="images_used_by_wiki">Gambar Digunakan</string>
@ -410,7 +413,7 @@
<string name="deletion_reason_bad_for_my_privacy">Saya menyadari itu buruk untuk privasi saya</string>
<string name="deletion_reason_no_longer_want_public">Saya berubah pikiran, saya tidak ingin itu terlihat publik lagi</string>
<string name="deletion_reason_not_interesting">Maaf gambar ini tidak menarik untuk ensiklopedia</string>
<string name="uploaded_by_myself" fuzzy="true">Diunggah saya sendiri pada %1$s, digunakan dalam %2$d artikel.</string>
<string name="uploaded_by_myself">Diunggah saya sendiri pada %1$s, digunakan paling tidak dalam %2$d artikel.</string>
<string name="no_uploads">Unggah media pertama Anda dengan mengetuk tombol.</string>
<string name="no_categories_selected">Tidak ada Kategori yang Dipilih</string>
<string name="no_categories_selected_warning_desc">Gambar tanpa kategori jarang dapat digunakan. Apakah Anda yakin ingin mengirim tanpa memilih kategori?</string>
@ -427,7 +430,7 @@
<string name="never_ask_again">Jangan pernah menanyakan ini lagi</string>
<string name="display_location_permission_title">Tanya untuk izin lokasi</string>
<string name="display_location_permission_explanation">Minta izin lokasi ketika diperlukan untuk fitur tampilan kartu pemberitahuan sekitar.</string>
<string name="achievements_fetch_failed" fuzzy="true">Terjadi kesalahan. Kami tidak dapat mengambil pencapaian Anda</string>
<string name="achievements_fetch_failed">Terjadi kesalahan. Kami tidak dapat mengambil pencapaian Anda</string>
<string name="achievements_fetch_failed_ultimate_achievement">Anda telah membuat begitu banyak kontribusi sehingga sistem perhitungan pencapaian kami tidak dapat menanggulanginya. Ini adalah pencapaian yang tertinggi.</string>
<string name="ends_on">Berakhir pada:</string>
<string name="display_campaigns">Sinahang penyobyahan</string>
@ -435,7 +438,7 @@
<string name="option_allow">Izinkan</string>
<string name="option_dismiss">Tutup</string>
<string name="nearby_campaign_dismiss_message">Anda tidak akan melihat kampanye ini lagi. Namun, Anda bisa mengaktifkan kembali pemberitahuan ini di Pengaturan jika diinginkan.</string>
<string name="this_function_needs_network_connection" fuzzy="true">Fungsi ini membutuhkan hubungan jaringan, tolong periksa pengaturan jaringan Anda.</string>
<string name="this_function_needs_network_connection">Fungsi ini memerlukan hubungan jaringan. Tolong periksa pengaturan jaringan Anda.</string>
<string name="error_processing_image">Terjadi masalah saat memproses gambar. Harap mengulang lagi.</string>
<string name="getting_edit_token">Dapatkan token untuk menyunting</string>
<string name="check_category_adding_template">Menambahkan templat untuk pemeriksaan kategori</string>
@ -543,7 +546,7 @@
<string name="coordinates_edit_helper_edit_message_else">Tidak bisa menambahkan koordinat.</string>
<string name="description_edit_helper_edit_message_else">Tidak bisa menambahkan deskripsi.</string>
<string name="caption_edit_helper_edit_message_else">Tidak bisa menambahkan takarir.</string>
<string name="coordinates_picking_unsuccessful" fuzzy="true">Tidak bisa mendapatkan koordinat.</string>
<string name="coordinates_picking_unsuccessful">Koordinat gambar tidak diperbarui</string>
<string name="descriptions_picking_unsuccessful">Tidak bisa memperoleh deskripsi.</string>
<string name="description_activity_title">Sunting deskripsi dan takarir</string>
<string name="share_image_via">Bagikan gambar melalui</string>
@ -558,7 +561,7 @@
<string name="place_state_needs_photo">Perlu foto</string>
<string name="place_type">Jenis tempat:</string>
<string name="nearby_search_hint">Jembatas, museum, hotel, dll.</string>
<string name="you_must_reset_your_passsword" fuzzy="true">Terjadi kesalahan ketika masuk log, Anda perlu mengubah kata sandi Anda !!</string>
<string name="you_must_reset_your_passsword">Terjadi kesalahan ketika masuk log. Anda perlu mengatur ulang kata sandi Anda !!</string>
<string name="title_for_media">MEDIA</string>
<string name="title_for_child_classes">KELAS ANAK</string>
<string name="title_for_parent_classes">KELAS INDUK</string>
@ -578,7 +581,7 @@
<string name="recommend_high_accuracy_mode">Untuk hasil terbaik, pilih mode Akurasi Tinggi.</string>
<string name="ask_to_turn_location_on">Nyalakan lokasi?</string>
<string name="nearby_needs_location">Tempat sekitar perlu lokasi yang diaktifkan agar bekerja dengan benar</string>
<string name="upload_map_location_access" fuzzy="true">Anda perlu memberikan akses ke lokasi Anda saat ini untuk mengatur lokasi secara otomatis.</string>
<string name="upload_map_location_access">Anda perlu memberikan izin lokasi Anda saat ini untuk mengatur lokasi secara otomatis.</string>
<string name="use_location_from_similar_image">Apakah Anda menangkap kedua gambar ini di tempat yang sama? Apakah Anda ingin menggunakan lintang/bujur dari gambar yang di kanan?</string>
<string name="load_more">Muat Lebih Banyak</string>
<string name="nearby_no_results">Tempat tidak ditemukan, coba ubah kriteria pencarian Anda.</string>
@ -681,7 +684,7 @@
<string name="read_phone_state_permission_message">Peta Sekitar harus membaca STATUS TELEPON untuk berfungsi dengan benar</string>
<string name="contributions_of_user">Kontribusi Pengguna: %s</string>
<string name="achievements_of_user">Pencapaian Pengguna: %s</string>
<string name="menu_view_user_page" fuzzy="true">Lihat halaman pengguna</string>
<string name="menu_view_user_page">Lihat profil pengguna</string>
<string name="edit_depictions">Sunting penggambaran</string>
<string name="edit_categories">Sunting kategori</string>
<string name="advanced_options">Opsi Lanjutan</string>

View file

@ -209,6 +209,7 @@
<string name="media_detail_description">Deskripto</string>
<string name="media_detail_discussion">Diskuto</string>
<string name="media_detail_author">Autoro</string>
<string name="media_detail_uploader">Adkarganto</string>
<string name="media_detail_uploaded_date">Dato sendita</string>
<string name="media_detail_license">Licenco</string>
<string name="media_detail_coordinates">Koordinati</string>
@ -367,7 +368,7 @@
<string name="statistics_featured">Remarkinda imaji</string>
<string name="statistics_wikidata_edits">Imaji tra \"Loki Vicina\"</string>
<string name="level">Nivelo %d</string>
<string name="profileLevel">%s (Nivelo %s)</string>
<string name="profile_withLevel">%s (Nivelo %s)</string>
<string name="images_uploaded">Imaji sendita</string>
<string name="image_reverts">Imaji ne reversionita</string>
<string name="images_used_by_wiki">Imaji uzita</string>
@ -410,7 +411,7 @@
<string name="deletion_reason_bad_for_my_privacy">Me konstatis ke ol esas mala por mea privateso</string>
<string name="deletion_reason_no_longer_want_public">Me chanjis mea ideo: me ne pluse deziras ke ol esos publike videbla</string>
<string name="deletion_reason_not_interesting">Pardonez! Ca imajo ne esas interesanta por ula enciklopedio</string>
<string name="uploaded_by_myself" fuzzy="true">Adjuntita da me, che %1$s, uzita en %2$d artiklo/artikli.</string>
<string name="uploaded_by_myself">Adjuntita da me che %1$s; uzita en adminime %2$d artiklo/artikli.</string>
<string name="no_uploads">Bonveno a Commons!\n\nSendez vua unesma arkivo kliktanta sur butono \"adjuntez\" (\'\'add\'\').</string>
<string name="no_categories_selected">Nula kategorio selektita</string>
<string name="no_categories_selected_warning_desc">Imaji sen kategorii rare esas uzebla. Ka vu fakte deziras sendar ol sen selektar irga kategorio?</string>
@ -436,11 +437,13 @@
<string name="option_allow">Permisar</string>
<string name="option_dismiss">Eskartar</string>
<string name="in_app_camera_needs_location">Voluntez kapabligar registrago di lokizo en \'\'Settings\'\', e probez itere.\n\nNoto: l\'arkivo sendanta povas ne havar informo pri lokizo, se l\'\'\'app\'\' ne povas rekuperar l\'informo pri lokizo en kurta intervalo.</string>
<string name="in_app_camera_location_permission_rationale">La kamero en l\'utensilo bezonas permiso por adjuntar ca informo en imaji, se l\'informo ne esas disponebla che EXIF. Voluntez permisar ke l\'\'\'app\'\' acesez vua lokizo, e probez itere.\n\nAtencez: Esas posibla ke l\'imajo sendonta ne havos informo rekuperebla pri lokizo, se l\'\'\'app\'\' ne povos rekuperor ol pos kurta intervalo.</string>
<string name="in_app_camera_location_permission_denied">La programo \'\'app\'\' ne enrejistros informo pri lokizo en la fotografuri pro manko di permisi</string>
<string name="in_app_camera_location_unavailable">Sen kapabligar GPS, l\'enrejistro di la lokizo en la fotografuri ne facesas.</string>
<string name="open_document_photo_picker_title">Uzez selektilo di fotografuri segun dokumenti</string>
<string name="open_document_photo_picker_explanation">La nova funciono \'\'Android photo picker\'\' povas perdar informo pri lokizo. Kapabligez ol, se vu semblas uzar ol.</string>
<string name="location_loss_warning">Deskapabliganta ol povos deskuplar la nova funciono \'\'Android photo picker\'\'. Posible perdos informo pri lokizo.</string>
<string name="nearby_campaign_dismiss_message">Vu ne pluse vidos ta kampanii. Tamen, vu povos itere kapabligar ca avizo en Ajusti (\'\'Settings\'\'), se vu deziros.</string>
<string name="this_function_needs_network_connection">Ca funciono bezonas ligilo ad interreto. Verifikez vua ajusti pri konekti.</string>
<string name="error_processing_image">Eventis eroro dum procesado dil imajo. Voluntez probar ol itere!</string>
<string name="getting_edit_token">Kaptanta \'\'token\'\' por redaktar.</string>
@ -573,6 +576,8 @@
<string name="title_for_media">\'\'MEDIA\'\'</string>
<string name="title_for_child_classes">SUBKLASI</string>
<string name="title_for_parent_classes">KLASI PLU ABSTRAKTA</string>
<string name="title_for_subcategories">SUB-KATEGORII</string>
<string name="title_for_parent_categories">PRECIPUA KATEGORII</string>
<string name="upload_nearby_place_found_title">Loko proxima trovesis</string>
<string name="upload_nearby_place_found_description_plural">Ka ca imaji apartenas a %1$s?</string>
<string name="upload_nearby_place_found_description_singular">Ka to esas imajo di %1$s?</string>
@ -636,19 +641,26 @@
<string name="leaderboard_used">Uzita</string>
<string name="leaderboard_my_rank_button_text">Mea rango</string>
<string name="limited_connection_enabled">Kapabligesis por uzar kun limitizita konekti!</string>
<string name="limited_connection_disabled">Posibleso pri uzo kun limitizita konekto deskapabligita. La sendo di arkivi rikomencos nun.</string>
<string name="limited_connection_mode">Modo por limitizita retoligilo</string>
<string name="statistics_quality">Imaji di qualeso</string>
<string name="quality_images_info">Imaji kun qualeso esas diagrami o fotografuri qui havas qualesi (maxim-multa-kaze teknikala) ed esas valoroza por projeti de Wikimedia</string>
<string name="resuming_upload">Duriganta sendajo...</string>
<string name="pausing_upload">Pauzanta sendajo...</string>
<string name="cancelling_upload">Nuliganta sendajo...</string>
<string name="cancel_upload">Cesar kargajo</string>
<string name="limited_connection_explanation">Vu kapabligesis l\'uzo di limitizita konekto. Omna senduri pauzesis e durigos nur kande vu deskapabligos ta uzo.</string>
<string name="limited_connection_is_on">Kapabligesis por uzar limitizita konekti.</string>
<string name="media_details_tooltip">Voluntez skribar kurta titulo deskriptanta quon vua imajo montras. En la deskripto, explikez pro quo la fotografuro esas interesanta, tipala o rara, ed explikez la kuntexto, videbla o ne. Skriptez tan exakta kam posibla.</string>
<string name="depicts_tooltip">Voluntez trovar e selektar omna konceptaji quan ca imajo reprezentas. Esez plu preciza kam vu povas. Se ta imajo montras diversa kozi, selektez precize omna ek li. Ne uzez nepreciza deskripturi, se specifika deskripturi existas.</string>
<string name="categories_tooltip">Voluntez selektar la kategorii konvenanta. Diferante de deskripturi, kategorii nur existas en Angla linguo.</string>
<string name="license_tooltip">En Commons, vua imaji povos riuzesar ed adaptesar da omni. Ka vu deziras renuncar omna autoroyuri? Ka vu deziras ke l\'imajo atribuesos a vu? Ka vu deziras adapti por uzar la sama licenco?</string>
<string name="depicts_step_title">Montras</string>
<string name="license_step_title">Licencizo di \'\'media\'\'</string>
<string name="media_detail_step_title">Detali pri \'\'media\'\'</string>
<string name="menu_view_category_page">Vidar kategorio-pagino</string>
<string name="menu_view_item_page">Vidar pagino dil arkivo</string>
<string name="app_ui_language">Idiomo di vua interfacio</string>
<string name="remove">Removar titulo e deskripto</string>
<string name="read_help_link">Lektez pluse</string>
<string name="media_detail_in_all_languages">En omna idiomi</string>
@ -669,7 +681,10 @@
<string name="done">Facita</string>
<string name="back">Retroirar</string>
<string name="welcome_custom_picture_selector_text">Bonveno a personalizita selektilo di imaji</string>
<string name="custom_selector_info_text1">Ica selektilo montras quala imaji vu ja sendis a Commons.</string>
<string name="welcome_custom_selector_ok">Ecelanta</string>
<string name="custom_selector_already_uploaded_image_text">Ca imajo ja sendesis a Commons.</string>
<string name="custom_selector_over_limit_warning">Por teknikala motivi, l\'utensilo \'\'app\'\' ne povas fidinde sendar plua kam %1$d pikturi samatempe. La limito %1$d superesis per %2$d.</string>
<string name="custom_selector_dismiss_limit_warning_button_text">Eskartar</string>
<string name="custom_selector_button_limit_text">Maximo: %1$d</string>
<string name="custom_selector_limit_error_desc">Eroro: Limito pri sendajo transpasita</string>
@ -686,12 +701,17 @@
<string name="edit_depictions">Redaktar deskripturi</string>
<string name="edit_categories">Redaktar kategorii</string>
<string name="advanced_options">Progresiva selektaji (advanced options)</string>
<string name="advanced_query_info_text">Vu povas ajustar la demando \"Vicini\" (\'\'Nearby\'\'). Se erori aparos, riadjustez ed aplikez.</string>
<string name="apply">Aplikar</string>
<string name="reset">Restaurar</string>
<string name="no_location_found_title">Nula lokizo trovita</string>
<string name="no_location_found_message">Ka vu deziras informar la loko de ube vu obtenis ca imajo?\nInformo pri la lokizo helpos editeri trovar vua imajo, do ol divenos plu utila.\nDanko!</string>
<string name="add_location">Adjuntez lokizo</string>
<string name="feedback_sharing_data_alert">Voluntez removar de ca e-postala mesajo irga informo quan vu ne deziras divenar publika. Anke konciez ke vua e-postal-adreso quan vu esas uzanta, vua nomo e l\'imajo asociita a vu divenos publike videbla.</string>
<string name="explore_map_details">Detali</string>
<string name="achievements_unavailable_beta">Sucesi nur esas disponebla en la definitiva versiono. Voluntez verifikar la dokumentigo pri developo.</string>
<string name="leaderboard_unavailable_beta">La klasifiko-tabelo nur esas disponebla en la definitiva versiono. Voluntez verifikar la dokumentigo pri developo.</string>
<string name="copyright_popup">Voluntez sendar nur pikturi facita da vu. Senderi di imaji kun autoroyuro ne libera blokusesos. To aplikesas anke por probi \'\'beta\'\'. Danko por probar l\'utensilo \'\'app\'\'!</string>
<string name="api_level">nivelo di API</string>
<string name="android_version">versiono di Android</string>
<string name="device_manufacturer">Fabrikanto dil aparato</string>
@ -705,6 +725,9 @@
<string name="mark_as_not_for_upload">Indikez por ne sendar ol</string>
<string name="unmark_as_not_for_upload">Itere indikez por sendar ol</string>
<string name="marking_as_not_for_upload">Indikanta ke ol ne sendesos</string>
<string name="unmarking_as_not_for_upload">Indikita kom por ne sendar</string>
<string name="show_already_actioned_pictures">Montrar imaji ja traktita</string>
<string name="hiding_already_actioned_pictures">Celanta imaji ja traktita</string>
<string name="no_more_images_found">Ne trovesis plusa imaji</string>
<string name="this_image_is_already_uploaded">Ca imajo ja sendesis</string>
<string name="can_not_select_this_image_for_upload">Ne povis selektar ca imajo por sendar (\'\'upload\'\')</string>
@ -719,11 +742,15 @@
<string name="request_user_block">Demandar blokuso di ca uzero</string>
<string name="welcome_to_full_screen_mode_text">Bonveno a selekto di Modo \"tota-skreno\"</string>
<string name="full_screen_mode_zoom_info">Uzez du fingri por augmentar o diminutar \'\'zoom\'\'.</string>
<string name="full_screen_mode_features_info">Glitez rapide e longe por facar lo sequanta: \n- Sinistre/Dextre: Irar al antea/nexta\n- Adsupre: Selektar\n- Adinfre: Indikez kom ne por sendar.</string>
<string name="set_up_avatar_toast_string">Por establisar l\'avataro di vua klasifiko-tabelo, kliktez \"Uzar kom avataro\" en la 3-punti menuo de irga imajo.</string>
<string name="similar_coordinate_description_auto_set">Koordinati ne esas l\'exakta, tamen l\'individuo qua sendis ca imajo kredas ke la koordinati quin lu informis esas suficante proxima.</string>
<string name="storage_permissions_denied">Permiso pri enmagazinigado neaceptata</string>
<string name="unable_to_share_upload_item">Ne povis partigar ca arkivo</string>
<string name="permissions_are_required_for_functionality">Uzar la funcionado bezonas permisi</string>
<string name="learn_how_to_write_a_useful_description">Savez quale skribar utila deskripto</string>
<string name="learn_how_to_write_a_useful_caption">Savez quale skribar utila etiketo</string>
<string name="see_your_achievements">Videz vua sucesi</string>
<string name="see_your_achievements">Vidar vua sucesi</string>
<string name="edit_image">Modifikar imajo</string>
<string name="edit_location">Aktualigar lokizo</string>
<string name="location_updated">Lokizo aktualigita!</string>
@ -734,6 +761,7 @@
<string name="send_thanks_to_author">Dankar l\'autoro</string>
<string name="error_sending_thanks">Eroro sendanta danki al autoro.</string>
<string name="invalid_login_message">La tempo-quanto por vua \'\'log in\'\' finis. Voluntez itere enirar.</string>
<string name="no_application_available_to_open_gpx_files">Nula utensilo \'\'app\'\' disponebla por apertar arkivi GPX</string>
<string name="file_saved_successfully">Konservo sucesoza di arkivo</string>
<string name="do_you_want_to_open_gpx_file">Ka vu deziras apertar arkivo GPX?</string>
<string name="do_you_want_to_open_kml_file">Ka vu deziras apartar l\'arkivo KML?</string>
@ -745,8 +773,13 @@
<item quantity="one">%d imajo selektita</item>
<item quantity="other">%d imaji selektita</item>
</plurals>
<string name="please_enter_some_comments">Voluntez facar kelka komenti</string>
<string name="talk">Diskuto</string>
<string name="write_something_about_the_item">Dicez irgu pri l\'arkivo \'%1$s\'. Ol esos videbla publike.</string>
<string name="does_not_exist_anymore_no_picture_can_ever_be_taken_of_it">\'%1$s\' ne pluse existas, nula imajo povos rekuperesar de ol.</string>
<string name="is_at_a_different_place_wikidata">\'%1$s\' esas en diferanta loko.</string>
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">\'%1$s\' esas en diferanta loko. Voluntez mencionar la korekta loko adinfre e, se posibla, skribez la korekta latitudo e longitudo.</string>
<string name="other_problem_or_information_please_explain_below">Altra problemo od informo (voluntez explikar adinfre).</string>
<string name="cancelling_all_the_uploads">Extinganta la tota sendaji...</string>
<string name="uploads">Arkivi sendita</string>
<string name="pending">Vartanta</string>
@ -776,6 +809,8 @@
<string name="caption">Deskripto-texto</string>
<string name="caption_copied_to_clipboard">Deskripto-texto kopiita a \'\'clipboard\'\'</string>
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">Gratuli! Omna imaji en ca albumo sive sendesis, sive indikesis por ne sendar.</string>
<string name="show_in_nearby">Montrez en Proxima (\'\'Nearby\'\')</string>
<string name="image_tag_line_created_and_uploaded_by">Kreesis e sendesis da: %1$s</string>
<string name="image_tag_line_created_by_and_uploaded_by">Kreita da %1$s e sendita da %2$s</string>
<string name="nominated_for_deletion_btn">Indikita por Efaco</string>
</resources>

View file

@ -391,7 +391,7 @@
<string name="statistics_featured">Immagini in evidenza</string>
<string name="statistics_wikidata_edits">Immagini tramite \"Luoghi nelle vicinanze\"</string>
<string name="level">Livello %d</string>
<string name="profileLevel">%s (Livello %s)</string>
<string name="profile_withLevel">%s (Livello %s)</string>
<string name="images_uploaded">Immagini caricate</string>
<string name="image_reverts">Immagini non ripristinate</string>
<string name="images_used_by_wiki">Immagini utilizzate</string>

View file

@ -243,6 +243,7 @@
<string name="media_detail_description">תיאור</string>
<string name="media_detail_discussion">דיון</string>
<string name="media_detail_author">יוצר</string>
<string name="media_detail_uploader">מעלה</string>
<string name="media_detail_uploaded_date">תאריך העלאה</string>
<string name="media_detail_license">רישיון</string>
<string name="media_detail_coordinates">נקודות ציון</string>
@ -401,7 +402,7 @@
<string name="statistics_featured">תמונות מומלצות</string>
<string name="statistics_wikidata_edits">תמונות דרך \"מקומות בסביבה\"</string>
<string name="level">רמה %d</string>
<string name="profileLevel">%s (רמה %s)</string>
<string name="profile_withLevel">%s (רמה %s)</string>
<string name="images_uploaded">תמונות שהועלו</string>
<string name="image_reverts">תמונות שלא שוחזרו</string>
<string name="images_used_by_wiki">תמונות בשימוש</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Anastasia
* Beqabai
* David1010
* Mehman97
@ -49,6 +50,10 @@
<item quantity="one">%1$d ატვირთვა</item>
<item quantity="other">%1$d ატვირთვა</item>
</plurals>
<plurals name="receiving_shared_content">
<item quantity="one">გაზიარებული კონტენტის მიღება. სურათის დამუშავებას შეიძლება გარკვეული დრო დასჭირდეს სურათის ზომისა და თქვენი მოწყობილობიდან გამომდინარე</item>
<item quantity="other">გაზიარებული კონტენტის მიღება. სურათების დამუშავებას შეიძლება გარკვეული დრო დასჭირდეს სურათების ზომისა და თქვენი მოწყობილობიდან გამომდინარე</item>
</plurals>
<string name="navigation_item_explore">აღმოაჩინე</string>
<string name="preference_category_appearance">იერსახე</string>
<string name="preference_category_general">მთავარი</string>
@ -96,6 +101,7 @@
<string name="menu_nearby">ახლოს</string>
<string name="provider_contributions">ჩემი ატვირთვები</string>
<string name="menu_copy_link">ბმულის კოპირება</string>
<string name="menu_link_copied">ბმული დაკოპირებულია ბუფერში</string>
<string name="menu_share">გაზიარება</string>
<string name="menu_view_file_page">ფაილის გვერდის ნახვა</string>
<string name="share_title_hint">წარწერა (სავალდებულო)</string>
@ -106,6 +112,7 @@
<string name="login_failed_throttled">ძალიან ბევრი წარუმატებელი მცდელობა. გთხოვთ, რამდენიმე წუთში სცადეთ კვლავ.</string>
<string name="login_failed_blocked">უკაცრავად, ეს მომხმარებელი დაბლოკილია ვიკისაწყობში</string>
<string name="login_failed_2fa_needed">თქვენ უნდა შეიყვანოთ ორფაქტორიანი ავტორიზაციის კოდი.</string>
<string name="login_failed_email_auth_needed">თქვენს ელ. ფოსტის მისამართზე გამოიგზავნა შესვლის დამადასტურებელი კოდი. გთხოვთ, შეიყვანოთ კოდი იმისთვის ,რომ შეხვიდეთ.</string>
<string name="login_failed_generic">შესვლა ვერ მოხერხდა</string>
<string name="share_upload_button">ატვირთვა</string>
<string name="multiple_share_base_title">სერიის სახელი</string>
@ -114,11 +121,14 @@
<string name="categories_search_text_hint">კატეგორიის არჩევა</string>
<string name="depicts_search_text_hint">მოძებნეთ რაიმე, რაც თქვენს ფაილზეა ასახული (მთა, ტაჯ მაჰალი და ა.შ.)</string>
<string name="menu_save_categories">შენახვა</string>
<string name="menu_overflow_desc">გადავსების მენიუ</string>
<string name="refresh_button">განახლება</string>
<string name="display_list_button">სია</string>
<string name="contributions_subtitle_zero">(ატვირთვები არ არის)</string>
<string name="categories_not_found">შესატყვისი კატეგორიები „%1$s“ ვერ მოიძებნა</string>
<string name="depictions_not_found">არანაირი Wikidata- ს ობიექტი არ აღმოაჩნდა, რომელიც შეესაბამებოდა %1$s- ს</string>
<string name="no_child_classes">%1$s არ აქვს ბავშვების კლასები</string>
<string name="no_parent_classes">%1$s არ აქვს მშობლის კლასები</string>
<string name="categories_skip_explanation">დაამატეთ კატეგორიები, რომ თქვენი სურათები უფრო აღმოსაჩენი იყოს ვიკისაწყობში.\nდაიწეთ წერა კატეგორიების დასამატებლად.</string>
<string name="categories_activity_title">კატეგორია</string>
<string name="title_activity_settings">კონფიგურაცია</string>
@ -208,6 +218,7 @@
<string name="become_a_tester_title">ბეტა ტესტირებაში მონაწილეობა</string>
<string name="become_a_tester_description">ჩაერთეთ Beta-ზე წვდომა Google Play-ზე და მიიღეთ ადრეული წვდომა ახალ ფუნქციებზე და შეცდომების აღმოფხვრაზე</string>
<string name="_2fa_code">2ფა კოდი</string>
<string name="email_auth_code">ელფოსტის დამადასტურებელი კოდი</string>
<string name="logout_verification">ნამდვილად გსურთ გასვლა?</string>
<string name="mediaimage_failed">მედიაგამოსახულების შეცდომა</string>
<string name="no_subcategory_found">ქვეკატეგორიები ვერ მოიძებნა</string>
@ -268,6 +279,7 @@
<string name="copy_wikicode">დააკოპირეთ ვიკიტექსტი</string>
<string name="wikicode_copied">ვიკიტექსტი დაკოპირდა</string>
<string name="nearby_location_not_available">Nearby ფუნქცია შეიძლება არ მუშაობდეს სწორად, მდებარეობა მიუწვდომელია.</string>
<string name="nearby_showing_pins_offline">ინტერნეტი მიუწვდომელია. ნაჩვენებია მხოლოდ ქეშირებული ადგილები.</string>
<string name="upload_location_access_denied">მდებარეობაზე წვდომა უარყოფილია. გთხოვთ, დააყენოთ თქვენი მდებარეობა ხელით ამ ფუნქციის გამოსაყენებლად.</string>
<string name="location_permission_rationale_nearby">საჭიროა ნებართვა ახლომდებარე ადგილების სიის საჩვენებლად</string>
<string name="location_permission_rationale_explore">ახლომდებარე სურათების სიის საჩვენებლად საჭიროა ნებართვა</string>
@ -329,13 +341,23 @@
<string name="blurry_image_answer">Commons-ის ერთ-ერთი მიზანია ხარისხიანი სურათების შეგროვება. ამიტომ ბუნდოვანი სურათები არ უნდა აიტვირთოს. ყოველთვის ეცადეთ გადაიღოთ ლამაზი სურათები კარგი განათებით.</string>
<string name="construction_event_answer">სურათები, რომლებიც აჩვენებს ტექნოლოგიას ან კულტურას, ძალიან მისასალმებელია Commons-ზე.</string>
<string name="congratulatory_message_quiz">თქვენ მიიღეთ %1$s სწორი პასუხიდან. გილოცავ!</string>
<string name="warning_for_no_answer">კითხვაზე პასუხის გასაცემად აირჩიეთ ორი ვარიანტიდან ერთ-ერთი</string>
<string name="user_not_logged_in">შესვლის ვადა ამოიწურა. გთხოვთ, ხელახლა შეხვიდეთ სისტემაში.</string>
<string name="quiz_result_share_message">გაუზიარეთ თქვენი ვიქტორინა მეგობრებს!</string>
<string name="continue_message">გაგრძელება</string>
<string name="correct">სწორი პასუხი</string>
<string name="wrong">პასუხი არასწორია</string>
<string name="quiz_screenshot_question">ეს სკრინშოტი კარგია ასატვირთად?</string>
<string name="share_app_title">გაუზიარეთ აპლიკაცია</string>
<string name="rotate">შეტრიალება</string>
<string name="error_fetching_nearby_places">ახლომდებარე ადგილების ჩამოტვირთვა ვერ მოხერხდა</string>
<string name="no_pictures_in_this_area">ამ ტერიტორიაზე სურათები არ არის</string>
<string name="no_nearby_places_around">ახლომდებარე ადგილები არ არის</string>
<string name="error_fetching_nearby_monuments">შეცდომა მოხდა ახლომდებარე ძეგლების მოძიებისას.</string>
<string name="no_recent_searches">ბოლო მოძიებულები არ არსებობს</string>
<string name="delete_recent_searches_dialog">დარწმუნებული ხართ, რომ გსურთ ძიების ისტორიის გასუფთავება?</string>
<string name="cancel_upload_dialog">დაწმუნებული ხართ რომ გსურთ ატვირთვის გაუქმება?</string>
<string name="delete_search_dialog">გსურთ ამ ძიების წაშლა?</string>
<string name="search_history_deleted">ძიების ისტორია წაშლილია</string>
<string name="nominate_delete">წაშლაზე ნომინირება</string>
<string name="delete">წაშლა</string>
@ -345,38 +367,84 @@
<string name="statistics">სტატისტიკა</string>
<string name="statistics_thanks">მადლობა მიღებულია</string>
<string name="statistics_featured">რჩეული სურათები</string>
<string name="statistics_wikidata_edits">სურათები „ახლომდებარე ადგილები“ -დან</string>
<string name="level">დონე %d</string>
<string name="profileLevel">%s (დონე %s)</string>
<string name="profile_withLevel">%s (დონე %s)</string>
<string name="images_uploaded">სურათები ატვირთულია</string>
<string name="image_reverts">სურათები არ დაბრუნებულა</string>
<string name="images_used_by_wiki">სურათები გამოიყენება</string>
<string name="achievements_share_message">გაუზიარეთ თქვენი მიღწევები თქვენს მეგობრებს!</string>
<string name="achievements_info_message">თქვენი დონე იზრდება, როდესაც ამ მოთხოვნებს აკმაყოფილებთ. \"სტატისტიკის\" განყოფილებაში მოცემული პუნქტები არ არის გათვალისწინებული თქვენს დონის ზრდაზე.</string>
<string name="achievements_revert_limit_message">მინიმალური მოთხოვნა:</string>
<string name="images_uploaded_explanation">სურათების რაოდენობა, რომლებიც თქვენ ატვირთეთ Commons-ზე, ნებისმიერი ატვირთვის პროგრამული უზრუნველყოფის მეშვეობით</string>
<string name="images_reverted_explanation">Commons-ზე ატვირთული სურათების პროცენტული მაჩვენებელი, რომლებიც არ წაიშალა</string>
<string name="images_used_explanation">თქვენი მიერ Commons-ზე ატვირთული სურათების რაოდენობა, რომლებიც ვიკიმედიის სტატიებში გამოიყენეს</string>
<string name="error_occurred">შეცდომა მოხდა!</string>
<string name="notifications_channel_name_all">ვიკისაწყობის შეტყობინება</string>
<string name="preference_author_name_toggle">ავტორის სხვა სახელის გამოყენება</string>
<string name="preference_author_name_toggle_summary">გამოიყენეთ ფოტოების მომხმარებლის სახელი თქვენი მომხმარებლის სახელის ნაცვლად ფოტოების ატვირთვის დროს</string>
<string name="preference_author_name">თქვენი არჩეული ავტორის სახელწოდება</string>
<string name="contributions_fragment">წვლილი</string>
<string name="nearby_fragment">ახლოს</string>
<string name="notifications">შეტყობინებები</string>
<string name="read_notifications">შეტყობინებები (წაკითხვა)</string>
<string name="display_nearby_notification">ახლომდებარე შეტყობინების ჩვენება</string>
<string name="display_nearby_notification_summary">აჩვენეთ აპლიკაციაში შეტყობინება უახლოესი ადგილისთვის, რომელსაც სურათები სჭირდება</string>
<string name="list_sheet">სია</string>
<string name="storage_permission">მეხსიერების ნებართვა</string>
<string name="write_storage_permission_rationale_for_image_share">სურათების ასატვირთად, გვჭირდება წვდომა თქვენ მოწყობილობის გარე მეხსიერებაზე .</string>
<string name="nearby_notification_dismiss_message">თქვენ აღარ ნახავთ უახლოეს ადგილს, რომელსაც სურათები სჭირდება. თუმცა, თუ გსურთ, შეგიძლიათ ხელახლა ჩართოთ ეს შეტყობინება პარამეტრებში.</string>
<string name="step_count">ნაბიჯი %1$d %2$d-დან: %3$s</string>
<string name="next">შემდეგი</string>
<string name="previous">წინა</string>
<string name="upload_title_duplicate">ფაილი %1$s სახელით არსებობს. დარწმუნებული ხართ, რომ გსურთ გაგრძელება?\n\nშენიშვნა: ფაილის სახელს ავტომატურად დაემატება შესაბამისი სუფიქსი.</string>
<string name="map_application_missing">თქვენს მოწყობილობაზე შესაბამისი რუკის აპლიკაცია ვერ მოიძებნა. ამ ფუნქციის გამოსაყენებლად, გთხოვთ, დააინსტალიროთ რუკის აპლიკაცია.</string>
<string name="title_page_bookmarks_pictures">სურათები</string>
<string name="title_page_bookmarks_locations">მდებარეობები</string>
<string name="title_page_bookmarks_categories">კატეგორია</string>
<string name="menu_bookmark">სანიშნებში დამატება/წაშლა</string>
<string name="provider_bookmarks">სანიშნები</string>
<string name="bookmark_empty">თქვენ არ დაგიმატებიათ სანიშნეები</string>
<string name="provider_bookmarks_location">სანიშნეები</string>
<string name="log_collection_started">ჟურნალის შეგროვება დაიწყო. გთხოვთ, გადატვირთოთ აპლიკაცია, შეასრულოთ სასურველი მოქმედება ჟურნალში რომელიც გსურთ რომ შეინახოთ და შემდეგ კვლავ დააჭიროთ „ჟურნალის ფაილის გაგზავნას“.</string>
<string name="deletion_reason_uploaded_by_mistake">შეცდომით ავტვირთე</string>
<string name="deletion_reason_publicly_visible">არ ვიცოდი, რომ საჯაროდ გამოქვეყნდებოდა</string>
<string name="deletion_reason_bad_for_my_privacy">მივხვდი, რომ ეს ჩემს კონფიდენციალურობას აზიანებს</string>
<string name="deletion_reason_no_longer_want_public">გადავიფიქრე, აღარ მინდა, რომ საჯაროდ ჩანდეს</string>
<string name="deletion_reason_not_interesting">ბოდიშს გიხდით, ეს სურათი ენციკლოპედიისთვის საინტერესო არ არის</string>
<string name="uploaded_by_myself">ავტვირთე ჩემ მიერ %1$s ზე, გამოყენებულია სულ მცირე %2$d სტატიაში.</string>
<string name="no_uploads">კეთილი იყოს თქვენი მობრძანება Commons-ში!\n\nატვირთეთ თქვენი პირველი მედიაფაილი დამატების ღილაკზე დაჭერით.</string>
<string name="no_categories_selected">კატეგორია არ არის არჩეული</string>
<string name="no_categories_selected_warning_desc">კატეგორიების გარეშე სურათები იშვიათად გამოიყენება. დარწმუნებული ხართ, რომ გსურთ გააგრძელოთ კატეგორიების არჩევის გარეშე?</string>
<string name="no_depictions_selected">გამოსახულებები არ არის არჩეული</string>
<string name="no_depictions_selected_warning_desc">სურათებს, რომლებსაც აღწერილობითა აქვთ, უფრო ადვილად საპოვნია და უფრო ხშირად გამოიყენება. დარწმუნებული ხართ ,რომ გსურთ გააგრძელოთ გამოსახულების არჩევის გარეშე?</string>
<string name="back_button_warning">ატვირთვის გაუქმება</string>
<string name="back_button_warning_desc">უკან დაბრუნების ღილაკის გამოყენება გააუქმებს ამ ატვირთვას და თქვენ დაკარგავთ თქვენს პროგრესს</string>
<string name="back_button_continue">ატვირთვის გაგრძელება</string>
<string name="upload_flow_all_images_in_set">(ნაკრების ყველა სურათისთვის)</string>
<string name="search_this_area">ამ ტერიტორიაზე ძიება</string>
<string name="nearby_card_permission_title">ნებართვის თხოვნა</string>
<string name="nearby_card_permission_explanation">გსურთ, რომ თქვენი ამჟამინდელი მდებარეობა გამოვიყენოთ უახლოესი ადგილის საჩვენებლად, რომელსაც სურათები სჭირდება?</string>
<string name="unable_to_display_nearest_place">მდებარეობის ნებართვის გარეშე სურათების საჭიროების მქონე უახლოესი ადგილის ჩვენება შეუძლებელია</string>
<string name="never_ask_again">აღარ მკითხო</string>
<string name="display_location_permission_title">მდებარეობის ნებართვის მოთხოვნა</string>
<string name="display_location_permission_explanation">ახლომდებარე შეტყობინებების ბარათის ნახვის ფუნქციისთვის, მოითხოვეთ მდებარეობის ნებართვა.</string>
<string name="achievements_fetch_failed">რაღაც შეცდომა მოხდა, ჩვენ ვერ მოვახერხეთ მიღწევის განხილვა</string>
<string name="achievements_fetch_failed_ultimate_achievement">თქვენ იმდენი წვლილი შეიტანეთ, რომ ჩვენი მიღწევების გაანგარიშების სისტემას არ შეუძლია გაუმკლავდეს. ეს უდიდესი მიღწევაა.</string>
<string name="ends_on">მთავრდება:</string>
<string name="display_campaigns">საჩვენებელი კამპანიები</string>
<string name="display_campaigns_explanation">იხილეთ მიმდინარე კამპანიები</string>
<string name="in_app_camera_location_access_explanation">ნება მიეცით აპლიკაციას, მიიღოს მდებარეობა იმ შემთხვევაში, თუ კამერა მას არ იწერს. ზოგიერთი მოწყობილობის კამერა არ იწერს მდებარეობას. ასეთ შემთხვევებში, აპლიკაციისთვის მდებარეობის მოძიების და მასზე მიმაგრების ნებართვა თქვენს წვლილს უფრო სასარგებლოს ხდის. ამის შეცვლა ნებისმიერ დროს შეგიძლიათ პარამეტრებიდან.</string>
<string name="option_allow">დაშვება</string>
<string name="option_dismiss">უარყოფა</string>
<string name="in_app_camera_needs_location">გთხოვთ, პარამეტრებიდან ჩართოთ მდებარეობაზე წვდომა და ხელახლა სცადოთ. \n\nშენიშვნა: ატვირთულ ფაილს შესაძლოა მდებარეობა არ ჰქონდეს, თუ აპლიკაცია მოკლე დროში ვერ შეძლებს მდებარეობის მოწყობილობიდან მონაცემების მოძიებას.</string>
<string name="in_app_camera_location_permission_rationale">აპლიკაციის შიდა კამერას თქვენს სურათებზე დასამაგრებლად მდებარეობის ნებართვა სჭირდება, იმ შემთხვევაში, თუ მდებარეობა EXIF ფორმატში მიუწვდომელია. გთხოვთ, აპლიკაციას თქვენს მდებარეობაზე წვდომის უფლება მისცეთ და ხელახლა სცადოთ.\n\nშენიშვნა: ატვირთულ მასალას შესაძლოა მდებარეობა არ ჰქონდეს, თუ აპლიკაცია მოკლე დროში ვერ შეძლებს მოწყობილობიდან მდებარეობის მოძიებას.</string>
<string name="in_app_camera_location_permission_denied">აპლიკაცია არ იწერდა მდებარეობას კადრებთან ერთად მდებარეობის ნებართვის არარსებობის გამო.</string>
<string name="in_app_camera_location_unavailable">აპლიკაცია არ იწერს მდებარეობას კადრებთან ერთად, რადგან GPS გამორთულია</string>
<string name="open_document_photo_picker_title">გამოიყენეთ დოკუმენტზე დაფუძნებული ფოტო ამომრჩევი</string>
<string name="open_document_photo_picker_explanation">ახალი Android-ის ფოტოების ამომრჩევი მდებარეობის ინფორმაციის დაკარგვის რისკის ქვეშაა. ჩართეთ, თუ მას იყენებთ.</string>
<string name="getting_edit_token">რედაქტირებისთვის ტოკენის მიღება</string>
<string name="check_category_notification_title">კატეგორიის შეამოწმება %1$s-ისთვის</string>
<string name="nominate_for_deletion_done">შესრულდა</string>
<string name="please_wait">გთხოვთ, მოიცადოთ…</string>
<string name="exif_tag_name_author">ავტორი</string>
@ -386,16 +454,111 @@
<string name="exif_tag_name_lensModel">ლინზის მოდელი</string>
<string name="exif_tag_name_serialNumbers">სერიული ნომერი</string>
<string name="exif_tag_name_software">პროგრამული უზრუნველყოფა</string>
<string name="delete_helper_ask_spam_selfie" fuzzy="true">სელფი</string>
<string name="delete_helper_ask_spam_blurry" fuzzy="true">გაბუნტებულ</string>
<string name="delete_helper_ask_spam_selfie">სელფი, რომელიც არცერთ სტატიაში არ არის გამოყენებულ</string>
<string name="delete_helper_ask_spam_blurry">სრულიად ბუნდოვან</string>
<string name="delete_helper_ask_reason_copyright_internet_photo">შემთხვევითი ფოტო ინტერნეტიდან</string>
<string name="delete_helper_ask_reason_copyright_logo">ლოგო</string>
<string name="coordinates_edit_helper_show_edit_title_success">წარმატება</string>
<string name="coordinates_edit_helper_show_edit_message">კოორდინატები %1$s დამატებულია.</string>
<string name="caption_edit_helper_show_edit_message">წარწერა დამატებულია.</string>
<string name="account_created">ანგარიში შეიქმნა!</string>
<string name="theme_dark_name">მუქი</string>
<string name="theme_light_name">ღია</string>
<string name="confirm">დადასტურება</string>
<string name="leaderboard_yearly">ყოველწლიური</string>
<string name="leaderboard_weekly">ყოველკვირეული</string>
<string name="leaderboard_all_time">ყველა დროს</string>
<string name="leaderboard_upload">ატვირთვა</string>
<string name="leaderboard_nearby">ახლოს</string>
<string name="leaderboard_used">გამოყენებული</string>
<string name="limited_connection_enabled">შეზღუდული კავშირის რეჟიმი ჩართულია!</string>
<string name="limited_connection_disabled">შეზღუდული კავშირის რეჟიმი გამორთულია. მომლოდინე ატვირთვები ახლა განახლდება.</string>
<string name="limited_connection_mode">შეზღუდული კავშირის რეჟიმი</string>
<string name="depicts_step_title">ასახავს</string>
<string name="license_step_title">მედია ლიცენზია</string>
<string name="media_detail_step_title">მედიის დეტალები</string>
<string name="menu_view_category_page">კატეგორიის გვერდის ნახვა</string>
<string name="menu_view_item_page">ნივთის გვერდის ნახვა</string>
<string name="app_ui_language">აპლიკაციის მომხმარებლის ინტერფეისის ენა</string>
<string name="modify_location">ადგილმდებარეობის რედაქტირება</string>
<string name="location_picker_image_view">ადგილმდებარეობის მაჩვენებლის სურათის ნახვა</string>
<string name="location_picker_image_view_shadow">ადგილმდებარეობის მაჩვენებლის სურათის სურათის ჩრდილი</string>
<string name="image_location">სურათის მდებარეობა</string>
<string name="check_whether_location_is_correct">შეამოწმეთ, არის თუ არა ადგილმდებარეობა სწორი</string>
<string name="label">სახელი</string>
<string name="description">აღწერილობა</string>
<string name="title_page_bookmarks_items">ელემენტი</string>
<string name="custom_selector_title">მორგებული ამრჩევი</string>
<string name="custom_selector_empty_text">სურათები არ არის</string>
<string name="done">გაკეთდა</string>
<string name="back">უკან</string>
<string name="welcome_custom_selector_ok">შესანიშნავია</string>
<string name="custom_selector_dismiss_limit_warning_button_text">უარყოფა</string>
<string name="learn_more">მეტის გაგება</string>
<string name="explore_map_details">დეტალები</string>
<string name="api_level">API დონე</string>
<string name="android_version">Android-ის ვერსია</string>
<string name="device_manufacturer">მოწყობილობის მწარმოებელი</string>
<string name="device_model">მოწყობილობის მოდელი</string>
<string name="device_name">მოწყობილობის სახელი</string>
<string name="network_type">ქსელის ტიპი</string>
<string name="thanks_feedback">მადლობა, რომ გაგვიზიარეთ თქვენი აზრი</string>
<string name="error_feedback">შეცდომა შეტყობინებების გაგზავნისას</string>
<string name="enter_description">როგორია თქვენი გამოხმაურება?</string>
<string name="your_feedback">თქვენი გამოხმაურება</string>
<string name="mark_as_not_for_upload">მონიშნეთ,რომ არ არის ასატვირთი</string>
<string name="failed_to_save_gpx_file">GPX ფაილის შენახვა ვერ მოხერხდა.</string>
<string name="saving_kml_file">KML ფაილის შენახვა</string>
<string name="saving_gpx_file">GPX ფაილის შენახვა</string>
<plurals name="custom_picker_images_selected_title_appendix">
<item quantity="one">%d სურათი არჩეულია</item>
<item quantity="other"> %d სურათი არჩეულია</item>
</plurals>
<string name="multiple_files_depiction">გთხოვთ, გაითვალისწინოთ, რომ მრავალჯერადი ატვირთვისას ყველა სურათს ერთნაირი კატეგორიები და გამოსახულებები აქვს. თუ სურათებს საერთო გამოსახულებები და კატეგორიები არ აქვთ, გთხოვთ, ატვირთოთ ისინი ცალ-ცალკე.</string>
<string name="multiple_files_depiction_header">შენიშვნა მრავალჯერადი ატვირთვის შესახებ</string>
<string name="nearby_wikitalk">ამ ნივთთან დაკავშირებული პრობლემის შესახებ Wikidata-ს შეატყობინეთ</string>
<string name="please_enter_some_comments">გთხოვთ, შეიყვანოთ რამდენიმე კომენტარი</string>
<string name="talk">განხილვა</string>
<string name="write_something_about_the_item">დაწერეთ რამე „ %1$s “ ერთეულის შესახებ. ის საჯაროდ ხილული იქნება.</string>
<string name="does_not_exist_anymore_no_picture_can_ever_be_taken_of_it">„ %1$s “ აღარ არსებობს, მისი სურათის გადაღება შეუძლებელია.</string>
<string name="is_at_a_different_place_wikidata">„ %1$s “ სხვა ადგილასაა.</string>
<string name="is_at_a_different_place_please_specify_the_correct_place_below_if_possible_tell_us_the_correct_latitude_longitude">„ %1$s “ სხვა ადგილასაა. გთხოვთ, ქვემოთ მიუთითოთ სწორი ადგილი და, თუ შესაძლებელია, ჩაწეროთ სწორი განედი და გრძედი.</string>
<string name="other_problem_or_information_please_explain_below">სხვა პრობლემა ან ინფორმაცია (გთხოვთ, განმარტოთ ქვემოთ).</string>
<string name="feedback_destination_note">თქვენი გამოხმაურება გამოქვეყნდება შემდეგ ვიკი გვერდზე: &lt;a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\"&gt;Commons:მობილური აპლიკაცია/გამოხმაურება&lt;/a&gt;</string>
<string name="are_you_sure_that_you_want_cancel_all_the_uploads">დარწმუნებული ხართ, რომ გსურთ ყველა ატვირთვის გაუქმება?</string>
<string name="cancelling_all_the_uploads">ყველა ატვირთვის გაუქმება...</string>
<string name="uploads">ატვირთვები</string>
<string name="pending">განიხილება</string>
<string name="failed">ვერ მოხერხდა</string>
<string name="could_not_load_place_data">ადგილის მონაცემების ჩატვირთვა ვერ მოხერხდა</string>
<string name="custom_selector_delete_folder">ფაილის წაშლა</string>
<string name="custom_selector_confirm_deletion_title">წაშლის დადასტურება</string>
<string name="custom_selector_confirm_deletion_message">დარწმუნებული ხართ, რომ გსურთ წაშალოთ %1$s ფოლდერი , რომელიც შეიცავს %2$d ერთეულებს?</string>
<string name="custom_selector_delete">წაშლა</string>
<string name="custom_selector_cancel">გაუქმება</string>
<string name="custom_selector_folder_deleted_success">საქაღალდე %1$s წარმატებით წაიშალა</string>
<string name="custom_selector_folder_deleted_failure">საქაღალდის %1$s წაშლა ვერ მოხერხდა</string>
<string name="custom_selector_error_trashing_folder_contents">შეცდომა საქაღალდის შიგთავსის წაშლისას: %1$s</string>
<string name="custom_selector_folder_not_found_error">ვერ მოხერხდა Bucket ID-ის ფოლდერის გზის მოძიება: %1$d</string>
<string name="red_pin">ამ ადგილს ჯერ არ აქვს სურათი, წადი და გადაიღე!</string>
<string name="green_pin">ამ ადგილს უკვე აქვს ფოტო</string>
<string name="grey_pin">ახლა ვამოწმებ, აქვს თუ არა ამ ადგილს სურათი.</string>
<string name="error_while_loading">შეცდომა ჩატვირთვისას</string>
<string name="no_usages_found">არ არის ნაპოვნი გამოყენება</string>
<string name="usages_on_commons_heading">ვიკისაწყობი</string>
<string name="usages_on_other_wikis_heading">სხვა ვიკისები</string>
<string name="file_usages_container_heading">ფაილის გამოყენება</string>
<string name="title_activity_single_web_view">SingleWebViewActivity</string>
<string name="account">ანგარიში</string>
<string name="vanish_account">ანგარიშის გაქრობა</string>
<string name="account_vanish_request_confirm_title">ანგარიშის გაქრობის გაფრთხილება</string>
<string name="account_vanish_request_confirm">გაქრობა &lt;b&gt;უკიდურესი საშუალებაა&lt;/b&gt; და &lt;b&gt;მხოლოდ მაშინ უნდა გამოიყენოთ, როდესაც გსურთ რედაქტირების სამუდამოდ შეწყვეტა&lt;/b&gt; და ასევე თქვენი წარსული ასოციაციების რაც შეიძლება მეტი დამალვა.&lt;br/&gt;&lt;br/&gt; ვიკიმედიის საერთო სივრცეში ანგარიშის წაშლა ხდება თქვენი ანგარიშის სახელის შეცვლით, რათა სხვებმა ვერ ამოიცნონ თქვენი წვლილი ანგარიშის გაქრობის სახელით ცნობილი პროცესით. &lt;b&gt;გაქრობა არ იძლევა სრულ ანონიმურობას ან პროექტებში წვლილის წაშლას&lt;/b&gt; .</string>
<string name="caption">წარწერა</string>
<string name="caption_copied_to_clipboard">წარწერა კოპირებულია ბუფერში</string>
<string name="congratulations_all_pictures_in_this_album_have_been_either_uploaded_or_marked_as_not_for_upload">გილოცავთ, ამ ალბომში არსებული ყველა სურათი ან აიტვირთა, ან მონიშნულია, როგორც ასატვირთად შეუძლებელი.</string>
<string name="show_in_explore">ჩვენება Explore-ში</string>
<string name="show_in_nearby">ახლომდებარე სივრცეში ჩვენება</string>
<string name="image_tag_line_created_and_uploaded_by">შექმნილი და ატვირთულია: %1$s მიერ</string>
<string name="image_tag_line_created_by_and_uploaded_by">შექმნილია %1$s მიერ და ატვირთულია %2$s მიერ</string>
<string name="nominated_for_deletion_btn">წაშლაზე ნომინირება</string>
</resources>

View file

@ -2,6 +2,7 @@
<!-- Authors:
* Anoop rao
* Anzx
* Gopala Krishna A
* Mahadevaiah Siddaiah
* Omshivaprakash
* VASANTH S.N.
@ -10,8 +11,23 @@
-->
<resources>
<string name="commons_facebook">ಕಾಮನ್ಸ್ ಫೇಸ್ಬುಕ್ ಪುಟ</string>
<string name="commons_github">ಕಾಮನ್ಸ್‌ನ ಗಿಟ್‍ಹಬ್ ಮೂಲ ಕೋಡ್</string>
<string name="commons_logo">ಕಾಮನ್ಸ್‌ ಲಾಂಛನ</string>
<string name="commons_website">ಕಾಮನ್ಸ್ ಜಾಲತಾಣ</string>
<string name="exit_location_picker">ಸ್ಥಳ ಆಯ್ಕೆಯಿಂದ ನಿರ್ಗಮಿಸಿ</string>
<string name="submit">ಸಲ್ಲಿಸಿ</string>
<string name="add_another_description">ಇನ್ನೊಂದು ವಿವರಣೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="add_new_contribution">ಹೊಸ ಕೊಡುಗೆಗಳನ್ನು ಸೇರಿಸಿ</string>
<string name="add_contribution_from_camera">ಕ್ಯಾಮೆರಾದಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="add_contribution_from_photos">ಫೋಟೋಗಳಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="add_contribution_from_contributions_gallery">ಹಿಂದಿನ ಕೊಡುಗೆಗಳ ಗ್ಯಾಲರಿಯಿಂದ ಕೊಡುಗೆಯನ್ನು ಸೇರಿಸಿ</string>
<string name="show_captions">ತಲೆಬರಹ</string>
<string name="row_item_language_description">ಭಾಷಾ ವಿವರಣೆ</string>
<string name="row_item_caption">ತಲೆಬರಹ</string>
<string name="show_captions_description">ವಿವರಣೆ</string>
<string name="nearby_row_image">ಚಿತ್ರ</string>
<string name="nearby_all">ಎಲ್ಲಾ</string>
<string name="nearby_filter_toggle">ಮೇಲಕ್ಕೆ ಟಾಗಲ್ ಮಾಡಿ</string>
<string name="appwidget_img">ದಿನದ ಚಿತ್ರ</string>
<plurals name="uploads_pending_notification_indicator">
<item quantity="one">%1$d ಕಡತ ಅಪ್ಲೋಡ್ ಅಗುತ್ತಿದೆ</item>

View file

@ -222,6 +222,7 @@
<string name="media_detail_description">설명</string>
<string name="media_detail_discussion">토론</string>
<string name="media_detail_author">저자</string>
<string name="media_detail_uploader">올린 사람</string>
<string name="media_detail_uploaded_date">올린 날짜</string>
<string name="media_detail_license">라이선스</string>
<string name="media_detail_coordinates">좌표</string>
@ -379,7 +380,7 @@
<string name="statistics_featured">알찬 그림</string>
<string name="statistics_wikidata_edits">\"주변 장소\" 경유 이미지</string>
<string name="level">레벨 %d</string>
<string name="profileLevel">%s (레벨 %s)</string>
<string name="profile_withLevel">%s (레벨 %s)</string>
<string name="images_uploaded">사진 업로드됨</string>
<string name="images_used_by_wiki">사용된 이미지</string>
<string name="achievements_share_message">친구와 성과를 공유하세요!</string>
@ -538,6 +539,8 @@
<string name="title_for_media">미디어</string>
<string name="title_for_child_classes">자식 클래스</string>
<string name="title_for_parent_classes">상위 클래스</string>
<string name="title_for_subcategories">하위 분류</string>
<string name="title_for_parent_categories">상위 분류</string>
<string name="upload_nearby_place_found_title">주변 장소 발견</string>
<string name="upload_nearby_place_found_description_plural">%1$s의 사진이 맞습니까?</string>
<string name="upload_nearby_place_found_description_singular">%1$s의 사진이 맞습니까?</string>
@ -582,8 +585,10 @@
<string name="menu_set_avatar">아바타로 설정</string>
<string name="leaderboard_yearly">매년</string>
<string name="leaderboard_weekly">매주</string>
<string name="leaderboard_all_time">항상</string>
<string name="leaderboard_upload">업로드</string>
<string name="leaderboard_nearby">근처</string>
<string name="leaderboard_used">사용됨</string>
<string name="leaderboard_my_rank_button_text">내 순위</string>
<string name="limited_connection_mode">제한된 연결 모드</string>
<string name="statistics_quality">고품질 사진</string>
@ -671,7 +676,7 @@
<string name="permissions_are_required_for_functionality">기능에 대한 권한이 필요합니다</string>
<string name="learn_how_to_write_a_useful_description">유용한 설명을 추가하는 법 알아보기</string>
<string name="learn_how_to_write_a_useful_caption">유용한 캡션을 추가하는 법 알아보기</string>
<string name="see_your_achievements">업적 보기</string>
<string name="see_your_achievements" fuzzy="true">업적 보기</string>
<string name="edit_image">그림 편집</string>
<string name="edit_location">위치 편집</string>
<string name="location_updated">위치가 갱신되었습니다!</string>

Some files were not shown because too many files have changed in this diff Show more