mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 04:13:53 +01:00
Merge branch 'master' into macgills/3847-convert-media-and-contribution
This commit is contained in:
commit
f6267577f4
33 changed files with 575 additions and 302 deletions
|
|
@ -76,7 +76,7 @@ dependencies {
|
|||
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
|
||||
|
||||
// Unit testing
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'junit:junit:4.13'
|
||||
testImplementation 'org.robolectric:robolectric:4.3'
|
||||
testImplementation 'androidx.test:core:1.2.0'
|
||||
testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1'
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.espresso.matcher.ViewMatchers.withText
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.utils.ConfigUtils
|
||||
import fr.free.nrw.commons.utils.ConfigUtils.getVersionNameWithSha
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
|
|
@ -36,7 +36,9 @@ class AboutActivityTest {
|
|||
@Test
|
||||
fun testBuildNumber() {
|
||||
Espresso.onView(ViewMatchers.withId(R.id.about_version))
|
||||
.check(ViewAssertions.matches(withText(ConfigUtils.getVersionNameWithSha(getApplicationContext()))))
|
||||
.check(ViewAssertions.matches(
|
||||
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha())
|
||||
))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import androidx.test.rule.ActivityTestRule
|
|||
import androidx.test.rule.GrantPermissionRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.auth.LoginActivity
|
||||
import fr.free.nrw.commons.upload.DescriptionsAdapter
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter
|
||||
import fr.free.nrw.commons.util.MyViewAction
|
||||
import fr.free.nrw.commons.utils.ConfigUtils
|
||||
import org.hamcrest.core.AllOf.allOf
|
||||
|
|
@ -78,7 +78,7 @@ class UploadTest {
|
|||
|
||||
@Test
|
||||
fun testUploadWithDescription() {
|
||||
if (!ConfigUtils.isBetaFlavour()) {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
throw Error("This test should only be run in Beta!")
|
||||
}
|
||||
|
||||
|
|
@ -96,7 +96,7 @@ class UploadTest {
|
|||
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
|
||||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
|
||||
|
|
@ -150,7 +150,7 @@ class UploadTest {
|
|||
|
||||
@Test
|
||||
fun testUploadWithoutDescription() {
|
||||
if (!ConfigUtils.isBetaFlavour()) {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
throw Error("This test should only be run in Beta!")
|
||||
}
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ class UploadTest {
|
|||
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
|
||||
dismissWarning("Yes")
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
|
|
@ -209,7 +209,7 @@ class UploadTest {
|
|||
|
||||
@Test
|
||||
fun testUploadWithMultilingualDescription() {
|
||||
if (!ConfigUtils.isBetaFlavour()) {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
throw Error("This test should only be run in Beta!")
|
||||
}
|
||||
|
||||
|
|
@ -227,12 +227,12 @@ class UploadTest {
|
|||
// Try to dismiss the error, if there is one (probably about duplicate files on Commons)
|
||||
dismissWarningDialog()
|
||||
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.et_title)))
|
||||
onView(allOf<View>(isDisplayed(), withId(R.id.tv_title)))
|
||||
.perform(replaceText(commonsFileName))
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(0,
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description")))
|
||||
|
||||
onView(withId(R.id.btn_add_description))
|
||||
|
|
@ -240,12 +240,12 @@ class UploadTest {
|
|||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
|
||||
MyViewAction.selectSpinnerItemInChildViewWithId(R.id.spinner_description_languages, 2)))
|
||||
|
||||
onView(withId(R.id.rv_descriptions)).perform(
|
||||
RecyclerViewActions
|
||||
.actionOnItemAtPosition<DescriptionsAdapter.ViewHolder>(1,
|
||||
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1,
|
||||
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description")))
|
||||
|
||||
onView(allOf(isDisplayed(), withId(R.id.btn_next)))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ class WelcomeActivityTest {
|
|||
|
||||
@Test
|
||||
fun ifBetaShowsSkipButton() {
|
||||
if (ConfigUtils.isBetaFlavour()) {
|
||||
if (ConfigUtils.isBetaFlavour) {
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.check(matches(isDisplayed()))
|
||||
}
|
||||
|
|
@ -31,7 +31,7 @@ class WelcomeActivityTest {
|
|||
|
||||
@Test
|
||||
fun ifProdHidesSkipButton() {
|
||||
if (!ConfigUtils.isBetaFlavour()) {
|
||||
if (!ConfigUtils.isBetaFlavour) {
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.check(matches(not(isDisplayed())))
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ class WelcomeActivityTest {
|
|||
|
||||
@Test
|
||||
fun testBetaSkipButton() {
|
||||
if (ConfigUtils.isBetaFlavour()) {
|
||||
if (ConfigUtils.isBetaFlavour) {
|
||||
onView(withId(R.id.finishTutorialButton))
|
||||
.perform(ViewActions.click())
|
||||
assert(activityRule.activity.isDestroyed)
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ import fr.free.nrw.commons.Utils;
|
|||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||
import java.util.Locale;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
|
@ -41,7 +42,8 @@ import org.wikipedia.dataclient.WikiSite;
|
|||
*/
|
||||
|
||||
public class ContributionsListFragment extends CommonsDaggerSupportFragment implements
|
||||
ContributionsListContract.View, ContributionsListAdapter.Callback, WikipediaInstructionsDialogFragment.Callback {
|
||||
ContributionsListContract.View, ContributionsListAdapter.Callback,
|
||||
WikipediaInstructionsDialogFragment.Callback {
|
||||
|
||||
private static final String RV_STATE = "rv_scroll_state";
|
||||
|
||||
|
|
@ -275,7 +277,6 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
}
|
||||
|
||||
|
||||
|
||||
public Media getMediaAtPosition(final int i) {
|
||||
return adapter.getContributionForPosition(i).getMedia();
|
||||
}
|
||||
|
|
@ -291,13 +292,14 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
|
|||
*/
|
||||
@Override
|
||||
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
|
||||
if(copyWikicode) {
|
||||
String wikicode = contribution.getMedia().getWikiCode();
|
||||
if (copyWikicode) {
|
||||
String wikicode = contribution.getWikiCode();
|
||||
Utils.copy("wikicode", wikicode, getContext());
|
||||
}
|
||||
|
||||
final String url = languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
|
||||
.getWikipediaPageTitle();
|
||||
final String url =
|
||||
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
|
||||
.getWikipediaPageTitle();
|
||||
Utils.handleWebUrl(getContext(), Uri.parse(url));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -127,7 +127,8 @@ public class MainActivity extends NavigationBaseActivity implements FragmentMana
|
|||
tabLayout.getTabAt(1).setCustomView(nearbyTabLinearLayout);
|
||||
|
||||
nearbyInfo.setOnClickListener(view ->
|
||||
new AlertDialog.Builder(MainActivity.this).setTitle(R.string.title_activity_nearby).setMessage(R.string.showcase_view_whole_nearby_activity)
|
||||
new AlertDialog.Builder(MainActivity.this).setTitle(R.string.title_activity_nearby)
|
||||
.setView(getLayoutInflater().inflate(R.layout.dialog_nearby, null))
|
||||
.setCancelable(true)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, id) -> dialog.cancel())
|
||||
.create()
|
||||
|
|
|
|||
|
|
@ -12,7 +12,12 @@ import java.io.IOException
|
|||
*
|
||||
* @author Ashish Kumar
|
||||
*/
|
||||
class CountingRequestBody(protected var delegate: RequestBody, protected var listener: Listener) : RequestBody() {
|
||||
class CountingRequestBody(
|
||||
protected var delegate: RequestBody,
|
||||
protected var listener: Listener,
|
||||
var offset: Long,
|
||||
var totalContentLength: Long
|
||||
) : RequestBody() {
|
||||
protected var countingSink: CountingSink? = null
|
||||
override fun contentType(): MediaType? {
|
||||
return delegate.contentType()
|
||||
|
|
@ -37,11 +42,12 @@ class CountingRequestBody(protected var delegate: RequestBody, protected var lis
|
|||
|
||||
protected inner class CountingSink(delegate: Sink?) : ForwardingSink(delegate!!) {
|
||||
private var bytesWritten: Long = 0
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun write(source: Buffer, byteCount: Long) {
|
||||
super.write(source, byteCount)
|
||||
bytesWritten += byteCount
|
||||
listener.onRequestProgress(bytesWritten, contentLength())
|
||||
listener.onRequestProgress(offset + bytesWritten, totalContentLength)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,33 +1,84 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.content.Context;
|
||||
import io.reactivex.Observable;
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
import timber.log.Timber;
|
||||
|
||||
@Singleton
|
||||
public class FileUtilsWrapper {
|
||||
|
||||
@Inject
|
||||
public FileUtilsWrapper() {
|
||||
@Inject
|
||||
public FileUtilsWrapper() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public String getFileExt(String fileName) {
|
||||
return FileUtils.getFileExt(fileName);
|
||||
}
|
||||
public String getFileExt(String fileName) {
|
||||
return FileUtils.getFileExt(fileName);
|
||||
}
|
||||
|
||||
public String getSHA1(InputStream is) {
|
||||
return FileUtils.getSHA1(is);
|
||||
}
|
||||
public String getSHA1(InputStream is) {
|
||||
return FileUtils.getSHA1(is);
|
||||
}
|
||||
|
||||
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
|
||||
return FileUtils.getFileInputStream(filePath);
|
||||
}
|
||||
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
|
||||
return FileUtils.getFileInputStream(filePath);
|
||||
}
|
||||
|
||||
public String getGeolocationOfFile(String filePath) {
|
||||
return FileUtils.getGeolocationOfFile(filePath);
|
||||
public String getGeolocationOfFile(String filePath) {
|
||||
return FileUtils.getGeolocationOfFile(filePath);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Takes a file as input and returns an Observable of files with the specified chunk size
|
||||
*/
|
||||
public Observable<File> getFileChunks(Context context, File file, final int chunkSize)
|
||||
throws IOException {
|
||||
final byte[] buffer = new byte[chunkSize];
|
||||
|
||||
//try-with-resources to ensure closing stream
|
||||
try (final FileInputStream fis = new FileInputStream(file);
|
||||
final BufferedInputStream bis = new BufferedInputStream(fis)) {
|
||||
final List<File> buffers = new ArrayList<>();
|
||||
int size;
|
||||
while ((size = bis.read(buffer)) > 0) {
|
||||
buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
|
||||
getFileExt(file.getName())));
|
||||
}
|
||||
return Observable.fromIterable(buffers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temp file containing the passed byte data.
|
||||
*/
|
||||
private File writeToFile(Context context, final byte[] data, final String fileName,
|
||||
String fileExtension)
|
||||
throws IOException {
|
||||
final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir());
|
||||
try {
|
||||
if (!file.exists()) {
|
||||
file.createNewFile();
|
||||
}
|
||||
final FileOutputStream fos = new FileOutputStream(file);
|
||||
fos.write(data);
|
||||
fos.close();
|
||||
} catch (final Exception throwable) {
|
||||
Timber.e(throwable, "Failed to create file");
|
||||
}
|
||||
return file;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,15 @@ import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
|
|||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.Nullable;
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.upload.UploadService.NotificationUpdateProgressListener;
|
||||
import io.reactivex.Observable;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
|
@ -16,59 +20,112 @@ import okhttp3.MediaType;
|
|||
import okhttp3.MultipartBody;
|
||||
import okhttp3.RequestBody;
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import timber.log.Timber;
|
||||
|
||||
@Singleton
|
||||
public class UploadClient {
|
||||
|
||||
private final UploadInterface uploadInterface;
|
||||
private final CsrfTokenClient csrfTokenClient;
|
||||
private final PageContentsCreator pageContentsCreator;
|
||||
private final int CHUNK_SIZE = 256 * 1024; // 256 KB
|
||||
|
||||
@Inject
|
||||
public UploadClient(UploadInterface uploadInterface,
|
||||
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient,
|
||||
PageContentsCreator pageContentsCreator) {
|
||||
this.uploadInterface = uploadInterface;
|
||||
this.csrfTokenClient = csrfTokenClient;
|
||||
this.pageContentsCreator = pageContentsCreator;
|
||||
private final UploadInterface uploadInterface;
|
||||
private final CsrfTokenClient csrfTokenClient;
|
||||
private final PageContentsCreator pageContentsCreator;
|
||||
private final FileUtilsWrapper fileUtilsWrapper;
|
||||
|
||||
@Inject
|
||||
public UploadClient(final UploadInterface uploadInterface,
|
||||
@Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
|
||||
final PageContentsCreator pageContentsCreator,
|
||||
final FileUtilsWrapper fileUtilsWrapper) {
|
||||
this.uploadInterface = uploadInterface;
|
||||
this.csrfTokenClient = csrfTokenClient;
|
||||
this.pageContentsCreator = pageContentsCreator;
|
||||
this.fileUtilsWrapper = fileUtilsWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload file to stash in chunks of specified size. Uploading files in chunks will make handling
|
||||
* of large files easier. Also, it will be useful in supporting pause/resume of uploads
|
||||
*/
|
||||
Observable<UploadResult> uploadFileToStash(
|
||||
final Context context, final String filename, final File file,
|
||||
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
|
||||
final Observable<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
|
||||
final MediaType mediaType = MediaType
|
||||
.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
|
||||
|
||||
final long[] offset = {0};
|
||||
final String[] fileKey = {null};
|
||||
final AtomicReference<UploadResult> result = new AtomicReference<>();
|
||||
fileChunks.blockingForEach(chunkFile -> {
|
||||
final RequestBody requestBody = RequestBody
|
||||
.create(mediaType, chunkFile);
|
||||
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
|
||||
notificationUpdater::onProgress, offset[0], file.length());
|
||||
uploadChunkToStash(filename,
|
||||
file.length(),
|
||||
offset[0],
|
||||
fileKey[0],
|
||||
countingRequestBody).blockingSubscribe(uploadResult -> {
|
||||
result.set(uploadResult);
|
||||
offset[0] = uploadResult.getOffset();
|
||||
fileKey[0] = uploadResult.getFilekey();
|
||||
});
|
||||
});
|
||||
return Observable.just(result.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uploads a file chunk to stash
|
||||
*
|
||||
* @param filename The name of the file being uploaded
|
||||
* @param fileSize The total size of the file
|
||||
* @param offset The offset returned by the previous chunk upload
|
||||
* @param fileKey The filekey returned by the previous chunk upload
|
||||
* @param countingRequestBody Request body with chunk file
|
||||
* @return
|
||||
*/
|
||||
Observable<UploadResult> uploadChunkToStash(final String filename,
|
||||
final long fileSize,
|
||||
final long offset,
|
||||
final String fileKey,
|
||||
final CountingRequestBody countingRequestBody) {
|
||||
final MultipartBody.Part filePart = MultipartBody.Part
|
||||
.createFormData("chunk", filename, countingRequestBody);
|
||||
try {
|
||||
return uploadInterface.uploadFileToStash(toRequestBody(filename),
|
||||
toRequestBody(String.valueOf(fileSize)),
|
||||
toRequestBody(String.valueOf(offset)),
|
||||
toRequestBody(fileKey),
|
||||
toRequestBody(csrfTokenClient.getTokenBlocking()),
|
||||
filePart)
|
||||
.map(UploadResponse::getUpload);
|
||||
} catch (final Throwable throwable) {
|
||||
Timber.e(throwable, "Failed to upload chunk to stash");
|
||||
return Observable.error(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
Observable<UploadResult> uploadFileToStash(Context context, String filename, File file,
|
||||
NotificationUpdateProgressListener notificationUpdater) {
|
||||
RequestBody requestBody = RequestBody
|
||||
.create(MediaType.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath()))), file);
|
||||
@Nullable
|
||||
private RequestBody toRequestBody(@Nullable final String value) {
|
||||
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
|
||||
}
|
||||
|
||||
CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
|
||||
(bytesWritten, contentLength) -> notificationUpdater
|
||||
.onProgress(bytesWritten, contentLength));
|
||||
|
||||
MultipartBody.Part filePart = MultipartBody.Part.createFormData("file", filename, countingRequestBody);
|
||||
RequestBody fileNameRequestBody = RequestBody.create(okhttp3.MultipartBody.FORM, filename);
|
||||
RequestBody tokenRequestBody;
|
||||
try {
|
||||
tokenRequestBody = RequestBody.create(MultipartBody.FORM, csrfTokenClient.getTokenBlocking());
|
||||
return uploadInterface.uploadFileToStash(fileNameRequestBody, tokenRequestBody, filePart)
|
||||
.map(stashUploadResponse -> stashUploadResponse.getUpload());
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
return Observable.error(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
Observable<UploadResult> uploadFileFromStash(Context context,
|
||||
Contribution contribution,
|
||||
String uniqueFileName,
|
||||
String fileKey) {
|
||||
try {
|
||||
return uploadInterface
|
||||
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
|
||||
pageContentsCreator.createFrom(contribution),
|
||||
CommonsApplication.DEFAULT_EDIT_SUMMARY,
|
||||
uniqueFileName,
|
||||
fileKey).map(uploadResponse -> uploadResponse.getUpload());
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
return Observable.error(throwable);
|
||||
}
|
||||
Observable<UploadResult> uploadFileFromStash(final Context context,
|
||||
final Contribution contribution,
|
||||
final String uniqueFileName,
|
||||
final String fileKey) {
|
||||
try {
|
||||
return uploadInterface
|
||||
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
|
||||
pageContentsCreator.createFrom(contribution),
|
||||
CommonsApplication.DEFAULT_EDIT_SUMMARY,
|
||||
uniqueFileName,
|
||||
fileKey).map(UploadResponse::getUpload);
|
||||
} catch (final Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
return Observable.error(throwable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,19 +16,22 @@ import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
|
|||
|
||||
public interface UploadInterface {
|
||||
|
||||
@Multipart
|
||||
@POST(MW_API_PREFIX + "action=upload&stash=1&ignorewarnings=1")
|
||||
Observable<UploadResponse> uploadFileToStash(@Part("filename") RequestBody filename,
|
||||
@Part("token") RequestBody token,
|
||||
@Part MultipartBody.Part filePart);
|
||||
@Multipart
|
||||
@POST(MW_API_PREFIX + "action=upload&stash=1&ignorewarnings=1")
|
||||
Observable<UploadResponse> uploadFileToStash(@Part("filename") RequestBody filename,
|
||||
@Part("filesize") RequestBody totalFileSize,
|
||||
@Part("offset") RequestBody offset,
|
||||
@Part("filekey") RequestBody fileKey,
|
||||
@Part("token") RequestBody token,
|
||||
@Part MultipartBody.Part filePart);
|
||||
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=upload&ignorewarnings=1")
|
||||
@FormUrlEncoded
|
||||
@NonNull
|
||||
Observable<UploadResponse> uploadFileFromStash(@NonNull @Field("token") String token,
|
||||
@NonNull @Field("text") String text,
|
||||
@NonNull @Field("comment") String comment,
|
||||
@NonNull @Field("filename") String filename,
|
||||
@NonNull @Field("filekey") String filekey);
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=upload&ignorewarnings=1")
|
||||
@FormUrlEncoded
|
||||
@NonNull
|
||||
Observable<UploadResponse> uploadFileFromStash(@NonNull @Field("token") String token,
|
||||
@NonNull @Field("text") String text,
|
||||
@NonNull @Field("comment") String comment,
|
||||
@NonNull @Field("filename") String filename,
|
||||
@NonNull @Field("filekey") String filekey);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ private const val RESULT_SUCCESS = "Success"
|
|||
data class UploadResult(
|
||||
val result: String,
|
||||
val filekey: String,
|
||||
val offset: Int,
|
||||
val filename: String,
|
||||
val sessionkey: String,
|
||||
val imageinfo: ImageInfo
|
||||
|
|
|
|||
|
|
@ -22,24 +22,16 @@ import fr.free.nrw.commons.contributions.MainActivity;
|
|||
import fr.free.nrw.commons.di.CommonsApplicationModule;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerService;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import fr.free.nrw.commons.wikidata.WikidataEditService;
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.processors.PublishProcessor;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.text.ParseException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -316,7 +308,7 @@ public class UploadService extends CommonsDaggerService {
|
|||
.add(wikidataEditService.addDepictionsAndCaptions(uploadResult, contribution));
|
||||
WikidataPlace wikidataPlace = contribution.getWikidataPlace();
|
||||
if (wikidataPlace != null && wikidataPlace.getImageValue() == null) {
|
||||
wikidataEditService.createImageClaim(wikidataPlace, uploadResult);
|
||||
wikidataEditService.createClaim(wikidataPlace, uploadResult.getFilename(), contribution.getCaptions());
|
||||
}
|
||||
saveCompletedContribution(contribution, uploadResult);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
import fr.free.nrw.commons.upload.WikidataItem;
|
||||
import com.google.gson.Gson;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -9,64 +9,38 @@ import javax.inject.Singleton;
|
|||
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.ObservableSource;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.RequestBody;
|
||||
import org.wikipedia.wikidata.Statement_partial;
|
||||
|
||||
@Singleton
|
||||
public class WikidataClient {
|
||||
|
||||
|
||||
private final WikidataInterface wikidataInterface;
|
||||
private final WikidataInterface wikidataInterface;
|
||||
private final Gson gson;
|
||||
|
||||
@Inject
|
||||
public WikidataClient(WikidataInterface wikidataInterface) {
|
||||
this.wikidataInterface = wikidataInterface;
|
||||
}
|
||||
@Inject
|
||||
public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) {
|
||||
this.wikidataInterface = wikidataInterface;
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create wikidata claim to add P18 value
|
||||
* @param entity wikidata entity ID
|
||||
* @param value value of the P18 edit
|
||||
* @return revisionID of the edit
|
||||
*/
|
||||
Observable<Long> createImageClaim(WikidataItem entity, String value) {
|
||||
return getCsrfToken()
|
||||
.flatMap(csrfToken -> wikidataInterface.postCreateClaim(
|
||||
toRequestBody(entity.getId()),
|
||||
toRequestBody("value"),
|
||||
toRequestBody(WikidataProperties.IMAGE.getPropertyName()),
|
||||
toRequestBody(value),
|
||||
toRequestBody("en"),
|
||||
toRequestBody(csrfToken)))
|
||||
.map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
|
||||
}
|
||||
/**
|
||||
* Create wikidata claim to add P18 value
|
||||
*
|
||||
* @return revisionID of the edit
|
||||
*/
|
||||
Observable<Long> setClaim(Statement_partial claim, String tags) {
|
||||
return getCsrfToken()
|
||||
.flatMap(csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken))
|
||||
.map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts string value to RequestBody for multipart request
|
||||
*/
|
||||
private RequestBody toRequestBody(String value) {
|
||||
return RequestBody.create(MediaType.parse("text/plain"), value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get csrf token for wikidata edit
|
||||
*/
|
||||
@NotNull
|
||||
private Observable<String> getCsrfToken() {
|
||||
return wikidataInterface.getCsrfToken().map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add edit tag for a given revision ID. The app currently uses this to tag P18 edits
|
||||
* @param revisionId revision ID of the page edited
|
||||
* @param tag to be added
|
||||
* @param reason to be mentioned
|
||||
*/
|
||||
ObservableSource<AddEditTagResponse> addEditTag(Long revisionId, String tag, String reason) {
|
||||
return getCsrfToken()
|
||||
.flatMap(csrfToken -> wikidataInterface.addEditTag(String.valueOf(revisionId),
|
||||
tag,
|
||||
reason,
|
||||
csrfToken));
|
||||
}
|
||||
/**
|
||||
* Get csrf token for wikidata edit
|
||||
*/
|
||||
@NotNull
|
||||
private Observable<String> getCsrfToken() {
|
||||
return wikidataInterface.getCsrfToken()
|
||||
.map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,49 +20,58 @@ import io.reactivex.android.schedulers.AndroidSchedulers;
|
|||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
import org.wikipedia.dataclient.mwapi.MwPostResponse;
|
||||
import org.wikipedia.wikidata.DataValue;
|
||||
import org.wikipedia.wikidata.DataValue.ValueString;
|
||||
import org.wikipedia.wikidata.EditClaim;
|
||||
import org.wikipedia.wikidata.Snak_partial;
|
||||
import org.wikipedia.wikidata.Statement_partial;
|
||||
import org.wikipedia.wikidata.WikiBaseMonolingualTextValue;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* This class is meant to handle the Wikidata edits made through the app
|
||||
* It will talk with MediaWiki Apis to make the necessary calls, log the edits and fire listeners
|
||||
* on successful edits
|
||||
* This class is meant to handle the Wikidata edits made through the app It will talk with MediaWiki
|
||||
* Apis to make the necessary calls, log the edits and fire listeners on successful edits
|
||||
*/
|
||||
@Singleton
|
||||
public class WikidataEditService {
|
||||
|
||||
private static final String COMMONS_APP_TAG = "wikimedia-commons-app";
|
||||
private static final String COMMONS_APP_EDIT_REASON = "Add tag for edits made using Android Commons app";
|
||||
public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
|
||||
|
||||
private final Context context;
|
||||
private final WikidataEditListener wikidataEditListener;
|
||||
private final JsonKvStore directKvStore;
|
||||
private final WikiBaseClient wikiBaseClient;
|
||||
private final WikidataClient wikidataClient;
|
||||
private final Gson gson;
|
||||
private final Context context;
|
||||
private final WikidataEditListener wikidataEditListener;
|
||||
private final JsonKvStore directKvStore;
|
||||
private final WikiBaseClient wikiBaseClient;
|
||||
private final WikidataClient wikidataClient;
|
||||
private final Gson gson;
|
||||
|
||||
@Inject
|
||||
public WikidataEditService(final Context context,
|
||||
public WikidataEditService(final Context context,
|
||||
final WikidataEditListener wikidataEditListener,
|
||||
@Named("default_preferences") final JsonKvStore directKvStore,
|
||||
final WikiBaseClient wikiBaseClient,
|
||||
final WikidataClient wikidataClient, final Gson gson) {
|
||||
this.context = context;
|
||||
this.wikidataEditListener = wikidataEditListener;
|
||||
this.directKvStore = directKvStore;
|
||||
this.wikiBaseClient = wikiBaseClient;
|
||||
this.wikidataClient = wikidataClient;
|
||||
this.context = context;
|
||||
this.wikidataEditListener = wikidataEditListener;
|
||||
this.directKvStore = directKvStore;
|
||||
this.wikiBaseClient = wikiBaseClient;
|
||||
this.wikidataClient = wikidataClient;
|
||||
this.gson = gson;
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits the wikibase entity by adding DEPICTS property.
|
||||
* Adding DEPICTS property requires call to the wikibase API to set tag against the entity.
|
||||
* Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call to
|
||||
* the wikibase API to set tag against the entity.
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private Observable<Boolean> addDepictsProperty(final String fileEntityId,
|
||||
|
|
@ -70,7 +79,7 @@ public class WikidataEditService {
|
|||
|
||||
final EditClaim data = editClaim(
|
||||
ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
|
||||
: depictedItem.getId()
|
||||
: depictedItem.getId()
|
||||
);
|
||||
|
||||
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
|
||||
|
|
@ -81,44 +90,46 @@ public class WikidataEditService {
|
|||
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
|
||||
}
|
||||
})
|
||||
.doOnError( throwable -> {
|
||||
.doOnError(throwable -> {
|
||||
Timber.e(throwable, "Error occurred while setting DEPICTS property");
|
||||
ViewUtil.showLongToast(context, throwable.toString());
|
||||
})
|
||||
.subscribeOn(Schedulers.io());
|
||||
}
|
||||
}
|
||||
|
||||
private EditClaim editClaim(final String entityId) {
|
||||
return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Show a success toast when the edit is made successfully
|
||||
*/
|
||||
private void showSuccessToast(final String wikiItemName) {
|
||||
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
|
||||
final String successMessage = String.format(Locale.getDefault(), successStringTemplate, wikiItemName);
|
||||
ViewUtil.showLongToast(context, successMessage);
|
||||
}
|
||||
* Show a success toast when the edit is made successfully
|
||||
*/
|
||||
private void showSuccessToast(final String wikiItemName) {
|
||||
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
|
||||
final String successMessage = String
|
||||
.format(Locale.getDefault(), successStringTemplate, wikiItemName);
|
||||
ViewUtil.showLongToast(context, successMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from csrfTokenClient
|
||||
*
|
||||
* @param fileEntityId
|
||||
* @return
|
||||
*/
|
||||
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
|
||||
* csrfTokenClient
|
||||
*
|
||||
* @param fileEntityId
|
||||
* @return
|
||||
*/
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
|
||||
final String captionValue) {
|
||||
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
|
||||
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse) )
|
||||
.doOnError(throwable -> {
|
||||
Timber.e(throwable, "Error occurred while setting Captions");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
})
|
||||
.map(mwPostResponse -> mwPostResponse != null);
|
||||
}
|
||||
@SuppressLint("CheckResult")
|
||||
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
|
||||
final String captionValue) {
|
||||
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
|
||||
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
|
||||
.doOnError(throwable -> {
|
||||
Timber.e(throwable, "Error occurred while setting Captions");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
})
|
||||
.map(mwPostResponse -> mwPostResponse != null);
|
||||
}
|
||||
|
||||
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
|
||||
if (response != null) {
|
||||
|
|
@ -128,29 +139,41 @@ public class WikidataEditService {
|
|||
}
|
||||
}
|
||||
|
||||
public void createImageClaim(@Nullable final WikidataPlace wikidataPlace, final UploadResult imageUpload) {
|
||||
public void createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, final
|
||||
Map<String, String> captions) {
|
||||
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
|
||||
Timber.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
|
||||
Timber
|
||||
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
|
||||
return;
|
||||
}
|
||||
editWikidataImageProperty(wikidataPlace, imageUpload);
|
||||
addImageAndMediaLegends(wikidataPlace, fileName, captions);
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void editWikidataImageProperty(final WikidataItem wikidataItem, final UploadResult imageUpload) {
|
||||
wikidataClient.createImageClaim(wikidataItem, String.format("\"%s\"", imageUpload.getFilename()))
|
||||
.flatMap(revisionId -> {
|
||||
if (revisionId != -1) {
|
||||
return wikidataClient.addEditTag(revisionId, COMMONS_APP_TAG, COMMONS_APP_EDIT_REASON);
|
||||
}
|
||||
throw new RuntimeException("Unable to edit wikidata item");
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
public void addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
|
||||
final Map<String, String> captions) {
|
||||
final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(),
|
||||
new ValueString(fileName.replace("File:", "")));
|
||||
|
||||
final List<Snak_partial> snaks = new ArrayList<>();
|
||||
for (final Map.Entry<String, String> entry : captions.entrySet()) {
|
||||
snaks.add(new Snak_partial("value",
|
||||
WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
|
||||
new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
|
||||
}
|
||||
|
||||
final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
|
||||
final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id,
|
||||
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
|
||||
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
|
||||
|
||||
wikidataClient.setClaim(claim, COMMONS_APP_TAG).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)), throwable -> {
|
||||
Timber.e(throwable, "Error occurred while making claim");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
});
|
||||
.subscribe(revisionId -> handleImageClaimResult(wikidataItem, String.valueOf(revisionId)),
|
||||
throwable -> {
|
||||
Timber.e(throwable, "Error occurred while making claim");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
});
|
||||
;
|
||||
}
|
||||
|
||||
private void handleImageClaimResult(final WikidataItem wikidataItem, final String revisionId) {
|
||||
|
|
@ -185,9 +208,9 @@ public class WikidataEditService {
|
|||
}
|
||||
}
|
||||
).subscribe(
|
||||
success -> Timber.d("edit response: %s", success),
|
||||
throwable -> Timber.e(throwable, "posting edits failed")
|
||||
);
|
||||
success -> Timber.d("edit response: %s", success),
|
||||
throwable -> Timber.e(throwable, "posting edits failed")
|
||||
);
|
||||
}
|
||||
|
||||
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
|
||||
|
|
@ -202,6 +225,6 @@ public class WikidataEditService {
|
|||
depictedItems.add(wikidataPlace);
|
||||
}
|
||||
return Observable.fromIterable(depictedItems)
|
||||
.concatMap( wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
|
||||
.concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package fr.free.nrw.commons.wikidata;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
|
||||
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse;
|
||||
|
|
@ -20,35 +21,21 @@ import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
|
|||
|
||||
public interface WikidataInterface {
|
||||
|
||||
/**
|
||||
* Wikidata create claim API. Posts a new claim for the given entity ID
|
||||
*/
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST("w/api.php?format=json&errorformat=plaintext&action=wbcreateclaim&errorlang=uselang")
|
||||
@Multipart
|
||||
Observable<WbCreateClaimResponse> postCreateClaim(@NonNull @Part("entity") RequestBody entity,
|
||||
@NonNull @Part("snaktype") RequestBody snakType,
|
||||
@NonNull @Part("property") RequestBody property,
|
||||
@NonNull @Part("value") RequestBody value,
|
||||
@NonNull @Part("uselang") RequestBody useLang,
|
||||
@NonNull @Part("token") RequestBody token);
|
||||
/**
|
||||
* Get edit token for wikidata wiki site
|
||||
*/
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
||||
@NonNull
|
||||
Observable<MwQueryResponse> getCsrfToken();
|
||||
|
||||
/**
|
||||
* Add edit tag and reason for any revision
|
||||
*/
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST(MW_API_PREFIX + "action=tag")
|
||||
@FormUrlEncoded
|
||||
Observable<AddEditTagResponse> addEditTag(@NonNull @Field("revid") String revId,
|
||||
@NonNull @Field("add") String tagName,
|
||||
@NonNull @Field("reason") String reason,
|
||||
@NonNull @Field("token") String token);
|
||||
|
||||
/**
|
||||
* Get edit token for wikidata wiki site
|
||||
*/
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@GET(MW_API_PREFIX + "action=query&meta=tokens&type=csrf")
|
||||
@NonNull
|
||||
Observable<MwQueryResponse> getCsrfToken();
|
||||
/**
|
||||
* Wikidata create claim API. Posts a new claim for the given entity ID
|
||||
*/
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@POST("w/api.php?format=json&action=wbsetclaim")
|
||||
@FormUrlEncoded
|
||||
Observable<WbCreateClaimResponse> postSetClaim(@NonNull @Field("claim") String request,
|
||||
@NonNull @Field("tags") String tags,
|
||||
@NonNull @Field("token") String token);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,5 +6,6 @@ enum class WikidataProperties(val propertyName: String) {
|
|||
IMAGE("P18"),
|
||||
DEPICTS(BuildConfig.DEPICTS_PROPERTY),
|
||||
COMMONS_CATEGORY("P373"),
|
||||
INSTANCE_OF("P31");
|
||||
INSTANCE_OF("P31"),
|
||||
MEDIA_LEGENDS("P2096");
|
||||
}
|
||||
|
|
|
|||
76
app/src/main/res/layout/dialog_nearby.xml
Normal file
76
app/src/main/res/layout/dialog_nearby.xml
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingLeft="20dp"
|
||||
android:paddingRight="20dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="@string/showcase_view_whole_nearby_activity"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_custom_map_marker" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:text="@string/showcase_view_needs_photo"/>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_custom_map_marker_green" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:text="@string/showcase_view_has_photo"/>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:scaleType="fitCenter"
|
||||
app:srcCompat="@drawable/ic_custom_map_marker_grey" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="0"
|
||||
android:text="@string/showcase_view_no_longer_exists"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
@ -4,6 +4,7 @@
|
|||
* Abijeet Patro
|
||||
* Amirsara
|
||||
* Arash.pt
|
||||
* BaRaN6161 TURK
|
||||
* Ebraminio
|
||||
* Eshagh79
|
||||
* FarsiNevis
|
||||
|
|
@ -28,7 +29,11 @@
|
|||
<item quantity="one">%1$d پرونده در حال بارگذاری</item>
|
||||
<item quantity="other">%1$d پرونده در حال بارگذاری</item>
|
||||
</plurals>
|
||||
<string name="contributions_subtitle">{{%1$d|zero=@string/contributions_subtitle_zero|one=(%1$d)|(%1$d)}}</string>
|
||||
<plurals name="contributions_subtitle">
|
||||
<item quantity="zero">\@string/contributions_subtitle_zero</item>
|
||||
<item quantity="one">(%1$d)</item>
|
||||
<item quantity="other">(%1$d)</item>
|
||||
</plurals>
|
||||
<plurals name="starting_multiple_uploads">
|
||||
<item quantity="one">شروع %1$d بارگذاری پرونده</item>
|
||||
<item quantity="other">شروع بارگذاری %1$d پرونده</item>
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@
|
|||
</plurals>
|
||||
<plurals name="multiple_uploads_title">
|
||||
<item quantity="one">%1$d개 올리적재</item>
|
||||
<item quantity="other">%1$d개 올리적재</item>
|
||||
</plurals>
|
||||
<string name="share_license_summary">이 그림은 %1$s에 따라 사용이 허가됩니다</string>
|
||||
<string name="title_activity_explore">찾아보기</string>
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@
|
|||
</plurals>
|
||||
<plurals name="multiple_uploads_title">
|
||||
<item quantity="one">%1$d개 업로드</item>
|
||||
<item quantity="other">%1$d개 업로드</item>
|
||||
</plurals>
|
||||
<plurals name="share_license_summary">
|
||||
<item quantity="one">이 그림은 %1$s에 따라 사용이 허가됩니다</item>
|
||||
|
|
|
|||
10
app/src/main/res/values-nqo/error.xml
Normal file
10
app/src/main/res/values-nqo/error.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Authors:
|
||||
* Lancine.kounfantoh.fofana
|
||||
-->
|
||||
<resources>
|
||||
<string name="crash_dialog_title">ߞߐߡߐ߲ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
|
||||
<string name="crash_dialog_text">ߋߜߋ߫. ߞߏ ߘߏ߫ ߓߍ߲߬ߣߍ߲߫ ߕߎ߲߬ ߕߍ߫߹</string>
|
||||
<string name="crash_dialog_comment_prompt">ߊ߲ ߠߎ߬ ߘߍ߬ߡߍ߲߬ ߊ߬ ߘߐߓߍ߲߬ߠߌ߲ ߡߊ߬߹</string>
|
||||
<string name="crash_dialog_ok_toast">ߌ ߣߌ߫ ߗߋ߫߹</string>
|
||||
</resources>
|
||||
|
|
@ -50,10 +50,14 @@
|
|||
<string name="provider_contributions">ߒ ߠߊ߫ ߟߊ߬ߦߟߍ߬ߣߍ߲ ߠߎ߬</string>
|
||||
<string name="menu_share">ߊ߬ ߟߊߖߍ߲ߛߍ߲߫</string>
|
||||
<string name="menu_open_in_browser">ߊ߬ ߘߐߜߍ߫ ߛߏ߲߯ߓߊߟߊ߲ ߠߊ߫</string>
|
||||
<string name="share_title_hint" fuzzy="true">ߞߎ߲߬ߕߐ߰ ߞߊ߬ߣߌ߲߬ߣߍ߲</string>
|
||||
<string name="share_title_hint">ߝߍ߬ߛߓߍߟߌ (ߡߊߢߌ߬ߣߌ߲߬ߞߊ߬ߣߍ߲)</string>
|
||||
<string name="add_caption_toast">ߝߍ߬ߛߓߍߟߌ ߘߏ߫ ߡߊߛߐ߫ ߞߐߕߐ߮ ߣߌ߲߬ ߠߊ߫ ߖߊ߰ߣߌ߲߫</string>
|
||||
<string name="share_description_hint">ߞߊ߲߬ߛߓߍߟߌ</string>
|
||||
<string name="share_caption_hint">ߝߍ߬ߛߓߍߟߌ (ߞߐߘߊ߲ ߦߋ߫ ߛߓߍߘߋ߲߫ ߂߅߅ ߟߋ߬ ߘߌ߫)</string>
|
||||
<string name="login_failed_network">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߛߐ߲߬ߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߊ߫ - ߞߙߏߝߏ ߟߊ߫ ߗߌߙߏ߲ߠߌ߲</string>
|
||||
<string name="login_failed_wrong_credentials">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߛߐ߲߬ߣߍ߲߫ ߕߍ߫ ߞߍ߫ ߟߊ߫ - ߌ ߟߊ߫ ߕߐ߯ ߟߊߓߊ߯ߕߊ ߣߌ߫ ߕߊ߬ߡߌ߲߬ߞߊ߲ ߠߎ߬ ߡߊߝߟߍ߫ ߖߊ߰ߣߌ߲߬</string>
|
||||
<string name="login_failed_throttled">ߛߊ߯ߛߊ߯ߟߌ߫ ߛߎߘߊ߲ߓߊߟߌ߫ ߛߘߍߡߊ߲߫ ߓߘߊ߫ ߞߍ߫ ߢߐ߲߮ ߞߐ߫ ߞߏߖߎ߯ߦߊ߫.ߊ߬ ߡߊߝߍߣߍ߲߫ ߡߌ߬ߛߍ߲߬ ߘߊ߲ߘߐ߫ ߞߐ߫ ߖߊ߰ߣߌ߲߫.</string>
|
||||
<string name="login_failed_blocked">ߤߊߞߍ߬ߕߏ߫߸ ߟߊ߬ߓߊ߰ߙߊ߬ߟߊ ߣߌ߲߬ ߓߘߊ߫ ߓߊ߬ߟߌ߬ ߞߐ߬ߡߐ߲ ߞߊ߲߬</string>
|
||||
<string name="login_failed_generic">ߜߊ߲߬ߞߎ߲߬ߠߌ߲ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
|
||||
<string name="share_upload_button">ߊ߬ ߟߊߦߟߍ߬</string>
|
||||
<string name="multiple_share_base_title">ߟߊ߬ߘߏ߲߬ߠߌ߲ ߣߌ߲߬ ߕߐ߯ߟߊ߫</string>
|
||||
|
|
@ -74,7 +78,7 @@
|
|||
<string name="title_activity_featured_images">ߟߊߓߊ߯ߙߊߟߌ ߖߌ߬ߦߊ߬ߓߍ ߟߎ߬</string>
|
||||
<string name="title_activity_category_details">ߦߌߟߡߊ</string>
|
||||
<string name="menu_about">ߞߊ߬ ߓߍ߲߬</string>
|
||||
<string name="about_privacy_policy" fuzzy="true"><u>ߜߎ߲߬ߘߎ߬ߢߐ߲߰ߦߊ ߞߎߙߎ߲߬ߘߎ</u></string>
|
||||
<string name="about_privacy_policy">ߜߎ߲߬ߘߎ߬ߢߐ߲߰ߦߊ ߞߎߙߎ߲߬ߘߎ</string>
|
||||
<string name="title_activity_about">ߞߊ߬ ߓߍ߲߬</string>
|
||||
<string name="menu_feedback">ߞߙߐ߬ߛߌ߬ߕߊ ߗߋ߫ (ߢ:ߞߏ߲ߘߏ ߟߊ߫)</string>
|
||||
<string name="no_email_client">ߢߎߡߍߙߋ߲ߞߏ߲ߘߏ ߛߊ߲߬ߠߌ߲߬ߢߐ߲߰ ߡߊߞߍߣߍ߲߫ ߕߴߦߋ߲߬</string>
|
||||
|
|
@ -107,15 +111,21 @@
|
|||
<string name="detail_panel_cats_label">ߦߌߟߡߊ ߟߎ߬</string>
|
||||
<string name="detail_panel_cats_loading">ߟߊ߬ߢߎ߲߬ߠߌ߲ ߦߵߌ ߘߐ߫...</string>
|
||||
<string name="detail_panel_cats_none">ߊ߬ ߡߊ߫ ߓߊߕߐ߬ߡߐ߲߬</string>
|
||||
<string name="detail_caption_empty">ߝߍ߬ߛߓߍߟߌ߫ ߕߍ߫ ߦߋ߲߬</string>
|
||||
<string name="detail_description_empty">ߞߊ߲߬ߛߓߍߟߌ߫ ߕߴߦߋ߲߬</string>
|
||||
<string name="detail_discussion_empty">ߘߊߘߐߖߊߥߏ߫ ߕߴߦߋ߲߬</string>
|
||||
<string name="ok">ߏ߬ߞߍ߫</string>
|
||||
<string name="title_activity_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ ߟߎ߬</string>
|
||||
<string name="no_nearby">ߛߌ߰ߢߐ߲߰ ߦߙߐ߫ ߡߊ߫ ߛߐ߬ߘߐ߲߫</string>
|
||||
<string name="warning">ߖߊ߲߬ߓߌ߬ߟߊ߬ߟߌ</string>
|
||||
<string name="duplicate_image_found">ߖߌ߬ߦߊ߬ߓߍ߫ ߓߊߟߌߣߍ߲ ߠߎ߬ ߦߋ߫ ߦߋ߲߬</string>
|
||||
<string name="upload_image_duplicate">ߞߐߕߐ߮ ߣߌ߲߬ ߓߘߊ߫ ߘߐߕߌߢߍ߫ ߞߐߡߐ߲ ߞߊ߲߬. ߌ ߟߊߣߴߊ߬ ߟߊ߫ ߞߴߌ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߘߊߓߊ߲߫؟</string>
|
||||
<string name="upload">ߊ߬ ߟߊߦߟߍ߬</string>
|
||||
<string name="yes">ߐ߲߬ߐ߲߬ߐ߲߫</string>
|
||||
<string name="no">ߍ߲߬ߍ߲߫</string>
|
||||
<string name="media_detail_caption">ߝߍ߬ߛߓߍߟߌ</string>
|
||||
<string name="media_detail_title">ߞߎ߲߬ߕߐ߮</string>
|
||||
<string name="media_detail_depiction">ߞߐߦߌߘߊߟߌ</string>
|
||||
<string name="media_detail_description">ߞߊ߲߬ߛߓߍߟߌ</string>
|
||||
<string name="media_detail_discussion">ߘߊߘߐߖߊߥߏ</string>
|
||||
<string name="media_detail_author">ߛߓߍߦߟߊ</string>
|
||||
|
|
@ -166,11 +176,15 @@
|
|||
<string name="upload_problem_different_geolocation">ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߕߊ߬ߣߍ߲߬ ߦߋ߫ ߦߙߐ߫ ߜߘߍ߫ ߟߋ߬</string>
|
||||
<string name="upload_problem_fbmd">ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߋ߬ ߟߊߦߍ߬ߟߍ߫ ߖߊ߰ߣߌ߲߫ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߒ߬ߞߊ߬ ߌ ߞߊ߫ ߖߌ߬ߦߊ߬ߓߍ߫ ߟߊߦߍ߬ߟߍ߫ ߡߎ߰ߡߍ߫ ߌ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬ ߡߐ߱ ߟߎ߬ ߟߊ߫ ߝߋߛߑߓߎߞ ߞߊ߲߬.</string>
|
||||
<string name="upload_problem_do_you_continue">ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߠߊߦߟߍ߬ ߡߎߣߎ߲߬؟</string>
|
||||
<string name="upload_problem_image">ߝߙߋߞߋ ߓߘߊ߫ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ ߟߊ߫</string>
|
||||
<string name="internet_downloaded">ߖߊ߰ߣߌ߲߬ ߌ ߦߋ߫ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߎ߫ ߟߋ߬ ߟߊߦߟߍ߬ ߌ ߖߍ߬ߘߍ ߞߊ߬ ߡߍ߲ ߠߎ߬ ߕߊ߬. ߞߏ߬ߣߌ߲߬ ߌ ߞߊߣߊ߬ ߖߌ߬ߦߊ߬ߓߍ߬ ߟߊߦߟߍ߬ ߌ ߣߊ߬ ߡߍ߲ ߠߊߖߌ߰ ߟߴߌ ߞߎ߲߬ ߓߟߐߟߐ ߟߊ߫.</string>
|
||||
<string name="give_permission">ߟߊ߬ߘߌ߬ߢߍ߬ߟߌ ߦߌ߬ߘߊ߬</string>
|
||||
<string name="login_to_your_account">ߌ ߜߊ߲߬ߞߎ߲߫ ߌ ߟߊ߫ ߖߊ߬ߕߋ߬ߘߊ ߟߊ߫.</string>
|
||||
<string name="no_web_browser">ߓߟߐߟߐ ߛߏ߲߯ߓߊߟߊ߲߫ ߡߊ߫ ߛߐ߬ߘߐ߲߬ ߞߊ߬ URL ߟߊߞߊ߬</string>
|
||||
<string name="null_url">ߝߎ߬ߕߎ߲߬ߕߌ߹ URL ߡߊ߫ ߛߐ߬ߘߐ߲߬</string>
|
||||
<string name="nominated_see_more">ߞߍߦߙߐ߫ ߞߐߜߍ ߘߐߜߍ߫ ߝߊߙߊ߲ߝߊ߯ߛߌ߫ ߞߏ ߘߐ߫</string>
|
||||
<string name="nominating_file_for_deletion">ߕߐ߯ߦߊߟߌ ߖߏ߰ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</string>
|
||||
<string name="nominating_for_deletion_status">ߞߐߕߐ߮ ߕߐ߯ߦߊߟߌ ߖߏ߬ߛߟߌ߬ ߞߊ߲ߡߊ߬: %1$s</string>
|
||||
<string name="view_browser">ߊ߬ ߘߐߜߍ߫ ߛߏ߲߯ߓߊߟߊ߲ ߠߊ߫</string>
|
||||
<string name="skip_login">ߊ߬ ߟߊߜߊ߲߫</string>
|
||||
<string name="navigation_item_login">ߌ ߜߊ߲߬ߞߎ߲߬</string>
|
||||
|
|
@ -187,11 +201,11 @@
|
|||
<string name="nearby_wikidata">ߥߞߌߘߕߊ</string>
|
||||
<string name="nearby_wikipedia">ߥߞߌߔߋߘߌߦߊ߫</string>
|
||||
<string name="nearby_commons">ߞߐߡߐ߲</string>
|
||||
<string name="about_rate_us" fuzzy="true"><u>ߊ߲ ߡߐ߬ߟߐ߲ ߦߌ߬ߘߊ߬</u></string>
|
||||
<string name="about_faq" fuzzy="true"><u>ߢ.ߡ</u></string>
|
||||
<string name="about_rate_us">ߡߐ߬ߟߐ߲ ߘߴߊ߲ ߡߊ߬</string>
|
||||
<string name="about_faq">ߢ.ߡ</string>
|
||||
<string name="no_internet">ߓߟߐߟߐ߫ ߕߴߦߋ߲߬</string>
|
||||
<string name="internet_established">ߓߟߐߟߐ ߦߋ߫ ߦߋ߲߬</string>
|
||||
<string name="about_translate" fuzzy="true"><u>ߘߟߊߡߌߘߊߟߌ</u></string>
|
||||
<string name="about_translate">ߘߟߊߡߌߘߊߟߌ</string>
|
||||
<string name="about_translate_title">ߞߊ߲ ߠߎ߬</string>
|
||||
<string name="about_translate_message">ߞߊ߲ ߘߏ߫ ߓߊߕߐ߬ߡߐ߲߬ ߌ ߦߴߊ߬ ߝߍ߬ ߞߵߊ߬ ߘߟߊߡߌ߬ߘߊ߬ ߡߍ߲ ߘߐ߫.</string>
|
||||
<string name="about_translate_cancel">ߊ߬ ߘߐߛߊ߬</string>
|
||||
|
|
@ -274,6 +288,11 @@
|
|||
<string name="review_thanks">ߊ߬ ߟߐ߯ ߦߴߌ ߟߊ߫ ߞߊ߬ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ߠߊ ߟߎ߬ ߞߎߟߎ߲ߖߋ߫ ߓߊ߬؟</string>
|
||||
<string name="review_no_category">ߐ߲߬ߤߐ߲߯߹ ߣߌ߲߬ ߝߊ߲߭ ߡߊ߫ ߦߌߟߡߊߦߊ߫ ߟߋ߬߹</string>
|
||||
<string name="review_category_explanation">ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߦߋ߫ %1$s ߦߌߟߡߊ ߟߎ߬ ߞߘߐ߫.</string>
|
||||
<string name="review_c_violation_report_question">ߊ߬ ߦߋ߫ ߓߊߦߟߍߡߊ߲ߠߌ߲ ߤߊߞߍ ߞߎ߬ߙߎ߲߬ߘߎ ߕߌߢߍ ߟߋ߬ ߘߌ߫ ߓߊߏ߬ ߊ߬ ߦߋ߫</string>
|
||||
<string name="review_category_yes_button_text">ߒ߬ߒ߫߸ ߦߌߟߡߦߊߟߌ ߞߐߢߌ߬ߣߊ߬ߣߍ߲߹</string>
|
||||
<string name="review_category_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߢߊ߬ߣߍ߲߫</string>
|
||||
<string name="review_spam_yes_button_text">ߒ߬ߒ߫߸ ߊ߬ ߞߍߣߍ߲߫ ߦߏ߫ ߞߣߍ ߕߴߦߋ߲߬</string>
|
||||
<string name="review_spam_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߢߊ߬ߣߍ߲߫</string>
|
||||
<string name="review_copyright_yes_button_text">ߒ߬ߒ߫߸ ߓߊߦߟߍߡߊ߲ ߤߊߞߍ ߡߊ߫ ߕߌߢߍ߫</string>
|
||||
<string name="review_copyright_no_button_text">ߊ߬ ߞߍߣߍ߲߫ ߦߴߊ߬ ߓߍ߲߬ߣߍ߲߫</string>
|
||||
<string name="review_thanks_yes_button_text">ߐ߲߬ߐ߲ߐ߲߫߸ ߡߎ߲߬ߠߊ߫ ߍ߲߬ߍ߲߫</string>
|
||||
|
|
@ -287,10 +306,16 @@
|
|||
<string name="exif_tag_name_copyright">ߓߊߦߟߍߡߊ߲ ߤߊߞߍ</string>
|
||||
<string name="exif_tag_name_location">ߘߌ߲߬ߞߌߙߊ</string>
|
||||
<string name="exif_tag_name_cameraModel">ߖߌ߬ߦߊ߬ߕߊ߬ߟߊ߲ ߛߎ߮ߦߊ</string>
|
||||
<string name="image_info">ߖߌ߬ߦߊ߬ߓߍ ߞߌ߬ߓߊ߬ߙߏ߬ߦߊ</string>
|
||||
<string name="no_categories_found">ߦߌߟߡߊߙߋ߲߫ ߕߴߦߋ߲߬</string>
|
||||
<string name="no_depiction_found">ߘߊ߲߬ߠߊ߬ߕߍ߰ߟߌ ߡߊ߫ ߛߐ߬ߘߐ߲߬</string>
|
||||
<string name="upload_cancelled">ߟߊ߬ߦߟߍ߬ߟߌ ߘߊߓߌ߬ߟߊ߬</string>
|
||||
<string name="dialog_box_text_nomination">ߡߎ߲߬ߠߊ߫ %1$s ߖߏ߬ߛߌ߬ߕߐ߫؟</string>
|
||||
<string name="review_is_uploaded_by">%1$s ߟߊߦߟߍ߬ߣߍ߲߬ ߦߋ߫: %2$s ߟߋ߬ ߓߟߏ߫</string>
|
||||
<string name="default_description_language">ߞߊ߲߬ߛߓߍߟߌ ߞߍ߫ ߞߊ߲ ߓߊߖߎߡߊ</string>
|
||||
<string name="delete_helper_make_deletion_toast">ߌ ߦߴߌ ߞߊߘߊ߲ ߞߊ߲߬ ߞߊ߬ %1$s ߕߐ߯ߦߊ߫ ߖߏ߬ߛߟߌ߬ ߞߊ߲ߡߊ߬</string>
|
||||
<string name="delete_helper_show_deletion_title">ߕߐ߯ߦߊߟߌ ߖߏ߰ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</string>
|
||||
<string name="delete_helper_show_deletion_title_success">ߊ߬ ߓߘߊ߫ ߛߎߘߊ߲߫</string>
|
||||
<string name="delete_helper_show_deletion_message_if">%1$s ߕߐ߯ߦߊߟߌ ߦߴߌ ߘߐ߫ ߖߏ߬ߛߌ߬ߟߌ ߞߊ߲ߡߊ߬</string>
|
||||
<string name="delete_helper_show_deletion_title_failed">ߊ߬ ߓߘߊ߫ ߗߌߙߏ߲߫</string>
|
||||
<string name="delete_helper_show_deletion_message_else">ߌ ߕߍ߫ ߛߋ߫ ߖߏ߰ߛߌ߬ߟߌ ߡߊߢߌߣߌ߲߫ ߠߊ߫</string>
|
||||
|
|
@ -302,5 +327,14 @@
|
|||
<string name="delete_helper_ask_alert_set_positive_button_reason">ߓߊߏ߬ ߊ߬ ߦߋ߫</string>
|
||||
<string name="share_image_via">ߖߌ߬ߦߊ߬ߓߍ ߟߊߖߍ߲ߛߍ߲߫ ߞߊߕߙߍ߬</string>
|
||||
<string name="no_achievements_yet">ߌ ߡߊ߫ ߓߟߏߓߌߟߊߢߐ߲߯ߞߊ߲ ߛߌ߫ ߞߍ߫ ߡߎߣߎ߲߬</string>
|
||||
<string name="account_created">ߖߊ߬ߕߋ߬ߘߊ ߓߘߊ߫ ߛߌ߲ߘߌ߫߹</string>
|
||||
<string name="some_error">ߝߎ߬ߕߎ߲߬ߕߌ ߘߏ߫ ߕߘߍ߬ ߦߋ߫ ߦߋ߲߬߹</string>
|
||||
<string name="theme_dark_name">ߘߌ߬ߓߌ</string>
|
||||
<string name="load_more">ߘߏߜߘߍ߫ ߟߎ߫ ߟߊߢߎ߲߫</string>
|
||||
<string name="add_picture_to_wikipedia_article_title">ߖߌ߬ߦߊ߬ߓߍ ߝߙߊ߬ ߥߞߌߔߋߘߌߦߊ ߞߊ߲߬</string>
|
||||
<string name="add_picture_to_wikipedia_article_desc">ߌ ߦߴߊ߬ ߝߍ߬ ߞߊ߬ ߖߌ߬ߦߊ߬ߓߍ ߣߌ߲߬ ߓߌ߬ߟߊ߬ %1$ ߞߊ߲ ߥߞߌߔߋߘߌߦߊ ߞߎߡߘߊ ߟߊ߫ ߓߊ߬؟</string>
|
||||
<string name="confirm">ߊ߬ ߟߊߛߙߋߦߊ߫</string>
|
||||
<string name="wikipedia_instructions_step_1">߁߭. ߥߞߌߛߓߍߟߌ ߢߌ߲߬ ߠߎ߬ ߟߊߓߊ߯ߙߊ߫:</string>
|
||||
<string name="wikipedia_instructions_step_2">߂.ߟߊ߬ߛߙߋ߬ߦߊ߬ߟߌ ߛߐ߲߬ߞߌ߲ ߓߍߣߊ߬ ߥߞߌߔߋߘߌߦߊ ߞߎߡߘߊ ߘߊߦߟߍ߬</string>
|
||||
<string name="wikipedia_instructions_step_7">߇߲. ߞߎߡߘߊ ߟߊߥߊ߲߬ߞߊ߫</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -600,4 +600,18 @@
|
|||
<string name="use_location_from_similar_image">Tog du dessa två bilder på samma plats? Vill du använda den högra bildens latitud/longitud?</string>
|
||||
<string name="load_more">Läs in fler</string>
|
||||
<string name="nearby_no_results">Inga platser hittades, försök ändra dina sökkriterier.</string>
|
||||
<string name="add_picture_to_wikipedia_article_title">Lägg till bild på Wikipedia</string>
|
||||
<string name="add_picture_to_wikipedia_article_desc">Vill du lägga till denna bild i Wikipedia-artikeln på %1$s?</string>
|
||||
<string name="add_picture_to_wikipedia_instructions_title">Instruktioner</string>
|
||||
<string name="add_picture_to_wikipedia_instructions_desc">Se till att följa riktlinjerna för hur man redigerar!</string>
|
||||
<string name="confirm">Bekräfta</string>
|
||||
<string name="instructions_title">Instruktioner</string>
|
||||
<string name="wikipedia_instructions_step_1">1. Använd följande wikitext:</string>
|
||||
<string name="wikipedia_instructions_step_2">2. Klicka på \"Bekräfta\" för att öppna Wikipedia-artikeln</string>
|
||||
<string name="wikipedia_instructions_step_3">3. Hitta ett lämplig avsnitt i artikeln för din bild</string>
|
||||
<string name="wikipedia_instructions_step_4">4. Klicka på redigeringsikonen (ser ut som en penna) för detta avsnitt.</string>
|
||||
<string name="wikipedia_instructions_step_5">5. Klistra in wikitexten på det lämpliga stället.</string>
|
||||
<string name="wikipedia_instructions_step_6">6. Redigera wikitexten för att placera den mer lämpligt vid behov. För mer information, se <a href=\"https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image\">här</a>.</string>
|
||||
<string name="wikipedia_instructions_step_7">7. Publicera artikeln</string>
|
||||
<string name="copy_wikicode_to_clipboard">Kopiera wikikod till urklipp</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@
|
|||
<string name="preference_author_name_toggle_summary">Fotoğraf yüklerken kullanıcı adınız yerine özel bir yazar adı kullanın</string>
|
||||
<string name="preference_author_name">Özel yazar adı</string>
|
||||
<string name="contributions_fragment">Katkılar</string>
|
||||
<string name="nearby_fragment">Yakınımdakiler</string>
|
||||
<string name="nearby_fragment">Yakınındakiler</string>
|
||||
<string name="notifications">Bildirimler</string>
|
||||
<string name="read_notifications">Bildirimler (okunmuş)</string>
|
||||
<string name="display_nearby_notification">Yakınımdakiler bildirimi görüntüle</string>
|
||||
|
|
|
|||
|
|
@ -14,10 +14,12 @@
|
|||
<plurals name="starting_multiple_uploads">
|
||||
<item quantity="one">იჭყაფუ %1$d ეხარგუა</item>
|
||||
<item quantity="few">იჭყაფუ %1$d ეხარგუა</item>
|
||||
<item quantity="other">იჭყაფუ %1$d ეხარგუა</item>
|
||||
</plurals>
|
||||
<plurals name="multiple_uploads_title">
|
||||
<item quantity="one">%1$d ეხარგუა</item>
|
||||
<item quantity="few">%1$d ეხარგუა</item>
|
||||
<item quantity="other">%1$d ეხარგუა</item>
|
||||
</plurals>
|
||||
<plurals name="share_license_summary">
|
||||
<item quantity="one">თე სურათი გიბჟინუ %1$s ლიცენზიათ</item>
|
||||
|
|
|
|||
|
|
@ -28,9 +28,11 @@
|
|||
</plurals>
|
||||
<plurals name="starting_multiple_uploads">
|
||||
<item quantity="one">開始 %1$d 次上傳</item>
|
||||
<item quantity="other">開始 %1$d 次上傳</item>
|
||||
</plurals>
|
||||
<plurals name="multiple_uploads_title">
|
||||
<item quantity="one">%1$d 次上傳</item>
|
||||
<item quantity="other">%1$d 次上傳</item>
|
||||
</plurals>
|
||||
<plurals name="share_license_summary">
|
||||
<item quantity="one">此圖片會按 %1$s 協議授權上載</item>
|
||||
|
|
|
|||
|
|
@ -99,9 +99,10 @@
|
|||
<string name="provider_contributions">我的上传</string>
|
||||
<string name="menu_share">分享</string>
|
||||
<string name="menu_open_in_browser">在浏览器中查看</string>
|
||||
<string name="share_title_hint" fuzzy="true">标题 (要求)</string>
|
||||
<string name="share_title_hint">说明 (要求)</string>
|
||||
<string name="add_caption_toast">请提供此文件的描述</string>
|
||||
<string name="share_description_hint">描述</string>
|
||||
<string name="share_caption_hint">说明(255个字符以内)</string>
|
||||
<string name="login_failed_network">无法登录 - 网络故障</string>
|
||||
<string name="login_failed_wrong_credentials">无法登录——请检查您的用户名和密码</string>
|
||||
<string name="login_failed_throttled">失败次数过多。请在几分钟后重试。</string>
|
||||
|
|
@ -206,8 +207,10 @@
|
|||
<string name="no_nearby">找不到附近地点</string>
|
||||
<string name="warning">警告</string>
|
||||
<string name="upload_image_duplicate">此文件已在共享资源下存在。您确定要继续吗?</string>
|
||||
<string name="upload">上传</string>
|
||||
<string name="yes">是</string>
|
||||
<string name="no">否</string>
|
||||
<string name="media_detail_caption">说明</string>
|
||||
<string name="media_detail_title">标题</string>
|
||||
<string name="media_detail_description">描述</string>
|
||||
<string name="media_detail_discussion">讨论</string>
|
||||
|
|
@ -315,8 +318,8 @@
|
|||
<string name="nearby_wikidata">维基数据</string>
|
||||
<string name="nearby_wikipedia">维基百科</string>
|
||||
<string name="nearby_commons">共享资源</string>
|
||||
<string name="about_rate_us" fuzzy="true"><u>评价我们</u></string>
|
||||
<string name="about_faq" fuzzy="true"><u>常见问题</u></string>
|
||||
<string name="about_rate_us">评价我们</string>
|
||||
<string name="about_faq">常见问题</string>
|
||||
<string name="welcome_skip_button">跳过指导</string>
|
||||
<string name="no_internet">互联网不可用</string>
|
||||
<string name="internet_established">互联网可用</string>
|
||||
|
|
@ -324,7 +327,7 @@
|
|||
<string name="error_review">获取审查图片错误。按刷新键重试。</string>
|
||||
<string name="error_review_categories">获取审查图片类别错误。按刷新按键重试。</string>
|
||||
<string name="no_notifications">找不到通知</string>
|
||||
<string name="about_translate" fuzzy="true"><u>翻译</u></string>
|
||||
<string name="about_translate">翻译</string>
|
||||
<string name="about_translate_title">语言</string>
|
||||
<string name="about_translate_message">选择您希望提交翻译的语言</string>
|
||||
<string name="about_translate_proceed">已处理</string>
|
||||
|
|
@ -351,6 +354,7 @@
|
|||
<string name="error_loading_subcategories">加载子分类时发生错误。</string>
|
||||
<string name="search_tab_title_media">媒体</string>
|
||||
<string name="search_tab_title_categories">分类</string>
|
||||
<string name="search_tab_title_depictions">项目</string>
|
||||
<string name="explore_tab_title_featured">特色</string>
|
||||
<string name="explore_tab_title_mobile">通过移动端上传</string>
|
||||
<string name="successful_wikidata_edit">图片已添加到维基数据上的%1$s!</string>
|
||||
|
|
@ -575,6 +579,9 @@
|
|||
<string name="place_type">地点类型:</string>
|
||||
<string name="nearby_search_hint">桥梁、博物馆、旅馆等</string>
|
||||
<string name="you_must_reset_your_passsword">登录时出现一些问题,您必须重新设置您的密码!</string>
|
||||
<string name="title_app_shortcut_explore">探索</string>
|
||||
<string name="title_app_shortcut_bookmark">书签</string>
|
||||
<string name="title_app_shortcut_setting">设置</string>
|
||||
<string name="wallpaper_set_unsuccessfully">出错了。无法设置壁纸</string>
|
||||
<string name="setting_wallpaper_dialog_title">设为壁纸</string>
|
||||
<string name="setting_wallpaper_dialog_message">正在设置壁纸。请稍等…</string>
|
||||
|
|
@ -582,4 +589,5 @@
|
|||
<string name="add_picture_to_wikipedia_instructions_title">说明</string>
|
||||
<string name="confirm">确认</string>
|
||||
<string name="instructions_title">说明</string>
|
||||
<string name="copy_wikicode_to_clipboard">复制维基代码到剪贴板</string>
|
||||
</resources>
|
||||
|
|
|
|||
|
|
@ -331,6 +331,9 @@
|
|||
<string name="retry">Retry</string>
|
||||
<string name="showcase_view_got_it_button">Got it!</string>
|
||||
<string name="showcase_view_whole_nearby_activity">These are the places near you that need pictures to illustrate their Wikipedia articles.\n\nClicking on \'SEARCH THIS AREA\' locks the map and launches a nearby search around that location.</string>
|
||||
<string name="showcase_view_needs_photo">This place needs a photo.</string>
|
||||
<string name="showcase_view_has_photo">This place already has a photo.</string>
|
||||
<string name="showcase_view_no_longer_exists">This place no longer exists.</string>
|
||||
|
||||
<string name="showcase_view_list_icon">Tapping this button brings up a list of these places</string>
|
||||
<string name="showcase_view_plus_fab">You can upload a picture for any place from your gallery or camera</string>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package fr.free.nrw.commons.wikidata
|
||||
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse
|
||||
import com.google.gson.Gson
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import fr.free.nrw.commons.wikidata.model.PageInfo
|
||||
import fr.free.nrw.commons.wikidata.model.WbCreateClaimResponse
|
||||
import io.reactivex.Observable
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
|
@ -14,12 +16,16 @@ import org.mockito.Mockito.mock
|
|||
import org.mockito.MockitoAnnotations
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResult
|
||||
import org.wikipedia.wikidata.Statement_partial
|
||||
|
||||
class WikidataClientTest {
|
||||
|
||||
@Mock
|
||||
internal var wikidataInterface: WikidataInterface? = null
|
||||
|
||||
@Mock
|
||||
internal var gson: Gson? = null
|
||||
|
||||
@InjectMocks
|
||||
var wikidataClient: WikidataClient? = null
|
||||
|
||||
|
|
@ -35,26 +41,18 @@ class WikidataClientTest {
|
|||
.thenReturn(Observable.just(mwQueryResponse))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun createClaim() {
|
||||
`when`(
|
||||
wikidataInterface!!.postCreateClaim(
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any(),
|
||||
any()
|
||||
)
|
||||
)
|
||||
.thenReturn(Observable.just(mock()))
|
||||
wikidataClient!!.createImageClaim(mock(), "test.jpg")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun addEditTag() {
|
||||
`when`(wikidataInterface!!.addEditTag(anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(Observable.just(mock(AddEditTagResponse::class.java)))
|
||||
wikidataClient!!.addEditTag(1L, "test", "test")
|
||||
val response = mock(WbCreateClaimResponse::class.java)
|
||||
val pageInfo = mock(PageInfo::class.java)
|
||||
whenever(pageInfo.lastrevid).thenReturn(1)
|
||||
whenever(response.pageinfo).thenReturn(pageInfo)
|
||||
`when`(wikidataInterface!!.postSetClaim(anyString(), anyString(), anyString()))
|
||||
.thenReturn(Observable.just(response))
|
||||
whenever(gson!!.toJson(any(Statement_partial::class.java))).thenReturn("claim")
|
||||
val request = mock(Statement_partial::class.java)
|
||||
|
||||
val claim = wikidataClient!!.setClaim(request, "test").test()
|
||||
.assertValue(1L)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
package fr.free.nrw.commons.wikidata
|
||||
|
||||
import android.content.Context
|
||||
import com.google.gson.Gson
|
||||
import com.nhaarman.mockitokotlin2.mock
|
||||
import com.nhaarman.mockitokotlin2.verifyZeroInteractions
|
||||
import com.nhaarman.mockitokotlin2.whenever
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore
|
||||
import fr.free.nrw.commons.upload.UploadResult
|
||||
import fr.free.nrw.commons.upload.WikidataPlace
|
||||
import fr.free.nrw.commons.wikidata.model.AddEditTagResponse
|
||||
import io.reactivex.Observable
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
|
|
@ -15,7 +15,6 @@ import org.mockito.ArgumentMatchers.any
|
|||
import org.mockito.ArgumentMatchers.anyString
|
||||
import org.mockito.InjectMocks
|
||||
import org.mockito.Mock
|
||||
import org.mockito.Mockito.*
|
||||
import org.mockito.MockitoAnnotations
|
||||
|
||||
class WikidataEditServiceTest {
|
||||
|
|
@ -31,6 +30,9 @@ class WikidataEditServiceTest {
|
|||
@Mock
|
||||
internal lateinit var wikibaseClient: WikiBaseClient
|
||||
|
||||
@Mock
|
||||
internal lateinit var gson: Gson
|
||||
|
||||
@InjectMocks
|
||||
lateinit var wikidataEditService: WikidataEditService
|
||||
|
||||
|
|
@ -44,7 +46,7 @@ class WikidataEditServiceTest {
|
|||
fun noClaimsWhenLocationIsNotCorrect() {
|
||||
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
|
||||
.thenReturn(false)
|
||||
wikidataEditService.createImageClaim(mock(), mock())
|
||||
wikidataEditService.createClaim(mock(), "Test.jpg", hashMapOf())
|
||||
verifyZeroInteractions(wikidataClient)
|
||||
}
|
||||
|
||||
|
|
@ -52,15 +54,16 @@ class WikidataEditServiceTest {
|
|||
fun createImageClaim() {
|
||||
whenever(directKvStore.getBoolean("Picture_Has_Correct_Location", true))
|
||||
.thenReturn(true)
|
||||
whenever(wikidataClient.createImageClaim(any(), any()))
|
||||
.thenReturn(Observable.just(1L))
|
||||
whenever(wikidataClient.addEditTag(anyLong(), anyString(), anyString()))
|
||||
.thenReturn(Observable.just(mock(AddEditTagResponse::class.java)))
|
||||
whenever(wikibaseClient.getFileEntityId(any())).thenReturn(Observable.just(1L))
|
||||
val wikidataPlace:WikidataPlace = mock()
|
||||
whenever(wikidataClient.setClaim(any(), anyString()))
|
||||
.thenReturn(Observable.just(1L))
|
||||
val wikidataPlace: WikidataPlace = mock()
|
||||
val uploadResult = mock<UploadResult>()
|
||||
whenever(uploadResult.filename).thenReturn("file")
|
||||
wikidataEditService.createImageClaim(wikidataPlace, uploadResult)
|
||||
verify(wikidataClient, times(1)).createImageClaim(wikidataPlace, """"file"""")
|
||||
wikidataEditService.createClaim(
|
||||
wikidataPlace,
|
||||
uploadResult.filename,
|
||||
hashMapOf<String, String>()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ sealed class DataValue(val type: String) {
|
|||
.registerSubtype(GlobeCoordinate_partial::class.java, GlobeCoordinate_partial.TYPE)
|
||||
.registerSubtype(Time_partial::class.java, Time_partial.TYPE)
|
||||
.registerSubtype(Quantity_partial::class.java, Quantity_partial.TYPE)
|
||||
.registerSubtype(MonoLingualText_partial::class.java, MonoLingualText_partial.TYPE)
|
||||
.registerSubtype(MonoLingualText::class.java, MonoLingualText.TYPE)
|
||||
}
|
||||
|
||||
// "value": {
|
||||
|
|
@ -87,7 +87,7 @@ sealed class DataValue(val type: String) {
|
|||
// "language": "ko"
|
||||
// }
|
||||
// }
|
||||
class MonoLingualText_partial() : DataValue(TYPE) {
|
||||
class MonoLingualText(val value: WikiBaseMonolingualTextValue) : DataValue(TYPE) {
|
||||
companion object {
|
||||
const val TYPE = "monolingualtext"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,5 +21,8 @@ import com.google.gson.annotations.SerializedName
|
|||
data class Statement_partial(
|
||||
@SerializedName("mainsnak") val mainSnak: Snak_partial,
|
||||
val type: String,
|
||||
val rank: String
|
||||
val rank: String,
|
||||
val id: String? = null,
|
||||
val qualifiers: Map<String, List<Snak_partial>> = mapOf(),
|
||||
@SerializedName("qualifiers-order") val qualifiersOrder: List<String> = listOf()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
package org.wikipedia.wikidata
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
/*"value": {
|
||||
"type": "monolingualtext",
|
||||
"value": {
|
||||
"text": "some value",
|
||||
"language": "en"
|
||||
}
|
||||
}*/
|
||||
|
||||
data class WikiBaseMonolingualTextValue(val text: String, val language: String)
|
||||
Loading…
Add table
Add a link
Reference in a new issue