Merge branch 'main' into added-button

This commit is contained in:
Nicolas Raoul 2025-07-16 17:22:26 +09:00 committed by GitHub
commit 2cfdac42f1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 1292 additions and 1269 deletions

View file

@ -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;

View file

@ -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

View file

@ -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

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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 :

View file

@ -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.

View file

@ -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;
}
}

View 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
}
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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"
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}

View file

@ -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);
}
}
}

View file

@ -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
}
}
}

View file

@ -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)
}

View file

@ -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;
}
}

View file

@ -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
}
}

View file

@ -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;
}
}

View 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
}
}

View file

@ -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 {

View file

@ -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?,

View file

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

View file

@ -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>

View file

@ -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>

View file

@ -232,8 +232,9 @@
<string name="navigation_item_about">Acerca de</string>
<string name="navigation_item_settings">Configuración</string>
<string name="navigation_item_feedback">Comentarios</string>
<string name="navigation_item_feedback_github">Comentarios a través de GitHub</string>
<string name="navigation_item_logout">Saír</string>
<string name="navigation_item_info">Titorial</string>
<string name="navigation_item_info">Guía</string>
<string name="navigation_item_notification">Notificacións</string>
<string name="navigation_item_review">Revisar</string>
<string name="no_description_found">non se atopou descrición</string>
@ -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>

View file

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

View file

@ -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>

View file

@ -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">د بگ راپور او وړانديزونو لپاره يوه &lt;a href=\"%1$s\"&gt;گيټ هاب ستونزه&lt;/a&gt; جوړه کړئ.</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>

View file

@ -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>

View file

@ -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

View file

@ -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

View file

@ -76,7 +76,7 @@ class CategoryDetailsActivityUnitTests {
@Test
@Throws(Exception::class)
fun testGetTotalMediaCount() {
activity.totalMediaCount
activity.getTotalMediaCount()
}
@Test

View file

@ -334,7 +334,7 @@ class ContributionsFragmentUnitTests {
@Throws(Exception::class)
fun testGetTotalMediaCount() {
Shadows.shadowOf(Looper.getMainLooper()).idle()
fragment.totalMediaCount
fragment.getTotalMediaCount()
}
@Test

View file

@ -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

View file

@ -108,7 +108,7 @@ class WikidataItemDetailsActivityUnitTests {
@Test
@Throws(Exception::class)
fun testGetTotalMediaCount() {
activity.totalMediaCount
activity.getTotalMediaCount()
}
@Test

View file

@ -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

View file

@ -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,

View file

@ -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