mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
Merge branch 'main' into added-button
This commit is contained in:
commit
2cfdac42f1
44 changed files with 1292 additions and 1269 deletions
|
|
@ -22,6 +22,7 @@ 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;
|
||||
|
|
@ -29,7 +30,7 @@ import timber.log.Timber;
|
|||
|
||||
public class BookmarkListRootFragment extends CommonsDaggerSupportFragment implements
|
||||
FragmentManager.OnBackStackChangedListener,
|
||||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
MediaDetailProvider,
|
||||
AdapterView.OnItemClickListener, CategoryImagesCallback {
|
||||
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import android.view.Menu
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.activity.viewModels
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
|
@ -22,6 +21,7 @@ import fr.free.nrw.commons.explore.categories.media.CategoriesMediaFragment
|
|||
import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesFragment
|
||||
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesFragment
|
||||
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.handleWebUrl
|
||||
import fr.free.nrw.commons.wikidata.model.WikiSite
|
||||
|
|
@ -36,7 +36,7 @@ import javax.inject.Inject
|
|||
* a particular category on wikimedia commons.
|
||||
*/
|
||||
class CategoryDetailsActivity : BaseActivity(),
|
||||
MediaDetailPagerFragment.MediaDetailProvider,
|
||||
MediaDetailProvider,
|
||||
CategoryImagesCallback {
|
||||
|
||||
private lateinit var supportFragmentManager: FragmentManager
|
||||
|
|
|
|||
|
|
@ -43,7 +43,7 @@ import fr.free.nrw.commons.location.LatLng
|
|||
import fr.free.nrw.commons.location.LocationServiceManager
|
||||
import fr.free.nrw.commons.location.LocationUpdateListener
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.nearby.NearbyController
|
||||
import fr.free.nrw.commons.nearby.NearbyNotificationCardView
|
||||
|
|
@ -72,7 +72,6 @@ import io.reactivex.disposables.CompositeDisposable
|
|||
import io.reactivex.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.os.Bundle;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
|
@ -17,10 +16,11 @@ 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
|
||||
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
private CategoriesMediaFragment listFragment;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import android.os.Bundle;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
|
@ -17,10 +16,11 @@ 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
|
||||
MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
private ExploreMapFragment mapFragment;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import android.os.Bundle;
|
|||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
|
|
@ -23,18 +22,14 @@ 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.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import javax.inject.Inject;
|
||||
import kotlin.Pair;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
|
|
@ -42,7 +37,7 @@ import timber.log.Timber;
|
|||
*/
|
||||
|
||||
public class SearchActivity extends BaseActivity
|
||||
implements MediaDetailPagerFragment.MediaDetailProvider, CategoryImagesCallback {
|
||||
implements MediaDetailProvider, CategoryImagesCallback {
|
||||
|
||||
@Inject
|
||||
RecentSearchesDao recentSearchesDao;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import android.view.Menu;
|
|||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
|
@ -24,6 +23,7 @@ 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;
|
||||
|
|
@ -31,16 +31,12 @@ import fr.free.nrw.commons.wikidata.WikidataConstants;
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import kotlin.Pair;
|
||||
|
||||
/**
|
||||
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
|
||||
*/
|
||||
public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailPagerFragment.MediaDetailProvider,
|
||||
public class WikidataItemDetailsActivity extends BaseActivity implements MediaDetailProvider,
|
||||
CategoryImagesCallback {
|
||||
private FragmentManager supportFragmentManager;
|
||||
private DepictedImagesFragment depictionImagesListFragment;
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import fr.free.nrw.commons.MediaDataExtractor
|
|||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback
|
||||
import fr.free.nrw.commons.explore.paging.BasePagingFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||
import javax.inject.Inject
|
||||
|
||||
abstract class PageableMediaFragment :
|
||||
|
|
|
|||
|
|
@ -256,7 +256,9 @@ object FilePicker : Constants {
|
|||
*
|
||||
*/
|
||||
val intent = if (openDocumentIntentPreferred) {
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
}
|
||||
} else {
|
||||
Intent(Intent.ACTION_GET_CONTENT)
|
||||
}
|
||||
|
|
@ -271,6 +273,7 @@ object FilePicker : Constants {
|
|||
callbacks: Callbacks
|
||||
) {
|
||||
if (result.resultCode == Activity.RESULT_OK && !isPhoto(result.data)) {
|
||||
takePersistableUriPermissions(activity, result)
|
||||
try {
|
||||
val files = getFilesFromGalleryPictures(result.data, activity)
|
||||
callbacks.onImagesPicked(files, ImageSource.DOCUMENTS, restoreType(activity))
|
||||
|
|
@ -283,6 +286,23 @@ object FilePicker : Constants {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* takePersistableUriPermission is necessary to persist the URI permission as
|
||||
* the permission granted by the system for read or write access on ACTION_OPEN_DOCUMENT
|
||||
* lasts only until the user's device restarts.
|
||||
* Ref: https://developer.android.com/training/data-storage/shared/documents-files#persist-permissions
|
||||
*
|
||||
* This helps fix the SecurityException reported in this issue:
|
||||
* https://github.com/commons-app/apps-android-commons/issues/6357
|
||||
*/
|
||||
private fun takePersistableUriPermissions(context: Context, result: ActivityResult) {
|
||||
result.data?.data?.also { uri ->
|
||||
val takeFlags: Int = (Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
context.contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onPictureReturnedFromCustomSelector.
|
||||
* Retrieve and forward the images to upload wizard through callback.
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing Captions when fetching captions using filename in MediaClient
|
||||
*/
|
||||
public class Caption {
|
||||
|
||||
/**
|
||||
* users language in which caption is written
|
||||
*/
|
||||
@SerializedName("language")
|
||||
private String language;
|
||||
@SerializedName("value")
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public Caption() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value
|
||||
* @param language
|
||||
*/
|
||||
public Caption(String language, String value) {
|
||||
super();
|
||||
this.language = language;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@SerializedName("language")
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
@SerializedName("value")
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
19
app/src/main/java/fr/free/nrw/commons/media/Caption.kt
Normal file
19
app/src/main/java/fr/free/nrw/commons/media/Caption.kt
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/**
|
||||
* Model class for parsing Captions when fetching captions using filename in MediaClient
|
||||
*/
|
||||
class Caption() {
|
||||
@SerializedName("language")
|
||||
var language: String? = null
|
||||
|
||||
@SerializedName("value")
|
||||
var value: String? = null
|
||||
|
||||
constructor(language: String?, value: String?) : this() {
|
||||
this.language = language
|
||||
this.value = value
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.TextView;
|
||||
import fr.free.nrw.commons.R;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Adapter for Caption Listview
|
||||
*/
|
||||
public class CaptionListViewAdapter extends BaseAdapter {
|
||||
|
||||
List<Caption> captions;
|
||||
|
||||
public CaptionListViewAdapter(final List<Caption> captions) {
|
||||
this.captions = captions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return size of captions list
|
||||
*/
|
||||
@Override
|
||||
public int getCount() {
|
||||
return captions.size();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Object at position i
|
||||
*/
|
||||
@Override
|
||||
public Object getItem(final int i) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return id for current item
|
||||
*/
|
||||
@Override
|
||||
public long getItemId(final int i) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* inflate the view and bind data with UI
|
||||
*/
|
||||
@Override
|
||||
public View getView(final int i, final View view, final ViewGroup viewGroup) {
|
||||
final TextView captionLanguageTextView;
|
||||
final TextView captionTextView;
|
||||
final View captionLayout = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.caption_item, null);
|
||||
captionLanguageTextView = captionLayout.findViewById(R.id.caption_language_textview);
|
||||
captionTextView = captionLayout.findViewById(R.id.caption_text);
|
||||
if (captions.size() == 1 && captions.get(0).getValue().equals("No Caption")) {
|
||||
captionLanguageTextView.setText(captions.get(i).getLanguage());
|
||||
captionTextView.setText(captions.get(i).getValue());
|
||||
} else {
|
||||
captionLanguageTextView.setText(captions.get(i).getLanguage() + ":");
|
||||
captionTextView.setText(captions.get(i).getValue());
|
||||
}
|
||||
|
||||
return captionLayout;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.TextView
|
||||
import fr.free.nrw.commons.R
|
||||
|
||||
/**
|
||||
* Adapter for Caption Listview
|
||||
*/
|
||||
class CaptionListViewAdapter(var captions: List<Caption>) : BaseAdapter() {
|
||||
override fun getCount(): Int = captions.size
|
||||
|
||||
override fun getItem(i: Int): Any? = null
|
||||
|
||||
override fun getItemId(i: Int): Long = 0
|
||||
|
||||
override fun getView(i: Int, view: View, viewGroup: ViewGroup): View {
|
||||
val captionLayout = LayoutInflater.from(viewGroup.context).inflate(R.layout.caption_item, null)
|
||||
val captionLanguageTextView = captionLayout.findViewById<TextView>(R.id.caption_language_textview)
|
||||
val captionTextView = captionLayout.findViewById<TextView>(R.id.caption_text)
|
||||
if (captions.size == 1 && captions[0].value == "No Caption") {
|
||||
captionLanguageTextView.text = captions[i].language
|
||||
captionTextView.text = captions[i].value
|
||||
} else {
|
||||
captionLanguageTextView.text = captions[i].language + ":"
|
||||
captionTextView.text = captions[i].value
|
||||
}
|
||||
|
||||
return captionLayout
|
||||
}
|
||||
}
|
||||
|
|
@ -1,76 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* Represents the Wikibase item associated with a Wikimedia Commons file.
|
||||
* For instance the Wikibase item M63996 represents the Commons file "Paul Cézanne - The Pigeon Tower at Bellevue - 1936.19 - Cleveland Museum of Art.jpg"
|
||||
*/
|
||||
public class CommonsWikibaseItem {
|
||||
|
||||
@SerializedName("type")
|
||||
private String type;
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
@SerializedName("labels")
|
||||
private Map<String, Caption> labels;
|
||||
@SerializedName("statements")
|
||||
private Object statements = null;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public CommonsWikibaseItem() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* @param statements
|
||||
* @param labels
|
||||
* @param type
|
||||
*/
|
||||
public CommonsWikibaseItem(String type, String id, Map<String, Caption> labels, Object statements) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.labels = labels;
|
||||
this.statements = statements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ex: "mediainfo
|
||||
*/
|
||||
@SerializedName("type")
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Wikibase Id
|
||||
*/
|
||||
@SerializedName("id")
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return value of captions
|
||||
*/
|
||||
@SerializedName("labels")
|
||||
public Map<String, Caption> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the Depicts item
|
||||
*/
|
||||
@SerializedName("statements")
|
||||
public Object getStatements() {
|
||||
return statements;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,236 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.facebook.imagepipeline.common.BytesRange;
|
||||
import com.facebook.imagepipeline.image.EncodedImage;
|
||||
import com.facebook.imagepipeline.producers.BaseNetworkFetcher;
|
||||
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks;
|
||||
import com.facebook.imagepipeline.producers.Consumer;
|
||||
import com.facebook.imagepipeline.producers.FetchState;
|
||||
import com.facebook.imagepipeline.producers.NetworkFetcher;
|
||||
import com.facebook.imagepipeline.producers.ProducerContext;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.Call;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import timber.log.Timber;
|
||||
|
||||
// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled
|
||||
// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java
|
||||
@Singleton
|
||||
public class CustomOkHttpNetworkFetcher
|
||||
extends BaseNetworkFetcher<CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState> {
|
||||
|
||||
private static final String QUEUE_TIME = "queue_time";
|
||||
private static final String FETCH_TIME = "fetch_time";
|
||||
private static final String TOTAL_TIME = "total_time";
|
||||
private static final String IMAGE_SIZE = "image_size";
|
||||
private final Call.Factory mCallFactory;
|
||||
private final @Nullable
|
||||
CacheControl mCacheControl;
|
||||
private final Executor mCancellationExecutor;
|
||||
private final JsonKvStore defaultKvStore;
|
||||
|
||||
/**
|
||||
* @param okHttpClient client to use
|
||||
*/
|
||||
@Inject
|
||||
public CustomOkHttpNetworkFetcher(final OkHttpClient okHttpClient,
|
||||
@Named("default_preferences") final JsonKvStore defaultKvStore) {
|
||||
this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory custom {@link Call.Factory} for fetching image from the network
|
||||
* @param cancellationExecutor executor on which fetching cancellation is performed if
|
||||
* cancellation is requested from the UI Thread
|
||||
*/
|
||||
public CustomOkHttpNetworkFetcher(final Call.Factory callFactory,
|
||||
final Executor cancellationExecutor,
|
||||
final JsonKvStore defaultKvStore) {
|
||||
this(callFactory, cancellationExecutor, defaultKvStore, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param callFactory custom {@link Call.Factory} for fetching image from the network
|
||||
* @param cancellationExecutor executor on which fetching cancellation is performed if
|
||||
* cancellation is requested from the UI Thread
|
||||
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
|
||||
*/
|
||||
public CustomOkHttpNetworkFetcher(
|
||||
final Call.Factory callFactory, final Executor cancellationExecutor,
|
||||
final JsonKvStore defaultKvStore,
|
||||
final boolean disableOkHttpCache) {
|
||||
this.defaultKvStore = defaultKvStore;
|
||||
mCallFactory = callFactory;
|
||||
mCancellationExecutor = cancellationExecutor;
|
||||
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OkHttpNetworkFetchState createFetchState(
|
||||
final Consumer<EncodedImage> consumer, final ProducerContext context) {
|
||||
return new OkHttpNetworkFetchState(consumer, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fetch(
|
||||
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
|
||||
fetchState.submitTime = SystemClock.elapsedRealtime();
|
||||
final Uri uri = fetchState.getUri();
|
||||
|
||||
try {
|
||||
if (defaultKvStore
|
||||
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
|
||||
Timber.d("Skipping loading of image as limited connection mode is enabled");
|
||||
callback.onFailure(
|
||||
new Exception("Failing image request as limited connection mode is enabled"));
|
||||
return;
|
||||
}
|
||||
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
|
||||
|
||||
if (mCacheControl != null) {
|
||||
requestBuilder.cacheControl(mCacheControl);
|
||||
}
|
||||
|
||||
final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
|
||||
if (bytesRange != null) {
|
||||
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
|
||||
}
|
||||
|
||||
fetchWithRequest(fetchState, callback, requestBuilder.build());
|
||||
} catch (final Exception e) {
|
||||
// handle error while creating the request
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchCompletion(final OkHttpNetworkFetchState fetchState, final int byteSize) {
|
||||
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getExtraMap(final OkHttpNetworkFetchState fetchState,
|
||||
final int byteSize) {
|
||||
final Map<String, String> extraMap = new HashMap<>(4);
|
||||
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
|
||||
extraMap
|
||||
.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
|
||||
extraMap
|
||||
.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
|
||||
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
|
||||
return extraMap;
|
||||
}
|
||||
|
||||
protected void fetchWithRequest(
|
||||
final OkHttpNetworkFetchState fetchState,
|
||||
final NetworkFetcher.Callback callback,
|
||||
final Request request) {
|
||||
final Call call = mCallFactory.newCall(request);
|
||||
|
||||
fetchState
|
||||
.getContext()
|
||||
.addCallbacks(
|
||||
new BaseProducerContextCallbacks() {
|
||||
@Override
|
||||
public void onCancellationRequested() {
|
||||
onFetchCancellationRequested(call);
|
||||
}
|
||||
});
|
||||
|
||||
call.enqueue(
|
||||
new okhttp3.Callback() {
|
||||
@Override
|
||||
public void onResponse(final Call call, final Response response) {
|
||||
onFetchResponse(fetchState, call, response, callback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Call call, final IOException e) {
|
||||
handleException(call, e, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void onFetchCancellationRequested(final Call call) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
call.cancel();
|
||||
} else {
|
||||
mCancellationExecutor.execute(call::cancel);
|
||||
}
|
||||
}
|
||||
|
||||
private void onFetchResponse(final OkHttpNetworkFetchState fetchState, final Call call,
|
||||
final Response response,
|
||||
final NetworkFetcher.Callback callback) {
|
||||
fetchState.responseTime = SystemClock.elapsedRealtime();
|
||||
try (final ResponseBody body = response.body()) {
|
||||
if (!response.isSuccessful()) {
|
||||
handleException(
|
||||
call, new IOException("Unexpected HTTP code " + response),
|
||||
callback);
|
||||
return;
|
||||
}
|
||||
|
||||
final BytesRange responseRange =
|
||||
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
|
||||
if (responseRange != null
|
||||
&& !(responseRange.from == 0
|
||||
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
|
||||
// Only treat as a partial image if the range is not all of the content
|
||||
fetchState.setResponseBytesRange(responseRange);
|
||||
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
|
||||
}
|
||||
|
||||
long contentLength = body.contentLength();
|
||||
if (contentLength < 0) {
|
||||
contentLength = 0;
|
||||
}
|
||||
callback.onResponse(body.byteStream(), (int) contentLength);
|
||||
} catch (final Exception e) {
|
||||
handleException(call, e, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exceptions.
|
||||
*
|
||||
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught
|
||||
* after request cancellation, then the exception is interpreted as successful cancellation and
|
||||
* onCancellation is called. Otherwise onFailure is called.
|
||||
*/
|
||||
private void handleException(final Call call, final Exception e, final Callback callback) {
|
||||
if (call.isCanceled()) {
|
||||
callback.onCancellation();
|
||||
} else {
|
||||
callback.onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class OkHttpNetworkFetchState extends FetchState {
|
||||
|
||||
public long submitTime;
|
||||
public long responseTime;
|
||||
public long fetchCompleteTime;
|
||||
|
||||
public OkHttpNetworkFetchState(
|
||||
final Consumer<EncodedImage> consumer, final ProducerContext producerContext) {
|
||||
super(consumer, producerContext);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,199 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import com.facebook.imagepipeline.common.BytesRange
|
||||
import com.facebook.imagepipeline.image.EncodedImage
|
||||
import com.facebook.imagepipeline.producers.BaseNetworkFetcher
|
||||
import com.facebook.imagepipeline.producers.BaseProducerContextCallbacks
|
||||
import com.facebook.imagepipeline.producers.Consumer
|
||||
import com.facebook.imagepipeline.producers.FetchState
|
||||
import com.facebook.imagepipeline.producers.NetworkFetcher
|
||||
import com.facebook.imagepipeline.producers.ProducerContext
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executor
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
// Custom implementation of Fresco's Network fetcher to skip downloading of images when limited connection mode is enabled
|
||||
// https://github.com/facebook/fresco/blob/master/imagepipeline-backends/imagepipeline-okhttp3/src/main/java/com/facebook/imagepipeline/backends/okhttp3/OkHttpNetworkFetcher.java
|
||||
@Singleton
|
||||
class CustomOkHttpNetworkFetcher
|
||||
@JvmOverloads constructor(
|
||||
private val mCallFactory: Call.Factory,
|
||||
private val mCancellationExecutor: Executor,
|
||||
private val defaultKvStore: JsonKvStore,
|
||||
disableOkHttpCache: Boolean = true
|
||||
) : BaseNetworkFetcher<OkHttpNetworkFetchState>() {
|
||||
|
||||
private val mCacheControl =
|
||||
if (disableOkHttpCache) CacheControl.Builder().noStore().build() else null
|
||||
private val isLimitedConnectionMode: Boolean
|
||||
get() = defaultKvStore.getBoolean(
|
||||
CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED,
|
||||
false
|
||||
)
|
||||
|
||||
/**
|
||||
* @param okHttpClient client to use
|
||||
*/
|
||||
@Inject
|
||||
constructor(
|
||||
okHttpClient: OkHttpClient,
|
||||
@Named("default_preferences") defaultKvStore: JsonKvStore
|
||||
) : this(okHttpClient, okHttpClient.dispatcher.executorService, defaultKvStore)
|
||||
|
||||
/**
|
||||
* @param mCallFactory custom [Call.Factory] for fetching image from the network
|
||||
* @param mCancellationExecutor executor on which fetching cancellation is performed if
|
||||
* cancellation is requested from the UI Thread
|
||||
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
|
||||
*/
|
||||
override fun createFetchState(consumer: Consumer<EncodedImage>, context: ProducerContext) =
|
||||
OkHttpNetworkFetchState(consumer, context)
|
||||
|
||||
override fun fetch(
|
||||
fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback
|
||||
) {
|
||||
fetchState.submitTime = SystemClock.elapsedRealtime()
|
||||
|
||||
try {
|
||||
if (isLimitedConnectionMode) {
|
||||
Timber.d("Skipping loading of image as limited connection mode is enabled")
|
||||
callback.onFailure(Exception("Failing image request as limited connection mode is enabled"))
|
||||
return
|
||||
}
|
||||
|
||||
val requestBuilder = Request.Builder().url(fetchState.uri.toString()).get()
|
||||
|
||||
if (mCacheControl != null) {
|
||||
requestBuilder.cacheControl(mCacheControl)
|
||||
}
|
||||
|
||||
val bytesRange = fetchState.context.imageRequest.bytesRange
|
||||
if (bytesRange != null) {
|
||||
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue())
|
||||
}
|
||||
|
||||
fetchWithRequest(fetchState, callback, requestBuilder.build())
|
||||
} catch (e: Exception) {
|
||||
// handle error while creating the request
|
||||
callback.onFailure(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFetchCompletion(fetchState: OkHttpNetworkFetchState, byteSize: Int) {
|
||||
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime()
|
||||
}
|
||||
|
||||
override fun getExtraMap(fetchState: OkHttpNetworkFetchState, byteSize: Int) =
|
||||
fetchState.toExtraMap(byteSize)
|
||||
|
||||
private fun fetchWithRequest(
|
||||
fetchState: OkHttpNetworkFetchState, callback: NetworkFetcher.Callback, request: Request
|
||||
) {
|
||||
val call = mCallFactory.newCall(request)
|
||||
|
||||
fetchState.context.addCallbacks(object : BaseProducerContextCallbacks() {
|
||||
override fun onCancellationRequested() {
|
||||
onFetchCancellationRequested(call)
|
||||
}
|
||||
})
|
||||
|
||||
call.enqueue(object : Callback {
|
||||
override fun onResponse(call: Call, response: Response) =
|
||||
onFetchResponse(fetchState, call, response, callback)
|
||||
|
||||
override fun onFailure(call: Call, e: IOException) =
|
||||
handleException(call, e, callback)
|
||||
})
|
||||
}
|
||||
|
||||
private fun onFetchCancellationRequested(call: Call) {
|
||||
if (Looper.myLooper() != Looper.getMainLooper()) {
|
||||
call.cancel()
|
||||
} else {
|
||||
mCancellationExecutor.execute { call.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun onFetchResponse(
|
||||
fetchState: OkHttpNetworkFetchState,
|
||||
call: Call,
|
||||
response: Response,
|
||||
callback: NetworkFetcher.Callback
|
||||
) {
|
||||
fetchState.responseTime = SystemClock.elapsedRealtime()
|
||||
try {
|
||||
response.body.use { body ->
|
||||
if (!response.isSuccessful) {
|
||||
handleException(call, IOException("Unexpected HTTP code $response"), callback)
|
||||
return
|
||||
}
|
||||
val responseRange =
|
||||
BytesRange.fromContentRangeHeader(response.header("Content-Range"))
|
||||
if (responseRange != null && !(responseRange.from == 0 && responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
|
||||
// Only treat as a partial image if the range is not all of the content
|
||||
fetchState.responseBytesRange = responseRange
|
||||
fetchState.onNewResultStatusFlags = Consumer.IS_PARTIAL_RESULT
|
||||
}
|
||||
|
||||
var contentLength = body!!.contentLength()
|
||||
if (contentLength < 0) {
|
||||
contentLength = 0
|
||||
}
|
||||
callback.onResponse(body.byteStream(), contentLength.toInt())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
handleException(call, e, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exceptions.
|
||||
*
|
||||
* OkHttp notifies callers of cancellations via an IOException. If IOException is caught
|
||||
* after request cancellation, then the exception is interpreted as successful cancellation and
|
||||
* onCancellation is called. Otherwise onFailure is called.
|
||||
*/
|
||||
private fun handleException(call: Call, e: Exception, callback: NetworkFetcher.Callback) {
|
||||
if (call.isCanceled()) {
|
||||
callback.onCancellation()
|
||||
} else {
|
||||
callback.onFailure(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OkHttpNetworkFetchState(
|
||||
consumer: Consumer<EncodedImage>?, producerContext: ProducerContext?
|
||||
) : FetchState(consumer, producerContext) {
|
||||
var submitTime: Long = 0
|
||||
var responseTime: Long = 0
|
||||
var fetchCompleteTime: Long = 0
|
||||
|
||||
fun toExtraMap(byteSize: Int) = buildMap {
|
||||
put(QUEUE_TIME, (responseTime - submitTime).toString())
|
||||
put(FETCH_TIME, (fetchCompleteTime - responseTime).toString())
|
||||
put(TOTAL_TIME, (fetchCompleteTime - submitTime).toString())
|
||||
put(IMAGE_SIZE, byteSize.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val QUEUE_TIME = "queue_time"
|
||||
private const val FETCH_TIME = "fetch_time"
|
||||
private const val TOTAL_TIME = "total_time"
|
||||
private const val IMAGE_SIZE = "image_size"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
import fr.free.nrw.commons.media.MediaDetailFragment.Companion.forMedia
|
||||
import timber.log.Timber
|
||||
|
||||
// FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
|
||||
class MediaDetailAdapter(
|
||||
val mediaDetailPagerFragment: MediaDetailPagerFragment,
|
||||
fm: FragmentManager
|
||||
) : FragmentStatePagerAdapter(fm) {
|
||||
/**
|
||||
* Keeps track of the current displayed fragment.
|
||||
*/
|
||||
private var currentFragment: Fragment? = null
|
||||
|
||||
override fun getItem(i: Int): Fragment {
|
||||
if (i == 0) {
|
||||
// See bug https://code.google.com/p/android/issues/detail?id=27526
|
||||
if (mediaDetailPagerFragment.activity == null) {
|
||||
Timber.d("Skipping getItem. Returning as activity is destroyed!")
|
||||
return Fragment()
|
||||
}
|
||||
mediaDetailPagerFragment.binding!!.mediaDetailsPager.postDelayed(
|
||||
{ mediaDetailPagerFragment.requireActivity().invalidateOptionsMenu() }, 5
|
||||
)
|
||||
}
|
||||
return if (mediaDetailPagerFragment.isFromFeaturedRootFragment) {
|
||||
forMedia(
|
||||
mediaDetailPagerFragment.position + i,
|
||||
mediaDetailPagerFragment.editable, mediaDetailPagerFragment.isFeaturedImage,
|
||||
mediaDetailPagerFragment.isWikipediaButtonDisplayed
|
||||
)
|
||||
} else {
|
||||
forMedia(
|
||||
i, mediaDetailPagerFragment.editable,
|
||||
mediaDetailPagerFragment.isFeaturedImage,
|
||||
mediaDetailPagerFragment.isWikipediaButtonDisplayed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
if (mediaDetailPagerFragment.activity == null) {
|
||||
Timber.d("Skipping getCount. Returning as activity is destroyed!")
|
||||
return 0
|
||||
}
|
||||
return mediaDetailPagerFragment.mediaDetailProvider!!.getTotalMediaCount()
|
||||
}
|
||||
|
||||
/**
|
||||
* If current fragment is of type MediaDetailFragment, return it, otherwise return null.
|
||||
*
|
||||
* @return MediaDetailFragment
|
||||
*/
|
||||
val currentMediaDetailFragment: MediaDetailFragment?
|
||||
get() = currentFragment as? MediaDetailFragment
|
||||
|
||||
/**
|
||||
* Called to inform the adapter of which item is currently considered to be the "primary", that
|
||||
* is the one show to the user as the current page.
|
||||
*/
|
||||
override fun setPrimaryItem(
|
||||
container: ViewGroup, position: Int,
|
||||
obj: Any
|
||||
) {
|
||||
// Update the current fragment if changed
|
||||
if (currentFragment !== obj) {
|
||||
currentFragment = (obj as Fragment)
|
||||
}
|
||||
super.setPrimaryItem(container, position, obj)
|
||||
}
|
||||
}
|
||||
|
|
@ -77,7 +77,6 @@ import fr.free.nrw.commons.CommonsApplication.Companion.instance
|
|||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.MediaDataExtractor
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.utils.UnderlineUtils
|
||||
import fr.free.nrw.commons.actions.ThanksClient
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
|
||||
|
|
@ -102,7 +101,6 @@ import fr.free.nrw.commons.kvstore.JsonKvStore
|
|||
import fr.free.nrw.commons.language.AppLanguageLookUpTable
|
||||
import fr.free.nrw.commons.location.LocationServiceManager
|
||||
import fr.free.nrw.commons.locationpicker.LocationPicker
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider
|
||||
import fr.free.nrw.commons.profile.ProfileActivity
|
||||
import fr.free.nrw.commons.review.ReviewHelper
|
||||
import fr.free.nrw.commons.settings.Prefs
|
||||
|
|
@ -322,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
|
||||
|
|
@ -816,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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,678 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import static fr.free.nrw.commons.utils.UrlUtilsKt.handleWebUrl;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.widget.ProgressBar;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.net.Uri;
|
||||
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 android.widget.Toast;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.utils.ClipboardUtils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.bookmarks.models.Bookmark;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.profile.ProfileActivity;
|
||||
import fr.free.nrw.commons.utils.DownloadUtils;
|
||||
import fr.free.nrw.commons.utils.ImageUtils;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.Callable;
|
||||
import javax.inject.Inject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment implements ViewPager.OnPageChangeListener, MediaDetailFragment.Callback {
|
||||
|
||||
@Inject BookmarkPicturesDao bookmarkDao;
|
||||
|
||||
@Inject
|
||||
protected OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
|
||||
@Inject
|
||||
protected SessionManager sessionManager;
|
||||
|
||||
private static CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
private FragmentMediaDetailPagerBinding binding;
|
||||
|
||||
private boolean editable;
|
||||
private boolean isFeaturedImage;
|
||||
private boolean isWikipediaButtonDisplayed;
|
||||
MediaDetailAdapter adapter;
|
||||
private Bookmark bookmark;
|
||||
private MediaDetailProvider provider;
|
||||
private boolean isFromFeaturedRootFragment;
|
||||
private int position;
|
||||
|
||||
/**
|
||||
* ProgressBar used to indicate the loading status of media items.
|
||||
*/
|
||||
private ProgressBar imageProgressBar;
|
||||
|
||||
private ArrayList<Integer> removedItems=new ArrayList<Integer>();
|
||||
|
||||
public void clearRemoved(){
|
||||
removedItems.clear();
|
||||
}
|
||||
public ArrayList<Integer> getRemovedItems() {
|
||||
return removedItems;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of this fragment using the provided
|
||||
* parameters.
|
||||
*
|
||||
* This method will create a new instance of MediaDetailPagerFragment and the arguments will be
|
||||
* saved to a bundle which will be later available in the {@link #onCreate(Bundle)}
|
||||
* @param editable
|
||||
* @param isFeaturedImage
|
||||
* @return
|
||||
*/
|
||||
public static MediaDetailPagerFragment newInstance(boolean editable, boolean isFeaturedImage) {
|
||||
MediaDetailPagerFragment mediaDetailPagerFragment = new MediaDetailPagerFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putBoolean("is_editable", editable);
|
||||
args.putBoolean("is_featured_image", isFeaturedImage);
|
||||
mediaDetailPagerFragment.setArguments(args);
|
||||
return mediaDetailPagerFragment;
|
||||
}
|
||||
|
||||
public MediaDetailPagerFragment() {
|
||||
// Required empty public constructor
|
||||
};
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater,
|
||||
ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false);
|
||||
binding.mediaDetailsPager.addOnPageChangeListener(this);
|
||||
// Initialize the ProgressBar by finding it in the layout
|
||||
imageProgressBar = binding.getRoot().findViewById(R.id.itemProgressBar);
|
||||
adapter = new MediaDetailAdapter(getChildFragmentManager());
|
||||
|
||||
// ActionBar is now supported in both activities - if this crashes something is quite wrong
|
||||
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
else {
|
||||
throw new AssertionError("Action bar should not be null!");
|
||||
}
|
||||
|
||||
// If fragment is associated with ProfileActivity, then hide the tabLayout
|
||||
if (getActivity() instanceof ProfileActivity) {
|
||||
((ProfileActivity)getActivity()).setTabLayoutVisibility(false);
|
||||
}
|
||||
|
||||
// Else if fragment is associated with MainActivity then hide that tab layout
|
||||
else if (getActivity() instanceof MainActivity) {
|
||||
((MainActivity)getActivity()).hideTabs();
|
||||
}
|
||||
|
||||
binding.mediaDetailsPager.setAdapter(adapter);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
final int pageNumber = savedInstanceState.getInt("current-page");
|
||||
binding.mediaDetailsPager.setCurrentItem(pageNumber, false);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt("current-page", binding.mediaDetailsPager.getCurrentItem());
|
||||
outState.putBoolean("editable", editable);
|
||||
outState.putBoolean("isFeaturedImage", isFeaturedImage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable", false);
|
||||
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false);
|
||||
|
||||
}
|
||||
setHasOptionsMenu(true);
|
||||
initProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* initialise the provider, based on from where the fragment was started, as in from an activity
|
||||
* or a fragment
|
||||
*/
|
||||
private void initProvider() {
|
||||
if (getParentFragment() instanceof MediaDetailProvider) {
|
||||
provider = (MediaDetailProvider) getParentFragment();
|
||||
} else if (getActivity() instanceof MediaDetailProvider) {
|
||||
provider = (MediaDetailProvider) getActivity();
|
||||
} else {
|
||||
throw new ClassCastException("Parent must implement MediaDetailProvider");
|
||||
}
|
||||
}
|
||||
|
||||
public MediaDetailProvider getMediaDetailProvider() {
|
||||
return provider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (getActivity() == null) {
|
||||
Timber.d("Returning as activity is destroyed!");
|
||||
return true;
|
||||
}
|
||||
|
||||
Media m = provider.getMediaAtPosition(binding.mediaDetailsPager.getCurrentItem());
|
||||
MediaDetailFragment mediaDetailFragment = this.adapter.getCurrentMediaDetailFragment();
|
||||
switch (item.getItemId()) {
|
||||
case R.id.menu_bookmark_current_image:
|
||||
boolean bookmarkExists = bookmarkDao.updateBookmark(bookmark);
|
||||
Snackbar snackbar = bookmarkExists ? Snackbar.make(getView(), R.string.add_bookmark, Snackbar.LENGTH_LONG) : Snackbar.make(getView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG);
|
||||
snackbar.show();
|
||||
updateBookmarkState(item);
|
||||
return true;
|
||||
case R.id.menu_copy_link:
|
||||
String uri = m.getPageTitle().getCanonicalUri();
|
||||
ClipboardUtils.copy("shareLink", uri, requireContext());
|
||||
Timber.d("Copied share link to clipboard: %s", uri);
|
||||
Toast.makeText(requireContext(), getString(R.string.menu_link_copied),
|
||||
Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
case R.id.menu_share_current_image:
|
||||
Intent shareIntent = new Intent(Intent.ACTION_SEND);
|
||||
shareIntent.setType("text/plain");
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getPageTitle().getCanonicalUri());
|
||||
startActivity(Intent.createChooser(shareIntent, "Share image via..."));
|
||||
|
||||
//Add media detail to backstack when the share button is clicked
|
||||
//So that when the share is cancelled or completed the media detail page is on top
|
||||
// of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
|
||||
FragmentManager supportFragmentManager = getActivity().getSupportFragmentManager();
|
||||
if (supportFragmentManager.getBackStackEntryCount() < 2) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.addToBackStack(MediaDetailPagerFragment.class.getName())
|
||||
.commit();
|
||||
supportFragmentManager.executePendingTransactions();
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_browser_current_image:
|
||||
// View in browser
|
||||
handleWebUrl(requireContext(), Uri.parse(m.getPageTitle().getMobileUri()));
|
||||
return true;
|
||||
case R.id.menu_download_current_image:
|
||||
// Download
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(getActivity())) {
|
||||
ViewUtil.showShortSnackbar(getView(), R.string.no_internet);
|
||||
return false;
|
||||
}
|
||||
DownloadUtils.downloadMedia(getActivity(), m);
|
||||
return true;
|
||||
case R.id.menu_set_as_wallpaper:
|
||||
// Set wallpaper
|
||||
setWallpaper(m);
|
||||
return true;
|
||||
case R.id.menu_set_as_avatar:
|
||||
// Set avatar
|
||||
setAvatar(m);
|
||||
return true;
|
||||
case R.id.menu_view_user_page:
|
||||
if (m != null && m.getUser() != null) {
|
||||
ProfileActivity.startYourself(getActivity(), m.getUser(),
|
||||
!Objects.equals(sessionManager.getUserName(), m.getUser()));
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_view_report:
|
||||
showReportDialog(m);
|
||||
case R.id.menu_view_set_white_background:
|
||||
if (mediaDetailFragment != null) {
|
||||
mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.white));
|
||||
}
|
||||
return true;
|
||||
case R.id.menu_view_set_black_background:
|
||||
if (mediaDetailFragment != null) {
|
||||
mediaDetailFragment.onImageBackgroundChanged(ContextCompat.getColor(getContext(), R.color.black));
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
|
||||
private void showReportDialog(final Media media) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
final AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity());
|
||||
final String[] values = requireContext().getResources()
|
||||
.getStringArray(R.array.report_violation_options);
|
||||
builder.setTitle(R.string.report_violation);
|
||||
builder.setItems(R.array.report_violation_options, (dialog, which) -> {
|
||||
sendReportEmail(media, values[which]);
|
||||
});
|
||||
builder.setNegativeButton(R.string.cancel, (dialog, which) -> {});
|
||||
builder.setCancelable(false);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void sendReportEmail(final Media media, final String type) {
|
||||
final String technicalInfo = getTechInfo(media, type);
|
||||
|
||||
final Intent feedbackIntent = new Intent(Intent.ACTION_SENDTO);
|
||||
feedbackIntent.setType("message/rfc822");
|
||||
feedbackIntent.setData(Uri.parse("mailto:"));
|
||||
feedbackIntent.putExtra(Intent.EXTRA_EMAIL,
|
||||
new String[]{CommonsApplication.REPORT_EMAIL});
|
||||
feedbackIntent.putExtra(Intent.EXTRA_SUBJECT,
|
||||
CommonsApplication.REPORT_EMAIL_SUBJECT);
|
||||
feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo);
|
||||
try {
|
||||
startActivity(feedbackIntent);
|
||||
} catch (final ActivityNotFoundException e) {
|
||||
Toast.makeText(getActivity(), R.string.no_email_client, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
private String getTechInfo(final Media media, final String type) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
|
||||
builder.append("Report type: ")
|
||||
.append(type)
|
||||
.append("\n\n");
|
||||
|
||||
builder.append("Image that you want to report: ")
|
||||
.append(media.getImageUrl())
|
||||
.append("\n\n");
|
||||
|
||||
builder.append("User that you want to report: ")
|
||||
.append(media.getUser())
|
||||
.append("\n\n");
|
||||
|
||||
if (sessionManager.getUserName() != null) {
|
||||
builder.append("Your username: ")
|
||||
.append(sessionManager.getUserName())
|
||||
.append("\n\n");
|
||||
}
|
||||
|
||||
builder.append("Violation reason: ")
|
||||
.append("\n");
|
||||
|
||||
builder.append("----------------------------------------------")
|
||||
.append("\n")
|
||||
.append("(please write reason here)")
|
||||
.append("\n")
|
||||
.append("----------------------------------------------")
|
||||
.append("\n\n")
|
||||
.append("Thank you for your report! Our team will investigate as soon as possible.")
|
||||
.append("\n")
|
||||
.append("Please note that images also have a `Nominate for deletion` button.");
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as the device's wallpaper if the imageUrl is not null
|
||||
* Fails silently if setting the wallpaper fails
|
||||
* @param media
|
||||
*/
|
||||
private void setWallpaper(Media media) {
|
||||
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
|
||||
Timber.d("Media URL not present");
|
||||
return;
|
||||
}
|
||||
ImageUtils.setWallpaperFromImageUrl(getActivity(), Uri.parse(media.getImageUrl()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as user's leaderboard avatar
|
||||
* @param media
|
||||
*/
|
||||
private void setAvatar(Media media) {
|
||||
if (media.getImageUrl() == null || media.getImageUrl().isEmpty()) {
|
||||
Timber.d("Media URL not present");
|
||||
return;
|
||||
}
|
||||
ImageUtils.setAvatarFromImageUrl(getActivity(), media.getImageUrl(),
|
||||
Objects.requireNonNull(sessionManager.getCurrentAccount()).name,
|
||||
okHttpJsonApiClient, compositeDisposable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
if (!editable) { // Disable menu options for editable views
|
||||
menu.clear(); // see http://stackoverflow.com/a/8495697/17865
|
||||
inflater.inflate(R.menu.fragment_image_detail, menu);
|
||||
if (binding.mediaDetailsPager != null) {
|
||||
MediaDetailProvider provider = getMediaDetailProvider();
|
||||
if(provider == null) {
|
||||
return;
|
||||
}
|
||||
final int position;
|
||||
if (isFromFeaturedRootFragment) {
|
||||
position = this.position;
|
||||
} else {
|
||||
position = binding.mediaDetailsPager.getCurrentItem();
|
||||
}
|
||||
|
||||
Media m = provider.getMediaAtPosition(position);
|
||||
if (m != null) {
|
||||
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true).setVisible(true);
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true);
|
||||
if (m.getUser() != null) {
|
||||
menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true);
|
||||
}
|
||||
|
||||
try {
|
||||
URL mediaUrl = new URL(m.getImageUrl());
|
||||
this.handleBackgroundColorMenuItems(
|
||||
() -> BitmapFactory.decodeStream(mediaUrl.openConnection().getInputStream()),
|
||||
menu
|
||||
);
|
||||
} catch (Exception e) {
|
||||
Timber.e("Cant detect media transparency");
|
||||
}
|
||||
|
||||
// Initialize bookmark object
|
||||
bookmark = new Bookmark(
|
||||
m.getFilename(),
|
||||
m.getAuthorOrUser(),
|
||||
BookmarkPicturesContentProvider.uriForName(m.getFilename())
|
||||
);
|
||||
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image));
|
||||
final Integer contributionState = provider.getContributionStateAt(position);
|
||||
if (contributionState != null) {
|
||||
switch (contributionState) {
|
||||
case Contribution.STATE_FAILED:
|
||||
case Contribution.STATE_IN_PROGRESS:
|
||||
case Contribution.STATE_QUEUED:
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false);
|
||||
break;
|
||||
case Contribution.STATE_COMPLETED:
|
||||
// Default set of menu items works fine. Treat same as regular media object
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false);
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false);
|
||||
}
|
||||
|
||||
if (!sessionManager.isUserLoggedIn()) {
|
||||
menu.findItem(R.id.menu_set_as_avatar).setVisible(false);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide wether or not we should display the background color menu items
|
||||
* We display them if the image is transparent
|
||||
* @param getBitmap
|
||||
* @param menu
|
||||
*/
|
||||
private void handleBackgroundColorMenuItems(Callable<Bitmap> getBitmap, Menu menu) {
|
||||
Observable.fromCallable(
|
||||
getBitmap
|
||||
).subscribeOn(Schedulers.newThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(image -> {
|
||||
if (image.hasAlpha()) {
|
||||
menu.findItem(R.id.menu_view_set_white_background).setVisible(true).setEnabled(true);
|
||||
menu.findItem(R.id.menu_view_set_black_background).setVisible(true).setEnabled(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateBookmarkState(MenuItem item) {
|
||||
boolean isBookmarked = bookmarkDao.findBookmark(bookmark);
|
||||
if(isBookmarked) {
|
||||
if(removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
|
||||
removedItems.remove(new Integer(binding.mediaDetailsPager.getCurrentItem()));
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(!removedItems.contains(binding.mediaDetailsPager.getCurrentItem())) {
|
||||
removedItems.add(binding.mediaDetailsPager.getCurrentItem());
|
||||
}
|
||||
}
|
||||
int icon = isBookmarked ? R.drawable.menu_ic_round_star_filled_24px : R.drawable.menu_ic_round_star_border_24px;
|
||||
item.setIcon(icon);
|
||||
}
|
||||
|
||||
public void showImage(int i, boolean isWikipediaButtonDisplayed) {
|
||||
this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed;
|
||||
setViewPagerCurrentItem(i);
|
||||
}
|
||||
|
||||
public void showImage(int i) {
|
||||
setViewPagerCurrentItem(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function waits for the item to load then sets the item to current item
|
||||
* @param position current item that to be shown
|
||||
*/
|
||||
private void setViewPagerCurrentItem(int position) {
|
||||
|
||||
final Handler handler = new Handler(Looper.getMainLooper());
|
||||
final Runnable runnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Show the ProgressBar while waiting for the item to load
|
||||
imageProgressBar.setVisibility(View.VISIBLE);
|
||||
// Check if the adapter has enough items loaded
|
||||
if(adapter.getCount() > position){
|
||||
// Set the current item in the ViewPager
|
||||
binding.mediaDetailsPager.setCurrentItem(position, false);
|
||||
// Hide the ProgressBar once the item is loaded
|
||||
imageProgressBar.setVisibility(View.GONE);
|
||||
} else {
|
||||
// If the item is not ready yet, post the Runnable again
|
||||
handler.post(this);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Start the Runnable
|
||||
handler.post(runnable);
|
||||
}
|
||||
|
||||
/**
|
||||
* The method notify the viewpager that number of items have changed.
|
||||
*/
|
||||
public void notifyDataSetChanged(){
|
||||
if (null != adapter) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrolled(int i, float v, int i2) {
|
||||
if(getActivity() == null) {
|
||||
Timber.d("Returning as activity is destroyed!");
|
||||
return;
|
||||
}
|
||||
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageSelected(int i) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageScrollStateChanged(int i) {
|
||||
}
|
||||
|
||||
public void onDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the media is nominated for deletion
|
||||
*
|
||||
* @param index item position that has been nominated
|
||||
*/
|
||||
@Override
|
||||
public void nominatingForDeletion(int index) {
|
||||
provider.refreshNominatedMedia(index);
|
||||
}
|
||||
|
||||
public interface MediaDetailProvider {
|
||||
Media getMediaAtPosition(int i);
|
||||
|
||||
int getTotalMediaCount();
|
||||
|
||||
Integer getContributionStateAt(int position);
|
||||
|
||||
// Reload media detail fragment once media is nominated
|
||||
void refreshNominatedMedia(int index);
|
||||
}
|
||||
|
||||
//FragmentStatePagerAdapter allows user to swipe across collection of images (no. of images undetermined)
|
||||
private class MediaDetailAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
/**
|
||||
* Keeps track of the current displayed fragment.
|
||||
*/
|
||||
private Fragment mCurrentFragment;
|
||||
|
||||
public MediaDetailAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
if (i == 0) {
|
||||
// See bug https://code.google.com/p/android/issues/detail?id=27526
|
||||
if(getActivity() == null) {
|
||||
Timber.d("Skipping getItem. Returning as activity is destroyed!");
|
||||
return null;
|
||||
}
|
||||
binding.mediaDetailsPager.postDelayed(() -> getActivity().invalidateOptionsMenu(), 5);
|
||||
}
|
||||
if (isFromFeaturedRootFragment) {
|
||||
return MediaDetailFragment.forMedia(position+i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
|
||||
} else {
|
||||
return MediaDetailFragment.forMedia(i, editable, isFeaturedImage, isWikipediaButtonDisplayed);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
if (getActivity() == null) {
|
||||
Timber.d("Skipping getCount. Returning as activity is destroyed!");
|
||||
return 0;
|
||||
}
|
||||
return provider.getTotalMediaCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently displayed fragment.
|
||||
* @return
|
||||
*/
|
||||
public Fragment getCurrentFragment() {
|
||||
return mCurrentFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* If current fragment is of type MediaDetailFragment, return it, otherwise return null.
|
||||
* @return MediaDetailFragment
|
||||
*/
|
||||
public MediaDetailFragment getCurrentMediaDetailFragment() {
|
||||
if (mCurrentFragment instanceof MediaDetailFragment) {
|
||||
return (MediaDetailFragment) mCurrentFragment;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to inform the adapter of which item is currently considered to be the "primary",
|
||||
* that is the one show to the user as the current page.
|
||||
* @param container
|
||||
* @param position
|
||||
* @param object
|
||||
*/
|
||||
@Override
|
||||
public void setPrimaryItem(@NonNull final ViewGroup container, final int position,
|
||||
@NonNull final Object object) {
|
||||
// Update the current fragment if changed
|
||||
if(getCurrentFragment() != object) {
|
||||
mCurrentFragment = ((Fragment)object);
|
||||
}
|
||||
super.setPrimaryItem(container, position, object);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,622 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.viewpager.widget.ViewPager.OnPageChangeListener
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import fr.free.nrw.commons.CommonsApplication
|
||||
import fr.free.nrw.commons.Media
|
||||
import fr.free.nrw.commons.R
|
||||
import fr.free.nrw.commons.auth.SessionManager
|
||||
import fr.free.nrw.commons.bookmarks.models.Bookmark
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao
|
||||
import fr.free.nrw.commons.contributions.Contribution
|
||||
import fr.free.nrw.commons.contributions.MainActivity
|
||||
import fr.free.nrw.commons.databinding.FragmentMediaDetailPagerBinding
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||
import fr.free.nrw.commons.profile.ProfileActivity
|
||||
import fr.free.nrw.commons.profile.ProfileActivity.Companion.startYourself
|
||||
import fr.free.nrw.commons.utils.ClipboardUtils.copy
|
||||
import fr.free.nrw.commons.utils.DownloadUtils.downloadMedia
|
||||
import fr.free.nrw.commons.utils.ImageUtils.setAvatarFromImageUrl
|
||||
import fr.free.nrw.commons.utils.ImageUtils.setWallpaperFromImageUrl
|
||||
import fr.free.nrw.commons.utils.NetworkUtils.isInternetConnectionEstablished
|
||||
import fr.free.nrw.commons.utils.ViewUtil.showShortSnackbar
|
||||
import fr.free.nrw.commons.utils.handleWebUrl
|
||||
import io.reactivex.Observable
|
||||
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 java.net.URL
|
||||
import java.util.concurrent.Callable
|
||||
import javax.inject.Inject
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class MediaDetailPagerFragment : CommonsDaggerSupportFragment(), OnPageChangeListener,
|
||||
MediaDetailFragment.Callback {
|
||||
@JvmField
|
||||
@Inject
|
||||
var bookmarkDao: BookmarkPicturesDao? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var okHttpJsonApiClient: OkHttpJsonApiClient? = null
|
||||
|
||||
@JvmField
|
||||
@Inject
|
||||
var sessionManager: SessionManager? = null
|
||||
|
||||
var binding: FragmentMediaDetailPagerBinding? = null
|
||||
var editable: Boolean = false
|
||||
var isFeaturedImage: Boolean = false
|
||||
var isWikipediaButtonDisplayed: Boolean = false
|
||||
var adapter: MediaDetailAdapter? = null
|
||||
var bookmark: Bookmark? = null
|
||||
var mediaDetailProvider: MediaDetailProvider? = null
|
||||
var isFromFeaturedRootFragment: Boolean = false
|
||||
var position: Int = 0
|
||||
|
||||
/**
|
||||
* ProgressBar used to indicate the loading status of media items.
|
||||
*/
|
||||
var imageProgressBar: ProgressBar? = null
|
||||
|
||||
var removedItems: ArrayList<Int> = ArrayList()
|
||||
|
||||
fun clearRemoved() = removedItems.clear()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
binding = FragmentMediaDetailPagerBinding.inflate(inflater, container, false)
|
||||
binding!!.mediaDetailsPager.addOnPageChangeListener(this)
|
||||
// Initialize the ProgressBar by finding it in the layout
|
||||
imageProgressBar = binding!!.root.findViewById(R.id.itemProgressBar)
|
||||
adapter = MediaDetailAdapter(this, childFragmentManager)
|
||||
|
||||
// ActionBar is now supported in both activities - if this crashes something is quite wrong
|
||||
val actionBar = (activity as AppCompatActivity).supportActionBar
|
||||
if (actionBar != null) {
|
||||
actionBar.setDisplayHomeAsUpEnabled(true)
|
||||
} else {
|
||||
throw AssertionError("Action bar should not be null!")
|
||||
}
|
||||
|
||||
// If fragment is associated with ProfileActivity, then hide the tabLayout
|
||||
if (activity is ProfileActivity) {
|
||||
(activity as ProfileActivity).setTabLayoutVisibility(false)
|
||||
} else if (activity is MainActivity) {
|
||||
(activity as MainActivity).hideTabs()
|
||||
}
|
||||
|
||||
binding!!.mediaDetailsPager.adapter = adapter
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
val pageNumber = savedInstanceState.getInt("current-page")
|
||||
binding!!.mediaDetailsPager.setCurrentItem(pageNumber, false)
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
}
|
||||
adapter!!.notifyDataSetChanged()
|
||||
|
||||
return binding!!.root
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt("current-page", binding!!.mediaDetailsPager.currentItem)
|
||||
outState.putBoolean("editable", editable)
|
||||
outState.putBoolean("isFeaturedImage", isFeaturedImage)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState != null) {
|
||||
editable = savedInstanceState.getBoolean("editable", false)
|
||||
isFeaturedImage = savedInstanceState.getBoolean("isFeaturedImage", false)
|
||||
}
|
||||
setHasOptionsMenu(true)
|
||||
initProvider()
|
||||
}
|
||||
|
||||
/**
|
||||
* initialise the provider, based on from where the fragment was started, as in from an activity
|
||||
* or a fragment
|
||||
*/
|
||||
private fun initProvider() {
|
||||
if (parentFragment is MediaDetailProvider) {
|
||||
mediaDetailProvider = parentFragment as MediaDetailProvider
|
||||
} else if (activity is MediaDetailProvider) {
|
||||
mediaDetailProvider = activity as MediaDetailProvider?
|
||||
} else {
|
||||
throw ClassCastException("Parent must implement MediaDetailProvider")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (activity == null) {
|
||||
Timber.d("Returning as activity is destroyed!")
|
||||
return true
|
||||
}
|
||||
|
||||
val m = mediaDetailProvider!!.getMediaAtPosition(binding!!.mediaDetailsPager.currentItem)
|
||||
val mediaDetailFragment = adapter!!.currentMediaDetailFragment
|
||||
when (item.itemId) {
|
||||
R.id.menu_bookmark_current_image -> {
|
||||
val bookmarkExists = bookmarkDao!!.updateBookmark(bookmark)
|
||||
val snackbar = if (bookmarkExists) Snackbar.make(
|
||||
requireView(),
|
||||
R.string.add_bookmark,
|
||||
Snackbar.LENGTH_LONG
|
||||
) else Snackbar.make(
|
||||
requireView(), R.string.remove_bookmark, Snackbar.LENGTH_LONG
|
||||
)
|
||||
snackbar.show()
|
||||
updateBookmarkState(item)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_copy_link -> {
|
||||
val uri = m!!.pageTitle.canonicalUri
|
||||
copy("shareLink", uri, requireContext())
|
||||
Timber.d("Copied share link to clipboard: %s", uri)
|
||||
Toast.makeText(
|
||||
requireContext(), getString(R.string.menu_link_copied),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_share_current_image -> {
|
||||
val shareIntent = Intent(Intent.ACTION_SEND)
|
||||
shareIntent.setType("text/plain")
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_TEXT, """${m!!.displayTitle}
|
||||
${m.pageTitle.canonicalUri}"""
|
||||
)
|
||||
startActivity(Intent.createChooser(shareIntent, "Share image via..."))
|
||||
|
||||
//Add media detail to backstack when the share button is clicked
|
||||
//So that when the share is cancelled or completed the media detail page is on top
|
||||
// of back stack fixing:https://github.com/commons-app/apps-android-commons/issues/2296
|
||||
val supportFragmentManager = requireActivity().supportFragmentManager
|
||||
if (supportFragmentManager.backStackEntryCount < 2) {
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.addToBackStack(MediaDetailPagerFragment::class.java.name)
|
||||
.commit()
|
||||
supportFragmentManager.executePendingTransactions()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_browser_current_image -> {
|
||||
// View in browser
|
||||
handleWebUrl(requireContext(), m!!.pageTitle.mobileUri.toUri())
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_download_current_image -> {
|
||||
// Download
|
||||
if (!isInternetConnectionEstablished(activity)) {
|
||||
showShortSnackbar(requireView(), R.string.no_internet)
|
||||
return false
|
||||
}
|
||||
downloadMedia(activity, m!!)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_set_as_wallpaper -> {
|
||||
// Set wallpaper
|
||||
setWallpaper(m!!)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_set_as_avatar -> {
|
||||
// Set avatar
|
||||
setAvatar(m!!)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_user_page -> {
|
||||
if (m?.user != null) {
|
||||
startYourself(
|
||||
requireActivity(), m.user!!,
|
||||
sessionManager!!.userName != m.user
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_report -> {
|
||||
showReportDialog(m)
|
||||
mediaDetailFragment?.onImageBackgroundChanged(
|
||||
ContextCompat.getColor(
|
||||
requireContext(),
|
||||
R.color.white
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_set_white_background -> {
|
||||
mediaDetailFragment?.onImageBackgroundChanged(
|
||||
ContextCompat.getColor(
|
||||
requireContext(),
|
||||
R.color.white
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
R.id.menu_view_set_black_background -> {
|
||||
mediaDetailFragment?.onImageBackgroundChanged(
|
||||
ContextCompat.getColor(
|
||||
requireContext(),
|
||||
R.color.black
|
||||
)
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showReportDialog(media: Media?) {
|
||||
if (media == null) {
|
||||
return
|
||||
}
|
||||
val builder = AlertDialog.Builder(requireActivity())
|
||||
val values = requireContext().resources
|
||||
.getStringArray(R.array.report_violation_options)
|
||||
builder.setTitle(R.string.report_violation)
|
||||
builder.setItems(
|
||||
R.array.report_violation_options
|
||||
) { dialog: DialogInterface?, which: Int ->
|
||||
sendReportEmail(media, values[which])
|
||||
}
|
||||
builder.setNegativeButton(
|
||||
R.string.cancel
|
||||
) { dialog: DialogInterface?, which: Int -> }
|
||||
builder.setCancelable(false)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun sendReportEmail(media: Media, type: String) {
|
||||
val technicalInfo = getTechInfo(media, type)
|
||||
|
||||
val feedbackIntent = Intent(Intent.ACTION_SENDTO)
|
||||
feedbackIntent.setType("message/rfc822")
|
||||
feedbackIntent.setData(Uri.parse("mailto:"))
|
||||
feedbackIntent.putExtra(
|
||||
Intent.EXTRA_EMAIL,
|
||||
arrayOf(CommonsApplication.REPORT_EMAIL)
|
||||
)
|
||||
feedbackIntent.putExtra(
|
||||
Intent.EXTRA_SUBJECT,
|
||||
CommonsApplication.REPORT_EMAIL_SUBJECT
|
||||
)
|
||||
feedbackIntent.putExtra(Intent.EXTRA_TEXT, technicalInfo)
|
||||
try {
|
||||
startActivity(feedbackIntent)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Toast.makeText(activity, R.string.no_email_client, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getTechInfo(media: Media, type: String): String {
|
||||
val builder = StringBuilder()
|
||||
|
||||
builder.append("Report type: ")
|
||||
.append(type)
|
||||
.append("\n\n")
|
||||
|
||||
builder.append("Image that you want to report: ")
|
||||
.append(media.imageUrl)
|
||||
.append("\n\n")
|
||||
|
||||
builder.append("User that you want to report: ")
|
||||
.append(media.user)
|
||||
.append("\n\n")
|
||||
|
||||
if (sessionManager!!.userName != null) {
|
||||
builder.append("Your username: ")
|
||||
.append(sessionManager!!.userName)
|
||||
.append("\n\n")
|
||||
}
|
||||
|
||||
builder.append("Violation reason: ")
|
||||
.append("\n")
|
||||
|
||||
builder.append("----------------------------------------------")
|
||||
.append("\n")
|
||||
.append("(please write reason here)")
|
||||
.append("\n")
|
||||
.append("----------------------------------------------")
|
||||
.append("\n\n")
|
||||
.append("Thank you for your report! Our team will investigate as soon as possible.")
|
||||
.append("\n")
|
||||
.append("Please note that images also have a `Nominate for deletion` button.")
|
||||
|
||||
return builder.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as the device's wallpaper if the imageUrl is not null
|
||||
* Fails silently if setting the wallpaper fails
|
||||
* @param media
|
||||
*/
|
||||
private fun setWallpaper(media: Media) {
|
||||
if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
|
||||
Timber.d("Media URL not present")
|
||||
return
|
||||
}
|
||||
setWallpaperFromImageUrl(requireActivity(), media.imageUrl!!.toUri())
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the media as user's leaderboard avatar
|
||||
* @param media
|
||||
*/
|
||||
private fun setAvatar(media: Media) {
|
||||
if (media.imageUrl == null || media.imageUrl!!.isEmpty()) {
|
||||
Timber.d("Media URL not present")
|
||||
return
|
||||
}
|
||||
setAvatarFromImageUrl(
|
||||
requireActivity(), media.imageUrl!!,
|
||||
sessionManager!!.currentAccount!!.name,
|
||||
okHttpJsonApiClient!!, Companion.compositeDisposable
|
||||
)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
if (!editable) { // Disable menu options for editable views
|
||||
menu.clear() // see http://stackoverflow.com/a/8495697/17865
|
||||
inflater.inflate(R.menu.fragment_image_detail, menu)
|
||||
if (binding!!.mediaDetailsPager != null) {
|
||||
val provider = mediaDetailProvider ?: return
|
||||
val position = if (isFromFeaturedRootFragment) {
|
||||
position
|
||||
} else {
|
||||
binding!!.mediaDetailsPager.currentItem
|
||||
}
|
||||
|
||||
val m = provider.getMediaAtPosition(position)
|
||||
if (m != null) {
|
||||
// Enable default set of actions, then re-enable different set of actions only if it is a failed contrib
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(true).setVisible(true)
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(true).setVisible(true)
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true)
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(true)
|
||||
.setVisible(true)
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(true)
|
||||
.setVisible(true)
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(true).setVisible(true)
|
||||
if (m.user != null) {
|
||||
menu.findItem(R.id.menu_view_user_page).setEnabled(true).setVisible(true)
|
||||
}
|
||||
|
||||
try {
|
||||
val mediaUrl = URL(m.imageUrl)
|
||||
handleBackgroundColorMenuItems({
|
||||
BitmapFactory.decodeStream(
|
||||
mediaUrl.openConnection().getInputStream()
|
||||
)
|
||||
}, menu)
|
||||
} catch (e: Exception) {
|
||||
Timber.e("Cant detect media transparency")
|
||||
}
|
||||
|
||||
// Initialize bookmark object
|
||||
bookmark = Bookmark(
|
||||
m.filename,
|
||||
m.getAuthorOrUser(),
|
||||
BookmarkPicturesContentProvider.uriForName(m.filename)
|
||||
)
|
||||
updateBookmarkState(menu.findItem(R.id.menu_bookmark_current_image))
|
||||
val contributionState = provider.getContributionStateAt(position)
|
||||
if (contributionState != null) {
|
||||
when (contributionState) {
|
||||
Contribution.STATE_FAILED, Contribution.STATE_IN_PROGRESS, Contribution.STATE_QUEUED -> {
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false)
|
||||
}
|
||||
|
||||
Contribution.STATE_COMPLETED -> {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
menu.findItem(R.id.menu_browser_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_copy_link).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_share_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_download_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_bookmark_current_image).setEnabled(false)
|
||||
.setVisible(false)
|
||||
menu.findItem(R.id.menu_set_as_wallpaper).setEnabled(false)
|
||||
.setVisible(false)
|
||||
}
|
||||
|
||||
if (!sessionManager!!.isUserLoggedIn) {
|
||||
menu.findItem(R.id.menu_set_as_avatar).setVisible(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide wether or not we should display the background color menu items
|
||||
* We display them if the image is transparent
|
||||
* @param getBitmap
|
||||
* @param menu
|
||||
*/
|
||||
private fun handleBackgroundColorMenuItems(getBitmap: Callable<Bitmap>, menu: Menu) {
|
||||
Observable.fromCallable(
|
||||
getBitmap
|
||||
).subscribeOn(Schedulers.newThread())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(Consumer { image: Bitmap ->
|
||||
if (image.hasAlpha()) {
|
||||
menu.findItem(R.id.menu_view_set_white_background).setVisible(true)
|
||||
.setEnabled(true)
|
||||
menu.findItem(R.id.menu_view_set_black_background).setVisible(true)
|
||||
.setEnabled(true)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateBookmarkState(item: MenuItem) {
|
||||
val isBookmarked = bookmarkDao!!.findBookmark(bookmark)
|
||||
if (isBookmarked) {
|
||||
if (removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
|
||||
removedItems.remove(binding!!.mediaDetailsPager.currentItem)
|
||||
}
|
||||
} else {
|
||||
if (!removedItems.contains(binding!!.mediaDetailsPager.currentItem)) {
|
||||
removedItems.add(binding!!.mediaDetailsPager.currentItem)
|
||||
}
|
||||
}
|
||||
|
||||
item.setIcon(if (isBookmarked) {
|
||||
R.drawable.menu_ic_round_star_filled_24px
|
||||
} else {
|
||||
R.drawable.menu_ic_round_star_border_24px
|
||||
})
|
||||
}
|
||||
|
||||
fun showImage(i: Int, isWikipediaButtonDisplayed: Boolean) {
|
||||
this.isWikipediaButtonDisplayed = isWikipediaButtonDisplayed
|
||||
setViewPagerCurrentItem(i)
|
||||
}
|
||||
|
||||
fun showImage(i: Int) {
|
||||
setViewPagerCurrentItem(i)
|
||||
}
|
||||
|
||||
/**
|
||||
* This function waits for the item to load then sets the item to current item
|
||||
* @param position current item that to be shown
|
||||
*/
|
||||
private fun setViewPagerCurrentItem(position: Int) {
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
val runnable: Runnable = object : Runnable {
|
||||
override fun run() {
|
||||
// Show the ProgressBar while waiting for the item to load
|
||||
imageProgressBar!!.visibility = View.VISIBLE
|
||||
// Check if the adapter has enough items loaded
|
||||
if (adapter!!.count > position) {
|
||||
// Set the current item in the ViewPager
|
||||
binding!!.mediaDetailsPager.setCurrentItem(position, false)
|
||||
// Hide the ProgressBar once the item is loaded
|
||||
imageProgressBar!!.visibility = View.GONE
|
||||
} else {
|
||||
// If the item is not ready yet, post the Runnable again
|
||||
handler.post(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
// Start the Runnable
|
||||
handler.post(runnable)
|
||||
}
|
||||
|
||||
/**
|
||||
* The method notify the viewpager that number of items have changed.
|
||||
*/
|
||||
fun notifyDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPageScrolled(i: Int, v: Float, i2: Int) {
|
||||
if (activity == null) {
|
||||
Timber.d("Returning as activity is destroyed!")
|
||||
return
|
||||
}
|
||||
|
||||
requireActivity().invalidateOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onPageSelected(i: Int) {
|
||||
}
|
||||
|
||||
override fun onPageScrollStateChanged(i: Int) {
|
||||
}
|
||||
|
||||
fun onDataSetChanged() {
|
||||
if (null != adapter) {
|
||||
adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after the media is nominated for deletion
|
||||
*
|
||||
* @param index item position that has been nominated
|
||||
*/
|
||||
override fun nominatingForDeletion(index: Int) {
|
||||
mediaDetailProvider!!.refreshNominatedMedia(index)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val compositeDisposable = CompositeDisposable()
|
||||
|
||||
/**
|
||||
* Use this factory method to create a new instance of this fragment using the provided
|
||||
* parameters.
|
||||
*
|
||||
* This method will create a new instance of MediaDetailPagerFragment and the arguments will be
|
||||
* saved to a bundle which will be later available in the [.onCreate]
|
||||
* @param editable
|
||||
* @param isFeaturedImage
|
||||
* @return
|
||||
*/
|
||||
@JvmStatic
|
||||
fun newInstance(editable: Boolean, isFeaturedImage: Boolean): MediaDetailPagerFragment {
|
||||
val mediaDetailPagerFragment = MediaDetailPagerFragment()
|
||||
val args = Bundle()
|
||||
args.putBoolean("is_editable", editable)
|
||||
args.putBoolean("is_featured_image", isFeaturedImage)
|
||||
mediaDetailPagerFragment.arguments = args
|
||||
return mediaDetailPagerFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import fr.free.nrw.commons.Media
|
||||
|
||||
interface MediaDetailProvider {
|
||||
fun getMediaAtPosition(i: Int): Media?
|
||||
|
||||
fun getTotalMediaCount(): Int
|
||||
|
||||
fun getContributionStateAt(position: Int): Int?
|
||||
|
||||
// Reload media detail fragment once media is nominated
|
||||
fun refreshNominatedMedia(index: Int)
|
||||
}
|
||||
|
|
@ -1,25 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwResponse;
|
||||
|
||||
public class MwParseResponse extends MwResponse {
|
||||
@Nullable
|
||||
private MwParseResult parse;
|
||||
|
||||
@Nullable
|
||||
public MwParseResult parse() {
|
||||
return parse;
|
||||
}
|
||||
|
||||
public boolean success() {
|
||||
return parse != null;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
protected void setParse(@Nullable MwParseResult parse) {
|
||||
this.parse = parse;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import fr.free.nrw.commons.wikidata.mwapi.MwResponse
|
||||
|
||||
class MwParseResponse : MwResponse() {
|
||||
private var parse: MwParseResult? = null
|
||||
|
||||
fun parse(): MwParseResult? = parse
|
||||
|
||||
fun success(): Boolean = parse != null
|
||||
|
||||
@VisibleForTesting
|
||||
protected fun setParse(parse: MwParseResult?) {
|
||||
this.parse = parse
|
||||
}
|
||||
}
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
public class MwParseResult {
|
||||
@SuppressWarnings("unused") private int pageid;
|
||||
@SuppressWarnings("unused") private int index;
|
||||
private MwParseText text;
|
||||
|
||||
public String text() {
|
||||
return text.text;
|
||||
}
|
||||
|
||||
|
||||
public class MwParseText{
|
||||
@SerializedName("*") private String text;
|
||||
}
|
||||
}
|
||||
18
app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
Normal file
18
app/src/main/java/fr/free/nrw/commons/media/MwParseResult.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package fr.free.nrw.commons.media
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
class MwParseResult {
|
||||
private val pageid = 0
|
||||
private val index = 0
|
||||
private val text: MwParseText? = null
|
||||
|
||||
fun text(): String? {
|
||||
return text?.text
|
||||
}
|
||||
|
||||
inner class MwParseText {
|
||||
@SerializedName("*")
|
||||
internal val text: String? = null
|
||||
}
|
||||
}
|
||||
|
|
@ -74,6 +74,7 @@ import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType
|
|||
import fr.free.nrw.commons.location.LocationUpdateListener
|
||||
import fr.free.nrw.commons.media.MediaClient
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment
|
||||
import fr.free.nrw.commons.media.MediaDetailProvider
|
||||
import fr.free.nrw.commons.navtab.NavTab
|
||||
import fr.free.nrw.commons.nearby.BottomSheetAdapter
|
||||
import fr.free.nrw.commons.nearby.BottomSheetAdapter.ItemClickListener
|
||||
|
|
@ -150,7 +151,7 @@ class NearbyParentFragment : CommonsDaggerSupportFragment(),
|
|||
LocationUpdateListener,
|
||||
LocationPermissionCallback,
|
||||
ItemClickListener,
|
||||
MediaDetailPagerFragment.MediaDetailProvider {
|
||||
MediaDetailProvider {
|
||||
var binding: FragmentNearbyParentBinding? = null
|
||||
|
||||
val mapEventsOverlay: MapEventsOverlay = MapEventsOverlay(object : MapEventsReceiver {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ class NearbyResultItem(
|
|||
private val wikipediaArticle: ResultTuple?,
|
||||
private val commonsArticle: ResultTuple?,
|
||||
private val location: ResultTuple?,
|
||||
@field:SerializedName("itemLabel")
|
||||
private val label: ResultTuple?,
|
||||
@field:SerializedName("streetAddress") private val address: ResultTuple?,
|
||||
private val icon: ResultTuple?,
|
||||
|
|
@ -15,7 +16,7 @@ class NearbyResultItem(
|
|||
@field:SerializedName("commonsCategory") private val commonsCategory: ResultTuple?,
|
||||
@field:SerializedName("pic") private val pic: ResultTuple?,
|
||||
@field:SerializedName("destroyed") private val destroyed: ResultTuple?,
|
||||
@field:SerializedName("description") private val description: ResultTuple?,
|
||||
@field:SerializedName("itemDescription") private val description: ResultTuple?,
|
||||
@field:SerializedName("endTime") private val endTime: ResultTuple?,
|
||||
@field:SerializedName("monument") private val monument: ResultTuple?,
|
||||
@field:SerializedName("dateOfOfficialClosure") private val dateOfOfficialClosure: ResultTuple?,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
* Assorted-Interests
|
||||
* BaRaN6161 TURK
|
||||
* Bananax47
|
||||
* Billibilbi
|
||||
* BlueCamille
|
||||
* Cigaryno
|
||||
* Cyclicus
|
||||
|
|
@ -50,7 +51,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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -277,7 +278,7 @@
|
|||
<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>
|
||||
|
|
|
|||
|
|
@ -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="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">თქვენი გამოხმაურება გამოქვეყნდება შემდეგ ვიკი გვერდზე: <a href=\"https://commons.wikimedia.org/wiki/Commons:Mobile_app/Feedback\">Commons:მობილური აპლიკაცია/გამოხმაურება</a></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">გაქრობა <b>უკიდურესი საშუალებაა</b> და <b>მხოლოდ მაშინ უნდა გამოიყენოთ, როდესაც გსურთ რედაქტირების სამუდამოდ შეწყვეტა</b> და ასევე თქვენი წარსული ასოციაციების რაც შეიძლება მეტი დამალვა.<br/><br/> ვიკიმედიის საერთო სივრცეში ანგარიშის წაშლა ხდება თქვენი ანგარიშის სახელის შეცვლით, რათა სხვებმა ვერ ამოიცნონ თქვენი წვლილი ანგარიშის გაქრობის სახელით ცნობილი პროცესით. <b>გაქრობა არ იძლევა სრულ ანონიმურობას ან პროექტებში წვლილის წაშლას</b> .</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>
|
||||
|
|
|
|||
|
|
@ -451,7 +451,7 @@
|
|||
<string name="deletion_reason_bad_for_my_privacy">Zdałem sobie sprawę, że jest to szkodliwe dla mojej prywatności</string>
|
||||
<string name="deletion_reason_no_longer_want_public">Zmieniłem zdanie, nie chcę, aby było to już publicznie widoczne</string>
|
||||
<string name="deletion_reason_not_interesting">Przepraszamy, to zdjęcie nie jest interesujące dla encyklopedii</string>
|
||||
<string name="uploaded_by_myself">Przesłane przeze mnie na %1$s , wykorzystane w co najmniej %2$d artykułach.</string>
|
||||
<string name="uploaded_by_myself" fuzzy="true">Przesłane przeze mnie na %1$s, używane w artykułach %2$d.</string>
|
||||
<string name="no_uploads">Prześlij swoje pierwsze multimedia, dotykając przycisku Dodaj.</string>
|
||||
<string name="no_categories_selected">Nie wybrano kategorii</string>
|
||||
<string name="no_categories_selected_warning_desc">Obrazy bez kategorii rzadko nadają się do użycia. Czy na pewno chcesz kontynuować bez wybierania kategorii?</string>
|
||||
|
|
@ -620,8 +620,6 @@
|
|||
<string name="title_for_media">MEDIA</string>
|
||||
<string name="title_for_child_classes">KLASY POTOMNE</string>
|
||||
<string name="title_for_parent_classes">KLASY NADRZĘDNE</string>
|
||||
<string name="title_for_subcategories">KATEGORIE PODRZĘDNE</string>
|
||||
<string name="title_for_parent_categories">KATEGORIE NADRZĘDNE</string>
|
||||
<string name="upload_nearby_place_found_title">Znaleziono miejsce w pobliżu</string>
|
||||
<string name="upload_nearby_place_found_description_plural">Czy to są zdjęcia %1$s?</string>
|
||||
<string name="upload_nearby_place_found_description_singular">Czy to jest zdjęcie %1$s?</string>
|
||||
|
|
@ -856,7 +854,6 @@
|
|||
<string name="usages_on_commons_heading">Commons</string>
|
||||
<string name="usages_on_other_wikis_heading">Inne wiki</string>
|
||||
<string name="file_usages_container_heading">Wykorzystanie pliku</string>
|
||||
<string name="title_activity_single_web_view">SingleWebViewActivity</string>
|
||||
<string name="account">Konto</string>
|
||||
<string name="vanish_account">Wymaż konto</string>
|
||||
<string name="account_vanish_request_confirm_title">Ostrzeżenie o wymazaniu konta</string>
|
||||
|
|
|
|||
|
|
@ -143,7 +143,7 @@
|
|||
<string name="title_activity_category_details">وېشنيزه</string>
|
||||
<string name="title_activity_review">ملگرو بياکتنه</string>
|
||||
<string name="menu_about">په اړه</string>
|
||||
<string name="about_license">د ويکيرسنۍ خونديځ کاريال يو پرانيستې سرچينې کاريال دی چې د ويکيرسنۍ ټولنې د بسپنه ورکوونکو او خپلخوښو کارنانو له خوا جوړ شوی او ساتل کېږي. د ويکيرسنۍ بنسټ د دې کاريال په جوړولو، پراختيا او ساتنه کې ښکېل نه دی.</string>
|
||||
<string name="about_license">د ويکيرسنۍ خونديځ کاريال يو پرانېستې سرچينې کاريال دی چې د ويکيرسنۍ ټولنې د بسپنه ورکوونکو او خپلخوښو کارنانو له خوا جوړ شوی او ساتل کېږي. د ويکيرسنۍ بنسټ د دې کاريال په جوړولو، پراختيا او ساتنه کې ښکېل نه دی.</string>
|
||||
<string name="about_improve">د بگ راپور او وړانديزونو لپاره يوه <a href=\"%1$s\">گيټ هاب ستونزه</a> جوړه کړئ.</string>
|
||||
<string name="about_privacy_policy">د پټنتيا تگلار</string>
|
||||
<string name="title_activity_about">په اړه</string>
|
||||
|
|
@ -210,7 +210,7 @@
|
|||
<string name="become_a_tester_title">ازمېښتي ازمايښتگر شئ</string>
|
||||
<string name="welcome_image_welcome_wikipedia">ويکيپېډياښه راغلئ</string>
|
||||
<string name="cancel">ناگارل</string>
|
||||
<string name="navigation_drawer_open">پرانيستل</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>
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@
|
|||
<string name="media_detail_description">Description</string>
|
||||
<string name="media_detail_discussion">Discussion</string>
|
||||
<string name="media_detail_author">Author</string>
|
||||
<string name="media_detail_uploader">Uploader</string>
|
||||
<string name="media_detail_uploaded_date">Uploaded date</string>
|
||||
<string name="media_detail_license">License</string>
|
||||
<string name="media_detail_coordinates">Coordinates</string>
|
||||
|
|
|
|||
|
|
@ -1,50 +1,32 @@
|
|||
SELECT
|
||||
?item
|
||||
(SAMPLE(?label) AS ?label)
|
||||
(SAMPLE(?class) AS ?class)
|
||||
(SAMPLE(?description) AS ?description)
|
||||
(SAMPLE(?classLabel) AS ?classLabel)
|
||||
(SAMPLE(?pic) AS ?pic)
|
||||
(SAMPLE(?destroyed) AS ?destroyed)
|
||||
(SAMPLE(?endTime) AS ?endTime)
|
||||
(SAMPLE(?wikipediaArticle) AS ?wikipediaArticle)
|
||||
(SAMPLE(?commonsArticle) AS ?commonsArticle)
|
||||
(SAMPLE(?commonsCategory) AS ?commonsCategory)
|
||||
(SAMPLE(?dateOfOfficialClosure) AS ?dateOfOfficialClosure)
|
||||
(SAMPLE(?pointInTime) AS ?pointInTime)
|
||||
?itemLabel
|
||||
?itemDescription
|
||||
?class
|
||||
?classLabel
|
||||
?pic
|
||||
?destroyed
|
||||
?endTime
|
||||
?wikipediaArticle
|
||||
?commonsArticle
|
||||
?commonsCategory
|
||||
?dateOfOfficialClosure
|
||||
?pointInTime
|
||||
WHERE {
|
||||
SERVICE <https://query.wikidata.org/sparql> {
|
||||
values ?item {
|
||||
${ENTITY}
|
||||
}
|
||||
VALUES ?item {${ENTITY}}
|
||||
}
|
||||
|
||||
# Get the label in the preferred language of the user, or any other language if no label is available in that language.
|
||||
OPTIONAL {?item rdfs:label ?itemLabelPreferredLanguage. FILTER (lang(?itemLabelPreferredLanguage) = "${LANG}")}
|
||||
OPTIONAL {?item rdfs:label ?itemLabelAnyLanguage}
|
||||
BIND(COALESCE(?itemLabelPreferredLanguage, ?itemLabelAnyLanguage, "?") as ?label)
|
||||
# Get item label/class label/description in the preferred language of the user, or fallback.
|
||||
SERVICE wikibase:label { bd:serviceParam wikibase:language "${LANG},en,aa,ab,ae,af,ak,am,an,ar,as,av,ay,az,ba,be,bg,bh,bi,bm,bn,bo,br,bs,ca,ce,ch,co,cr,cs,cu,cv,cy,da,de,dv,dz,ee,el,eo,es,et,eu,fa,ff,fi,fj,fo,fr,fy,ga,gd,gl,gn,gu,gv,ha,he,hi,ho,hr,ht,hu,hy,hz,ia,id,ie,ig,ii,ik,io,is,it,iu,ja,jv,ka,kg,ki,kj,kk,kl,km,kn,ko,kr,ks,ku,kv,kw,ky,la,lb,lg,li,ln,lo,lt,lu,lv,mg,mh,mi,mk,ml,mn,mo,mr,ms,mt,my,na,nb,nd,ne,ng,nl,nn,no,ny,oc,oj,om,or,os,pa,pi,pl,ps,pt,qu,rm,rn,ro,ru,rw,sa,sc,sd,se,sg,sh,si,sk,sl,sm,sn,so,sq,sr,ss,st,su,sv,sw,ta,te,tg,th,ti,tk,tl,tn,to,tr,ts,tt,tw,ty,ug,uk,ur,uz,ve,vi,vo,wa,wo,xh,yi,yo,za,zh,zu". }
|
||||
|
||||
# Get the description in the preferred language of the user, or any other language if no description is available in that language.
|
||||
OPTIONAL {?item schema:description ?itemDescriptionPreferredLanguage. FILTER (lang(?itemDescriptionPreferredLanguage) = "${LANG}")}
|
||||
OPTIONAL {?item schema:description ?itemDescriptionAnyLanguage}
|
||||
BIND(COALESCE(?itemDescriptionPreferredLanguage, ?itemDescriptionAnyLanguage, "?") as ?description)
|
||||
# Get class (such as forest or bridge)
|
||||
OPTIONAL {?item p:P31/ps:P31 ?class}
|
||||
|
||||
# Get the class label in the preferred language of the user, or any other language if no label is available in that language.
|
||||
OPTIONAL {
|
||||
?item p:P31/ps:P31 ?class.
|
||||
OPTIONAL {?class rdfs:label ?classLabelPreferredLanguage. FILTER (lang(?classLabelPreferredLanguage) = "${LANG}")}
|
||||
OPTIONAL {?class rdfs:label ?classLabelAnyLanguage}
|
||||
BIND(COALESCE(?classLabelPreferredLanguage, ?classLabelAnyLanguage, "?") as ?classLabel)
|
||||
}
|
||||
|
||||
OPTIONAL {
|
||||
?item p:P31/ps:P31 ?class.
|
||||
}
|
||||
|
||||
# Get picture
|
||||
# Get picture (items without a picture will be shown in red on the Nearby map)
|
||||
OPTIONAL {?item wdt:P18 ?pic}
|
||||
|
||||
# Get existence
|
||||
# Get existence (whether an item still exists or not)
|
||||
OPTIONAL {?item wdt:P576 ?destroyed}
|
||||
OPTIONAL {?item wdt:P582 ?endTime}
|
||||
OPTIONAL {?item wdt:P3999 ?dateOfOfficialClosure}
|
||||
|
|
@ -65,4 +47,3 @@ WHERE {
|
|||
?commonsArticle schema:isPartOf <https://commons.wikimedia.org/>.
|
||||
}
|
||||
}
|
||||
GROUP BY ?item
|
||||
|
|
@ -235,7 +235,7 @@ class BookmarkListRootFragmentUnitTest {
|
|||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCountCaseNull() {
|
||||
whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(null)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 0)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -244,7 +244,7 @@ class BookmarkListRootFragmentUnitTest {
|
|||
val listAdapter = mock(ListAdapter::class.java)
|
||||
whenever(bookmarksPagerAdapter.mediaAdapter).thenReturn(listAdapter)
|
||||
whenever(listAdapter.count).thenReturn(1)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 1)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -76,7 +76,7 @@ class CategoryDetailsActivityUnitTests {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
activity.totalMediaCount
|
||||
activity.getTotalMediaCount()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -334,7 +334,7 @@ class ContributionsFragmentUnitTests {
|
|||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
Shadows.shadowOf(Looper.getMainLooper()).idle()
|
||||
fragment.totalMediaCount
|
||||
fragment.getTotalMediaCount()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -189,8 +189,8 @@ class ExploreListRootFragmentUnitTest {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
`when`(listFragment.totalMediaCount).thenReturn(1)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 1)
|
||||
`when`(listFragment.getTotalMediaCount()).thenReturn(1)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 1)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
@ -199,7 +199,7 @@ class ExploreListRootFragmentUnitTest {
|
|||
val field: Field = ExploreListRootFragment::class.java.getDeclaredField("listFragment")
|
||||
field.isAccessible = true
|
||||
field.set(fragment, null)
|
||||
Assert.assertEquals(fragment.totalMediaCount, 0)
|
||||
Assert.assertEquals(fragment.getTotalMediaCount(), 0)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -108,7 +108,7 @@ class WikidataItemDetailsActivityUnitTests {
|
|||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun testGetTotalMediaCount() {
|
||||
activity.totalMediaCount
|
||||
activity.getTotalMediaCount()
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -160,8 +160,8 @@ class SearchActivityUnitTests {
|
|||
fun testGetTotalMediaCount() {
|
||||
val num = 1
|
||||
Whitebox.setInternalState(activity, "searchMediaFragment", searchMediaFragment)
|
||||
`when`(searchMediaFragment.totalMediaCount).thenReturn(num)
|
||||
assertEquals(activity.totalMediaCount, num)
|
||||
`when`(searchMediaFragment.getTotalMediaCount()).thenReturn(num)
|
||||
assertEquals(activity.getTotalMediaCount(), num)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ import java.util.concurrent.Executor
|
|||
class CustomOkHttpNetworkFetcherUnitTest {
|
||||
private lateinit var fetcher: CustomOkHttpNetworkFetcher
|
||||
private lateinit var okHttpClient: OkHttpClient
|
||||
private lateinit var state: CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState
|
||||
private lateinit var state: OkHttpNetworkFetchState
|
||||
|
||||
@Mock
|
||||
private lateinit var callback: NetworkFetcher.Callback
|
||||
|
|
@ -162,7 +162,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
|
|||
val method: Method =
|
||||
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
|
||||
"onFetchResponse",
|
||||
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java,
|
||||
OkHttpNetworkFetchState::class.java,
|
||||
Call::class.java,
|
||||
Response::class.java,
|
||||
NetworkFetcher.Callback::class.java,
|
||||
|
|
@ -196,7 +196,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
|
|||
val method: Method =
|
||||
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
|
||||
"onFetchResponse",
|
||||
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java,
|
||||
OkHttpNetworkFetchState::class.java,
|
||||
Call::class.java,
|
||||
Response::class.java,
|
||||
NetworkFetcher.Callback::class.java,
|
||||
|
|
@ -230,7 +230,7 @@ class CustomOkHttpNetworkFetcherUnitTest {
|
|||
val method: Method =
|
||||
CustomOkHttpNetworkFetcher::class.java.getDeclaredMethod(
|
||||
"onFetchResponse",
|
||||
CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState::class.java,
|
||||
OkHttpNetworkFetchState::class.java,
|
||||
Call::class.java,
|
||||
Response::class.java,
|
||||
NetworkFetcher.Callback::class.java,
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ class MediaDetailFragmentUnitTests {
|
|||
private lateinit var button: Button
|
||||
|
||||
@Mock
|
||||
private lateinit var detailProvider: MediaDetailPagerFragment.MediaDetailProvider
|
||||
private lateinit var detailProvider: MediaDetailProvider
|
||||
|
||||
@Mock
|
||||
private lateinit var applicationKvStore: JsonKvStore
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue