diff --git a/.travis.yml b/.travis.yml index 20c5bfaee..5e76e09d9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,12 +19,13 @@ android: components: - tools - platform-tools - - build-tools-26.0.2 + - build-tools-27.0.0 - extra-google-m2repository - extra-android-m2repository - ${ANDROID_TARGET} - android-25 - android-26 + - android-27 - sys-img-${ANDROID_ABI}-${ANDROID_TARGET} licenses: - 'android-sdk-license-.+' diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c77174cf..1688b3b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Wikimedia Commons for Android +## v2.7.2 +- Modified subtext for "automatically get current location" setting to emphasize that it will reveal user's location + ## v2.7.1 - Fixed UI and permission issues with Nearby - Fixed issue with My Recent Uploads being empty diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index caa02a103..37ecfca1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -32,3 +32,7 @@ The body should provide a meaningful commit message. 1. Write tests for your code (if possible) 1. Make sure the Wiki pages don't become stale by updating them (if needed) + +### Further reading + +* [Importance of good commit messages](https://blog.oozou.com/commit-messages-matter-60309983c227?gi=c550a10d0f67) diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md index 691562b04..8feca4268 100644 --- a/ISSUE_TEMPLATE.md +++ b/ISSUE_TEMPLATE.md @@ -1,25 +1,18 @@ -_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._ - **Summary:** Summarize your issue in one sentence (what goes wrong, what did you expect to happen) +_Before creating an issue, please search the existing issues to see if a similar one has already been created. You can search issues by specific labels (e.g. `label:nearby `) or just by typing keywords into the search filter._ + **Steps to reproduce:** -How can we reproduce the issue? +How can we reproduce the issue? +What did you expect the app to do, and what did you see instead? **Add System logs:** Add logcat files here (if possible). -**Expected behavior:** - -What did you expect the App to do? - -**Observed behavior:** - -What did you see instead? Describe your issue in detail here. - **Device and Android version:** What make and model device (e.g., Samsung J7) did you encounter this on? What Android @@ -28,7 +21,7 @@ version (e.g., Android 4.0 Ice Cream Sandwich or Android 6.0 Marshmallow) are yo **Commons app version:** -You can find this information by going to the navigation drawer in the app and tapping 'About' +You can find this information by going to the navigation drawer in the app and tapping 'About'. If you are building from our codebase instead of downloading the app, please also mention the branch and build variant (e.g. master and prodDebug). **Screen-shots:** diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 9d7150008..37e104d14 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -1,3 +1,7 @@ +## Title (required) + +Fixes #{GitHub issue number and title (Please do not forget adding title) } + ## Description (required) Fixes #{GitHub issue number and title} @@ -12,4 +16,4 @@ Tested on {API level & name of device/emulator}, with {build variant, e.g. ProdD {Only for user interface changes, otherwise remove this section. See [how to take a screenshot](https://android.stackexchange.com/questions/1759/how-to-take-a-screenshot-with-an-android-device)} -_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._ +_Note: Please ensure that you have read CONTRIBUTING.md if this is your first pull request._ \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 5d37f8f54..ea06c5c43 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,11 +7,12 @@ apply from: 'quality.gradle' apply plugin: 'com.getkeepsafe.dexcount' dependencies { + implementation 'com.squareup.picasso:picasso:2.71828' + implementation 'com.prof.rssparser:rssparser:1.1' implementation 'com.github.nicolas-raoul:Quadtree:ac16ea8035bf07' implementation 'fr.avianey.com.viewpagerindicator:library:2.4.1.1@aar' implementation 'in.yuvi:http.fluent:1.3' implementation 'com.github.chrisbanes:PhotoView:2.0.0' - implementation 'com.android.volley:volley:1.0.0' implementation 'ch.acra:acra:4.9.2' implementation 'org.mediawiki:api:1.3' implementation 'commons-codec:commons-codec:1.10' @@ -20,59 +21,57 @@ dependencies { implementation 'com.jakewharton.timber:timber:4.5.1' implementation 'info.debatty:java-string-similarity:0.24' implementation 'com.borjabravo:readmoretextview:2.1.0' - implementation 'com.android.support.constraint:constraint-layout:1.0.2' - implementation ('com.mapbox.mapboxsdk:mapbox-android-sdk:5.4.1@aar'){ - transitive=true + + implementation 'com.android.support.constraint:constraint-layout:1.1.0' + implementation('com.mapbox.mapboxsdk:mapbox-android-sdk:5.5.0@aar') { + transitive = true } - - implementation "com.github.deano2390:MaterialShowcaseView:1.2.0" - + implementation 'com.github.deano2390:MaterialShowcaseView:1.2.0' + //noinspection GradleCompatible implementation "com.android.support:support-v4:$SUPPORT_LIB_VERSION" implementation "com.android.support:appcompat-v7:$SUPPORT_LIB_VERSION" implementation "com.android.support:design:$SUPPORT_LIB_VERSION" implementation "com.android.support:customtabs:$SUPPORT_LIB_VERSION" - implementation "com.android.support:cardview-v7:$SUPPORT_LIB_VERSION" - implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION" kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION" - implementation 'com.squareup.okhttp3:okhttp:3.9.1' implementation 'com.squareup.okio:okio:1.13.0' - implementation 'io.reactivex.rxjava2:rxandroid:2.0.1' // Because RxAndroid releases are few and far between, it is recommended you also // explicitly depend on RxJava's latest version for bug fixes and new features. implementation 'com.android.support:multidex:1.0.3' - implementation 'io.reactivex.rxjava2:rxjava:2.1.2' implementation 'com.jakewharton.rxbinding2:rxbinding:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-support-v4:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-appcompat-v7:2.0.0' implementation 'com.jakewharton.rxbinding2:rxbinding-design:2.0.0' - implementation 'org.jsoup:jsoup:1.11.3' - implementation 'com.facebook.fresco:fresco:1.5.0' implementation 'com.facebook.stetho:stetho:1.5.0' - implementation "com.google.dagger:dagger:$DAGGER_VERSION" implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION" kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION" kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" - - testImplementation "org.robolectric:multidex:3.4.2" + testImplementation 'org.robolectric:multidex:3.4.2' testImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" testImplementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" testImplementation 'junit:junit:4.12' testImplementation 'org.robolectric:robolectric:3.7.1' testImplementation 'com.nhaarman:mockito-kotlin:1.5.0' testImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' + implementation 'com.dinuscxj:circleprogressbar:1.1.1' + + implementation 'com.caverock:androidsvg:1.2.1' + implementation 'com.github.bumptech.glide:glide:4.7.1' + kapt 'com.github.bumptech.glide:compiler:4.7.1' androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.8.1' androidTestImplementation "com.android.support:support-annotations:$SUPPORT_LIB_VERSION" - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2-alpha1' + androidTestImplementation 'com.android.support.test:rules:1.0.2' + androidTestImplementation 'com.android.support.test:runner:1.0.2' + androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' debugImplementation "com.squareup.leakcanary:leakcanary-android:$LEAK_CANARY" releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:$LEAK_CANARY" @@ -87,8 +86,8 @@ android { defaultConfig { applicationId 'fr.free.nrw.commons' - versionCode 84 - versionName '2.7.1' + versionCode 85 + versionName '2.7.2' setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName()) minSdkVersion project.minSdkVersion @@ -117,7 +116,7 @@ android { buildTypes { release { minifyEnabled false // See https://stackoverflow.com/questions/40232404/google-play-apk-and-android-studio-apk-usb-debug-behaving-differently - proguard.cfg modification alone insufficient. - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt', 'proguard-glide.txt' } debug { applicationIdSuffix ".debug" @@ -129,7 +128,9 @@ android { flavorDimensions 'tier' productFlavors { prod { + buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\"" + buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.org/wiki/\"" @@ -145,7 +146,9 @@ android { beta { // What values do we need to hit the BETA versions of the site / api ? + buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\"" buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\"" + buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\"" buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\"" buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\"" buildConfigField "String", "HOME_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/\"" diff --git a/app/libs/java-json.jar b/app/libs/java-json.jar new file mode 100644 index 000000000..2f211e366 Binary files /dev/null and b/app/libs/java-json.jar differ diff --git a/app/proguard-glide.txt b/app/proguard-glide.txt new file mode 100644 index 000000000..ef3437660 --- /dev/null +++ b/app/proguard-glide.txt @@ -0,0 +1,9 @@ +-keep public class * implements com.bumptech.glide.module.GlideModule +-keep public class * extends com.bumptech.glide.module.AppGlideModule +-keep public enum com.bumptech.glide.load.ImageHeaderParser$** { + **[] $VALUES; + public *; +} + +# for DexGuard only +-keepresourcexmlelements manifest/application/meta-data@value=GlideModule \ No newline at end of file diff --git a/app/proguard-rules.txt b/app/proguard-rules.txt index bbf3a3f0d..39b618718 100644 --- a/app/proguard-rules.txt +++ b/app/proguard-rules.txt @@ -1,5 +1,4 @@ -dontobfuscate -keep class org.apache.http.** { *; } -dontwarn org.apache.http.** --keep class fr.free.nrw.commons.upload.MwVolleyApi$Page {*;} -keep class android.support.v7.widget.ShareActionProvider { *; } \ No newline at end of file diff --git a/app/quality.gradle b/app/quality.gradle index 7ea20916a..1afdf0d68 100644 --- a/app/quality.gradle +++ b/app/quality.gradle @@ -18,7 +18,7 @@ task checkstyle(type: Checkstyle) { reports { html { enabled true - destination "${project.buildDir}/reports/checkstyle/checkstyle.html" + destination file("${project.buildDir}/reports/checkstyle/checkstyle.html") } } } @@ -36,10 +36,10 @@ task pmd(type: Pmd) { xml.enabled = false html.enabled = true xml { - destination "${project.buildDir}/reports/pmd/pmd.xml" + destination file("${project.buildDir}/reports/pmd/pmd.xml") } html { - destination "${project.buildDir}/reports/pmd/pmd.html" + destination file("${project.buildDir}/reports/pmd/pmd.html") } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 17f6770d2..3665042ac 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,3 +1,4 @@ + @@ -15,6 +16,7 @@ + @@ -26,10 +28,10 @@ android:theme="@style/LightAppTheme" android:supportsRtl="true" > + android:theme="@android:style/Theme.Dialog" + android:launchMode="singleInstance" + android:excludeFromRecents="true" + android:finishOnTaskLaunch="true" /> @@ -44,7 +46,7 @@ android:name=".upload.ShareActivity" android:icon="@drawable/ic_launcher" android:label="@string/app_name"> - + @@ -56,7 +58,7 @@ android:name=".upload.MultipleShareActivity" android:icon="@drawable/ic_launcher" android:label="@string/app_name"> - + @@ -96,8 +98,22 @@ android:label="@string/title_activity_featured_images" android:parentActivityName=".contributions.ContributionsActivity" /> - + + + + + + + + + + + + + + + + - + \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java index 65cba7531..c8941dcd8 100644 --- a/app/src/main/java/fr/free/nrw/commons/AboutActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/AboutActivity.java @@ -9,9 +9,6 @@ import android.os.Bundle; import android.text.Html; import android.text.SpannableString; import android.text.style.UnderlineSpan; -import android.util.Log; -import android.support.customtabs.CustomTabsIntent; -import android.support.v4.content.ContextCompat; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; @@ -20,7 +17,6 @@ import android.widget.ArrayAdapter; import android.widget.LinearLayout; import android.widget.Spinner; import android.widget.TextView; -import android.widget.Toast; import butterknife.BindView; import butterknife.ButterKnife; @@ -28,8 +24,6 @@ import butterknife.OnClick; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.ui.widget.HtmlTextView; -import static android.widget.Toast.LENGTH_SHORT; - /** * Represents about screen of this app */ diff --git a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java index 1b5a659c3..d29c22e76 100644 --- a/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java +++ b/app/src/main/java/fr/free/nrw/commons/CommonsApplication.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons; -import android.app.Application; import android.content.Context; import android.content.SharedPreferences; import android.database.sqlite.SQLiteDatabase; @@ -27,7 +26,7 @@ import fr.free.nrw.commons.contributions.ContributionDao; import fr.free.nrw.commons.data.DBOpenHelper; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.modifications.ModifierSequenceDao; -import fr.free.nrw.commons.utils.FileUtils; +import fr.free.nrw.commons.upload.FileUtils; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.schedulers.Schedulers; import timber.log.Timber; diff --git a/app/src/main/java/fr/free/nrw/commons/Media.java b/app/src/main/java/fr/free/nrw/commons/Media.java index 5f6a498ea..04f097d08 100644 --- a/app/src/main/java/fr/free/nrw/commons/Media.java +++ b/app/src/main/java/fr/free/nrw/commons/Media.java @@ -3,13 +3,17 @@ package fr.free.nrw.commons; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.HashMap; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -356,12 +360,8 @@ public class Media implements Parcelable { * @param descriptions Media descriptions */ void setDescriptions(Map descriptions) { - for (String key : this.descriptions.keySet()) { - this.descriptions.remove(key); - } - for (String key : descriptions.keySet()) { - this.descriptions.put(key, descriptions.get(key)); - } + this.descriptions.clear(); + this.descriptions.putAll(descriptions); } /** diff --git a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java index 35d197782..5e89e19a5 100644 --- a/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java +++ b/app/src/main/java/fr/free/nrw/commons/MediaWikiImageView.java @@ -51,10 +51,16 @@ public class MediaWikiImageView extends SimpleDraweeView { return; } - if (thumbnailUrlCache.get(media.getFilename()) != null) { - setImageUrl(thumbnailUrlCache.get(media.getFilename())); - } else { - setImageUrl(null); + if(media.getFilename() != null) { + if (thumbnailUrlCache.get(media.getFilename()) != null) { + setImageUrl(thumbnailUrlCache.get(media.getFilename())); + } else { + setImageUrl(null); + currentThumbnailTask = new ThumbnailFetchTask(media, mwApi); + currentThumbnailTask.execute(media.getFilename()); + } + } else { // local image + setImageUrl(media.getLocalUri().toString()); currentThumbnailTask = new ThumbnailFetchTask(media, mwApi); currentThumbnailTask.execute(media.getFilename()); } diff --git a/app/src/main/java/fr/free/nrw/commons/Utils.java b/app/src/main/java/fr/free/nrw/commons/Utils.java index 91c23ce26..4e4b46b01 100644 --- a/app/src/main/java/fr/free/nrw/commons/Utils.java +++ b/app/src/main/java/fr/free/nrw/commons/Utils.java @@ -2,11 +2,13 @@ package fr.free.nrw.commons; import android.content.Context; import android.content.Intent; +import android.graphics.Bitmap; import android.net.Uri; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.customtabs.CustomTabsIntent; import android.support.v4.content.ContextCompat; +import android.view.View; import android.widget.Toast; import org.apache.commons.codec.binary.Hex; @@ -76,7 +78,11 @@ public class Utils { * @return string with capitalized first character */ public static String capitalize(String string) { - return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1); + if(string.length() > 0) { + return string.substring(0, 1).toUpperCase(Locale.getDefault()) + string.substring(1); + } else { + return string; + } } /** @@ -146,7 +152,7 @@ public class Utils { StringBuilder stringBuilder = new StringBuilder(); try { - String[] command = new String[] {"logcat","-d","-v","threadtime"}; + String[] command = new String[]{"logcat","-d","-v","threadtime"}; Process process = Runtime.getRuntime().exec(command); @@ -178,6 +184,7 @@ public class Utils { } public static void handleWebUrl(Context context, Uri url) { + Timber.d("Launching web url %s", url.toString()); Intent browserIntent = new Intent(Intent.ACTION_VIEW, url); if (browserIntent.resolveActivity(context.getPackageManager()) == null) { Toast toast = Toast.makeText(context, context.getString(R.string.no_web_browser), LENGTH_SHORT); @@ -194,4 +201,18 @@ public class Utils { customTabsIntent.launchUrl(context, url); } + /** + * To take screenshot of the screen and return it in Bitmap format + * + * @param view + * @return + */ + public static Bitmap getScreenShot(View view) { + View screenView = view.getRootView(); + screenView.setDrawingCacheEnabled(true); + Bitmap bitmap = Bitmap.createBitmap(screenView.getDrawingCache()); + screenView.setDrawingCacheEnabled(false); + return bitmap; + } + } diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java b/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java new file mode 100644 index 000000000..5fac8da9a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/Achievements.java @@ -0,0 +1,205 @@ +package fr.free.nrw.commons.achievements; + +import android.util.Log; + +/** + * represnts Achievements class ans stores all the parameters + */ +public class Achievements { + private int uniqueUsedImages; + private int articlesUsingImages; + private int thanksReceived; + private int imagesEditedBySomeoneElse; + private int featuredImages; + private int imagesUploaded; + private int revertCount; + + public Achievements(){ + + } + + /** + * constructor for achievements class to set its data members + * @param uniqueUsedImages + * @param articlesUsingImages + * @param thanksReceived + * @param imagesEditedBySomeoneElse + * @param featuredImages + * @param imagesUploaded + * @param revertCount + */ + public Achievements(int uniqueUsedImages, + int articlesUsingImages, + int thanksReceived, + int imagesEditedBySomeoneElse, + int featuredImages, + int imagesUploaded, + int revertCount) { + this.uniqueUsedImages = uniqueUsedImages; + this.articlesUsingImages = articlesUsingImages; + this.thanksReceived = thanksReceived; + this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse; + this.featuredImages = featuredImages; + this.imagesUploaded = imagesUploaded; + this.revertCount = revertCount; + } + + /** + * Builder class for Achievements class + */ + public class AchievementsBuilder { + private int nestedUniqueUsedImages; + private int nestedArticlesUsingImages; + private int nestedThanksReceived; + private int nestedImagesEditedBySomeoneElse; + private int nestedFeaturedImages; + private int nestedImagesUploaded; + private int nestedRevertCount; + + public AchievementsBuilder setUniqueUsedImages(int uniqueUsedImages) { + this.nestedUniqueUsedImages = uniqueUsedImages; + return this; + } + + public AchievementsBuilder setArticlesUsingImages(int articlesUsingImages) { + this.nestedArticlesUsingImages = articlesUsingImages; + return this; + } + + public AchievementsBuilder setThanksReceived(int thanksReceived) { + this.nestedThanksReceived = thanksReceived; + return this; + } + + public AchievementsBuilder setImagesEditedBySomeoneElse(int imagesEditedBySomeoneElse) { + this.nestedImagesEditedBySomeoneElse = imagesEditedBySomeoneElse; + return this; + } + + public AchievementsBuilder setFeaturedImages(int featuredImages) { + this.nestedFeaturedImages = featuredImages; + return this; + } + + public AchievementsBuilder setImagesUploaded(int imagesUploaded) { + this.nestedImagesUploaded = imagesUploaded; + return this; + } + + public AchievementsBuilder setRevertCount( int revertCount){ + this.nestedRevertCount = revertCount; + return this; + } + + public Achievements createAchievements(){ + return new Achievements(nestedUniqueUsedImages, + nestedArticlesUsingImages, + nestedThanksReceived, + nestedImagesEditedBySomeoneElse, + nestedFeaturedImages, + nestedImagesUploaded, + nestedRevertCount); + } + + } + + /** + * getter function to get count of images uploaded + * @return + */ + public int getImagesUploaded() { + return imagesUploaded; + } + + /** + * getter function to get count of featured images + * @return + */ + public int getFeaturedImages() { + return featuredImages; + } + + /** + * getter function to get count of thanks received + * @return + */ + public int getThanksReceived() { + return thanksReceived; + } + + /** + * getter function to get count of unique images used by wiki + * @return + */ + public int getUniqueUsedImages() { + return uniqueUsedImages; + } + + /** + * setter function to count of images uploaded + * @param imagesUploaded + */ + public void setImagesUploaded(int imagesUploaded) { + this.imagesUploaded = imagesUploaded; + } + + /** + * setter function to set count of featured images + * @param featuredImages + */ + public void setFeaturedImages(int featuredImages) { + this.featuredImages = featuredImages; + } + + /** + * setter function to set the count of images edited by someone + * @param imagesEditedBySomeoneElse + */ + public void setImagesEditedBySomeoneElse(int imagesEditedBySomeoneElse) { + this.imagesEditedBySomeoneElse = imagesEditedBySomeoneElse; + } + + /** + * setter function to set count of thanks received + * @param thanksReceived + */ + public void setThanksReceived(int thanksReceived) { + this.thanksReceived = thanksReceived; + } + + /** + * setter function to count of articles using images uploaded + * @param articlesUsingImages + */ + public void setArticlesUsingImages(int articlesUsingImages) { + this.articlesUsingImages = articlesUsingImages; + } + + /** + * setter function to set count of uniques images used by wiki + * @param uniqueUsedImages + */ + public void setUniqueUsedImages(int uniqueUsedImages) { + this.uniqueUsedImages = uniqueUsedImages; + } + + /** + * to set count of images reverted + * @param revertCount + */ + public void setRevertCount(int revertCount) { + this.revertCount = revertCount; + } + + /** + * used to calculate the percentages of images that haven't been reverted + * @return + */ + public int getNotRevertPercentage(){ + try { + return ((imagesUploaded - revertCount) * 100)/imagesUploaded; + } catch (ArithmeticException divideByZero ){ + return 100; + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java new file mode 100644 index 000000000..905d0dfbd --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/AchievementsActivity.java @@ -0,0 +1,458 @@ +package fr.free.nrw.commons.achievements; + +import android.accounts.Account; +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.os.Bundle; +import android.support.v4.content.res.ResourcesCompat; +import android.support.v7.app.AlertDialog; +import android.support.v7.widget.Toolbar; +import android.util.DisplayMetrics; +import android.util.Log; +import android.view.ContextThemeWrapper; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ProgressBar; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import com.dinuscxj.progressbar.CircleProgressBar; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Optional; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.auth.SessionManager; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +/** + * activity for sharing feedback on uploaded activity + */ +public class AchievementsActivity extends NavigationBaseActivity { + + private static final double BADGE_IMAGE_WIDTH_RATIO = 0.4; + private static final double BADGE_IMAGE_HEIGHT_RATIO = 0.3; + private Boolean isUploadFetched = false; + private Boolean isStatisticsFetched = false; + private Boolean isRevertFetched = false; + private Achievements achievements = new Achievements(); + private LevelController.LevelInfo levelInfo; + + @BindView(R.id.achievement_badge) + ImageView imageView; + @BindView(R.id.achievement_level) + TextView levelNumber; + @BindView(R.id.toolbar) + Toolbar toolbar; + @BindView(R.id.thanks_received) + TextView thanksReceived; + @BindView(R.id.images_uploaded_progressbar) + CircleProgressBar imagesUploadedProgressbar; + @BindView(R.id.images_used_by_wiki_progressbar) + CircleProgressBar imagesUsedByWikiProgessbar; + @BindView(R.id.image_reverts_progressbar) + CircleProgressBar imageRevertsProgressbar; + @BindView(R.id.image_featured) + TextView imagesFeatured; + @BindView(R.id.images_revert_limit_text) + TextView imagesRevertLimitText; + @BindView(R.id.progressBar) + ProgressBar progressBar; + @BindView(R.id.layout_image_uploaded) + RelativeLayout layoutImageUploaded; + @BindView(R.id.layout_image_reverts) + RelativeLayout layoutImageReverts; + @BindView(R.id.layout_image_used_by_wiki) + RelativeLayout layoutImageUsedByWiki; + @BindView(R.id.layout_statistics) + LinearLayout layoutStatistics; + @Inject + SessionManager sessionManager; + @Inject + MediaWikiApi mediaWikiApi; + + private CompositeDisposable compositeDisposable = new CompositeDisposable(); + + /** + * This method helps in the creation Achievement screen and + * dynamically set the size of imageView + * + * @param savedInstanceState Data bundle + */ + @Override + @SuppressLint("StringFormatInvalid") + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_achievements); + ButterKnife.bind(this); + /** + * DisplayMetrics used to fetch the size of the screen + */ + DisplayMetrics displayMetrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); + int height = displayMetrics.heightPixels; + int width = displayMetrics.widthPixels; + + /** + * Used for the setting the size of imageView at runtime + */ + RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) + imageView.getLayoutParams(); + params.height = (int) (height * BADGE_IMAGE_HEIGHT_RATIO); + params.width = (int) (width * BADGE_IMAGE_WIDTH_RATIO); + imageView.setImageResource(R.drawable.badge); + imageView.requestLayout(); + + setSupportActionBar(toolbar); + progressBar.setVisibility(View.VISIBLE); + hideLayouts(); + setAchievements(); + setUploadCount(); + setRevertCount(); + initDrawer(); + } + + /** + * to invoke the AlertDialog on clicking info button + */ + @OnClick(R.id.achievement_info) + public void showInfoDialog(){ + launchAlert(getResources().getString(R.string.Achievements) + ,getResources().getString(R.string.achievements_info_message)); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_about, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == R.id.share_app_icon) { + View rootView = getWindow().getDecorView().findViewById(android.R.id.content); + Bitmap screenShot = Utils.getScreenShot(rootView); + showAlert(screenShot); + } + + return super.onOptionsItemSelected(item); + } + + /** + * To take bitmap and store it temporary storage and share it + * + * @param bitmap + */ + void shareScreen(Bitmap bitmap) { + try { + File file = new File(this.getExternalCacheDir(), "screen.png"); + FileOutputStream fOut = new FileOutputStream(file); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, fOut); + fOut.flush(); + fOut.close(); + file.setReadable(true, false); + final Intent intent = new Intent(android.content.Intent.ACTION_SEND); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file)); + intent.setType("image/png"); + startActivity(Intent.createChooser(intent, "Share image via")); + } catch (IOException e) { + //Do Nothing + } + } + + /** + * To call the API to get results in form Single + * which then calls parseJson when results are fetched + */ + private void setAchievements() { + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getAchievements(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + jsonObject -> parseJson(jsonObject), + t -> Timber.e(t, "Fetching achievements statisticss failed") + )); + } + } + + /** + * To call the API to get reverts count in form of JSONObject + * + */ + + private void setRevertCount(){ + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getRevertCount(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + object -> parseJsonRevertCount(object), + t -> Timber.e(t, "Fetching revert count failed") + )); + } + } + + /** + * used to set number of deleted images + * @param object + */ + private void parseJsonRevertCount(JSONObject object){ + try { + achievements.setRevertCount(object.getInt("deletedUploads")); + } catch (JSONException e) { + Timber.d( e, e.getMessage()); + } + isRevertFetched = true; + hideProgressBar(); + } + + /** + * used to the count of images uploaded by user + */ + private void setUploadCount() { + if(checkAccount()) { + compositeDisposable.add(mediaWikiApi + .getUploadCount(sessionManager.getCurrentAccount().name) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + uploadCount -> setAchievementsUploadCount(uploadCount), + t -> Timber.e(t, "Fetching upload count failed") + )); + } + } + + /** + * used to set achievements upload count and call hideProgressbar + * @param uploadCount + */ + private void setAchievementsUploadCount(int uploadCount){ + achievements.setImagesUploaded(uploadCount); + isUploadFetched = true; + hideProgressBar(); + } + + /** + * used to the uploaded images progressbar + * @param uploadCount + */ + private void setUploadProgress(int uploadCount){ + imagesUploadedProgressbar.setProgress + (100*uploadCount/levelInfo.getMaxUploadCount()); + imagesUploadedProgressbar.setProgressTextFormatPattern + (uploadCount +"/" + levelInfo.getMaxUploadCount() ); + } + + /** + * used to set the non revert image percentage + * @param notRevertPercentage + */ + private void setImageRevertPercentage(int notRevertPercentage){ + imageRevertsProgressbar.setProgress(notRevertPercentage); + String revertPercentage = Integer.toString(notRevertPercentage); + imageRevertsProgressbar.setProgressTextFormatPattern(revertPercentage + "%%"); + imagesRevertLimitText.setText(getResources().getString(R.string.achievements_revert_limit_message)+ levelInfo.getMinNonRevertPercentage() + "%"); + } + + /** + * used to parse the JSONObject containing results + * @param object + */ + private void parseJson(JSONObject object) { + try { + achievements.setUniqueUsedImages(object.getInt("uniqueUsedImages")); + achievements.setArticlesUsingImages(object.getInt("articlesUsingImages")); + achievements.setThanksReceived(object.getInt("thanksReceived")); + achievements.setImagesEditedBySomeoneElse(object.getInt("imagesEditedBySomeoneElse")); + JSONObject featuredImages = object.getJSONObject("featuredImages"); + achievements.setFeaturedImages + (featuredImages.getInt("Quality_images") + + featuredImages.getInt("Featured_pictures_on_Wikimedia_Commons")); + } catch (JSONException e) { + e.printStackTrace(); + } + isStatisticsFetched = true; + hideProgressBar(); + } + + /** + * Used the inflate the fetched statistics of the images uploaded by user + * and assign badge and level + * @param achievements + */ + private void inflateAchievements(Achievements achievements ){ + thanksReceived.setText(Integer.toString(achievements.getThanksReceived())); + imagesUsedByWikiProgessbar.setProgress + (100*achievements.getUniqueUsedImages()/levelInfo.getMaxUniqueImages() ); + imagesUsedByWikiProgessbar.setProgressTextFormatPattern + (achievements.getUniqueUsedImages() + "/" + levelInfo.getMaxUniqueImages()); + imagesFeatured.setText(Integer.toString(achievements.getFeaturedImages())); + String levelUpInfoString = getString(R.string.level); + levelUpInfoString += " " + Integer.toString(levelInfo.getLevelNumber()); + levelNumber.setText(levelUpInfoString); + final ContextThemeWrapper wrapper = new ContextThemeWrapper(this, levelInfo.getLevelStyle()); + Drawable drawable = ResourcesCompat.getDrawable(getResources(), R.drawable.badge, wrapper.getTheme()); + Bitmap bitmap = BitmapUtils.drawableToBitmap(drawable); + BitmapDrawable bitmapImage = BitmapUtils.writeOnDrawable(bitmap, Integer.toString(levelInfo.getLevelNumber()),this); + imageView.setImageDrawable(bitmapImage); + } + + /** + * Creates a way to change current activity to AchievementActivity + * @param context + */ + public static void startYourself(Context context) { + Intent intent = new Intent(context, AchievementsActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); + context.startActivity(intent); + } + + /** + * to hide progressbar + */ + private void hideProgressBar() { + if (progressBar != null && isUploadFetched && isStatisticsFetched && isRevertFetched) { + levelInfo = LevelController.LevelInfo.from(achievements.getImagesUploaded(), + achievements.getUniqueUsedImages(), + achievements.getNotRevertPercentage()); + inflateAchievements(achievements); + setUploadProgress(achievements.getImagesUploaded()); + setImageRevertPercentage(achievements.getNotRevertPercentage()); + progressBar.setVisibility(View.GONE); + layoutImageReverts.setVisibility(View.VISIBLE); + layoutImageUploaded.setVisibility(View.VISIBLE); + layoutImageUsedByWiki.setVisibility(View.VISIBLE); + layoutStatistics.setVisibility(View.VISIBLE); + imageView.setVisibility(View.VISIBLE); + levelNumber.setVisibility(View.VISIBLE); + } + } + + /** + * used to hide the layouts while fetching results from api + */ + private void hideLayouts(){ + layoutImageUsedByWiki.setVisibility(View.INVISIBLE); + layoutImageUploaded.setVisibility(View.INVISIBLE); + layoutImageReverts.setVisibility(View.INVISIBLE); + layoutStatistics.setVisibility(View.INVISIBLE); + imageView.setVisibility(View.INVISIBLE); + levelNumber.setVisibility(View.INVISIBLE); + } + + /** + * It display the alertDialog with Image of screenshot + * @param screenshot + */ + public void showAlert(Bitmap screenshot){ + AlertDialog.Builder alertadd = new AlertDialog.Builder(AchievementsActivity.this); + LayoutInflater factory = LayoutInflater.from(AchievementsActivity.this); + final View view = factory.inflate(R.layout.image_alert_layout, null); + ImageView screenShotImage = (ImageView) view.findViewById(R.id.alert_image); + screenShotImage.setImageBitmap(screenshot); + TextView shareMessage = (TextView) view.findViewById(R.id.alert_text); + shareMessage.setText(R.string.achievements_share_message); + alertadd.setView(view); + alertadd.setPositiveButton("Proceed", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + shareScreen(screenshot); + } + }); + alertadd.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.cancel(); + } + }); + alertadd.show(); + } + + @OnClick(R.id.images_upload_info) + public void showUploadInfo(){ + launchAlert(getResources().getString(R.string.images_uploaded) + ,getResources().getString(R.string.images_uploaded_explanation)); + } + + @OnClick(R.id.images_reverted_info) + public void showRevertedInfo(){ + launchAlert(getResources().getString(R.string.image_reverts) + ,getResources().getString(R.string.images_reverted_explanation)); + } + + @OnClick(R.id.images_used_by_wiki_info) + public void showUsedByWikiInfo(){ + launchAlert(getResources().getString(R.string.images_used_by_wiki) + ,getResources().getString(R.string.images_used_explanation)); + } + + /** + * takes title and message as input to display alerts + * @param title + * @param message + */ + private void launchAlert(String title, String message){ + new AlertDialog.Builder(AchievementsActivity.this) + .setTitle(title) + .setMessage(message) + .setCancelable(true) + .setNeutralButton(android.R.string.ok, (dialog, id) -> dialog.cancel()) + .create() + .show(); + } + + /** + * check to ensure that user is logged in + * @return + */ + private boolean checkAccount(){ + Account currentAccount = sessionManager.getCurrentAccount(); + if(currentAccount == null) { + Timber.d("Current account is null"); + ViewUtil.showLongToast(this, getResources().getString(R.string.user_not_logged_in)); + sessionManager.forceLogin(this); + return false; + } + return true; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java b/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java new file mode 100644 index 000000000..b7400117d --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/BitmapUtils.java @@ -0,0 +1,54 @@ +package fr.free.nrw.commons.achievements; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; + +public class BitmapUtils { + + /** + * write level Number on the badge + * @param bm + * @param text + * @return + */ + public static BitmapDrawable writeOnDrawable(Bitmap bm, String text, Context context){ + Bitmap.Config config = bm.getConfig(); + if(config == null){ + config = Bitmap.Config.ARGB_8888; + } + Bitmap bitmap = Bitmap.createBitmap(bm.getWidth(),bm.getHeight(),config); + Canvas canvas = new Canvas(bitmap); + canvas.drawBitmap(bm, 0, 0, null); + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setStyle(Paint.Style.FILL); + paint.setColor(Color.WHITE); + paint.setTextSize(Math.round(canvas.getHeight()/2)); + paint.setTextAlign(Paint.Align.CENTER); + Rect rectText = new Rect(); + paint.getTextBounds(text,0, text.length(),rectText); + canvas.drawText(text, Math.round(canvas.getWidth()/2),Math.round(canvas.getHeight()/1.35), paint); + return new BitmapDrawable(context.getResources(), bitmap); + } + + /** + * Convert Drawable to bitmap + * @param drawable + * @return + */ + public static Bitmap drawableToBitmap (Drawable drawable) { + if (drawable instanceof BitmapDrawable) { + return ((BitmapDrawable)drawable).getBitmap(); + } + Bitmap bitmap = Bitmap.createBitmap(drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bitmap; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java new file mode 100644 index 000000000..e0f84bbee --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/achievements/LevelController.java @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.achievements; + +import android.util.Log; + +import fr.free.nrw.commons.R; + +/** + * calculates the level of the user + */ +public class LevelController { + + public LevelInfo level; + public enum LevelInfo{ + LEVEL_1(1, R.style.LevelOne, 5, 20, 85), + LEVEL_2(2, R.style.LevelTwo, 10, 30, 86), + LEVEL_3(3, R.style.LevelThree, 15,40, 87), + LEVEL_4(4, R.style.LevelFour,20,50, 88), + LEVEL_5(5, R.style.LevelFive, 25, 60, 89), + LEVEL_6(6,R.style.LevelOne,30,70, 90), + LEVEL_7(7, R.style.LevelTwo, 40, 80, 90), + LEVEL_8(8, R.style.LevelThree, 45, 90, 90), + LEVEL_9(9, R.style.LevelFour, 50, 100, 90), + LEVEL_10(10, R.style.LevelFive, 55, 110, 90), + LEVEL_11(11,R.style.LevelOne, 60, 120, 90), + LEVEL_12(12,R.style.LevelTwo,65 , 130, 90), + LEVEL_13(13,R.style.LevelThree, 70, 140, 90), + LEVEL_14(14,R.style.LevelFour, 75 , 150, 90), + LEVEL_15(15,R.style.LevelFive, 80, 160, 90); + + private int levelNumber; + private int levelStyle; + private int maxUniqueImages; + private int maxUploadCount; + private int minNonRevertPercentage; + + LevelInfo(int levelNumber, + int levelStyle, + int maxUniqueImages, + int maxUploadCount, + int minNonRevertPercentage) { + this.levelNumber = levelNumber; + this.levelStyle = levelStyle; + this.maxUniqueImages = maxUniqueImages; + this.maxUploadCount = maxUploadCount; + this.minNonRevertPercentage = minNonRevertPercentage; + } + + public static LevelInfo from(int imagesUploaded, + int uniqueImagesUsed, + int nonRevertRate) { + LevelInfo level = LEVEL_15; + + for (LevelInfo levelInfo : LevelInfo.values()) { + if (imagesUploaded < levelInfo.maxUploadCount + || uniqueImagesUsed < levelInfo.maxUniqueImages + || nonRevertRate < levelInfo.minNonRevertPercentage ) { + level = levelInfo; + return level; + } + } + return level; + } + + public int getLevelStyle() { + return levelStyle; + } + + public int getLevelNumber() { + return levelNumber; + } + + public int getMaxUniqueImages() { + return maxUniqueImages; + } + + public int getMaxUploadCount() { + return maxUploadCount; + } + + public int getMinNonRevertPercentage(){ + return minNonRevertPercentage; + } + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java index e39528252..611cb7975 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/AuthenticatedActivity.java @@ -4,8 +4,13 @@ import android.os.Bundle; import javax.inject.Inject; +import fr.free.nrw.commons.R; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; import static fr.free.nrw.commons.auth.AccountUtil.AUTH_COOKIE; @@ -34,6 +39,8 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity { if (savedInstanceState != null) { authCookie = savedInstanceState.getString(AUTH_COOKIE); } + + showBlockStatus(); } @Override @@ -45,4 +52,20 @@ public abstract class AuthenticatedActivity extends NavigationBaseActivity { protected abstract void onAuthCookieAcquired(String authCookie); protected abstract void onAuthFailure(); + + /** + * Makes API call to check if user is blocked from Commons. If the user is blocked, a snackbar + * is created to notify the user + */ + protected void showBlockStatus() + { + Observable.fromCallable(() -> mediaWikiApi.isUserBlockedFromCommons()) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .filter(result -> result) + .subscribe(result -> { + ViewUtil.showSnackbar(findViewById(android.R.id.content), R.string.block_notification); + } + ); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java index 773811737..959317c1f 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/LoginActivity.java @@ -5,6 +5,7 @@ import android.accounts.AccountAuthenticatorActivity; import android.accounts.AccountAuthenticatorResponse; import android.accounts.AccountManager; import android.app.ProgressDialog; +import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; @@ -41,10 +42,10 @@ import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.WelcomeActivity; +import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.di.ApplicationlessInjection; import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.ui.widget.HtmlTextView; import fr.free.nrw.commons.utils.ViewUtil; @@ -62,6 +63,7 @@ import static fr.free.nrw.commons.auth.AccountUtil.AUTH_TOKEN_TYPE; public class LoginActivity extends AccountAuthenticatorActivity { public static final String PARAM_USERNAME = "fr.free.nrw.commons.login.username"; + private static final String FEATURED_IMAGES_CATEGORY = "Category:Featured_pictures_on_Wikimedia_Commons"; @Inject MediaWikiApi mwApi; @Inject AccountUtil accountUtil; @@ -146,9 +148,13 @@ public class LoginActivity extends AccountAuthenticatorActivity { } } + /** + * This function is called when user skips the login. + * It redirects the user to Explore Activity. + */ private void skipLogin() { prefs.edit().putBoolean("login_skipped", true).apply(); - NavigationBaseActivity.startActivityWithFlags(this, NearbyActivity.class, Intent.FLAG_ACTIVITY_CLEAR_TOP); + CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY); finish(); } @@ -180,6 +186,7 @@ public class LoginActivity extends AccountAuthenticatorActivity { && sessionManager.isUserLoggedIn() && sessionManager.getCachedAuthCookie() != null) { prefs.edit().putBoolean("login_skipped", false).apply(); + sessionManager.revalidateAuthToken(); startMainActivity(); } @@ -294,11 +301,11 @@ public class LoginActivity extends AccountAuthenticatorActivity { showMessageAndCancelDialog(R.string.login_failed_network); } else if (result.toLowerCase(Locale.getDefault()).contains("nosuchuser".toLowerCase()) || result.toLowerCase().contains("noname".toLowerCase())) { // Matches nosuchuser, nosuchusershort, noname - showMessageAndCancelDialog(R.string.login_failed_username); + showMessageAndCancelDialog(R.string.login_failed_wrong_credentials); emptySensitiveEditFields(); } else if (result.toLowerCase(Locale.getDefault()).contains("wrongpassword".toLowerCase())) { // Matches wrongpassword, wrongpasswordempty - showMessageAndCancelDialog(R.string.login_failed_password); + showMessageAndCancelDialog(R.string.login_failed_wrong_credentials); emptySensitiveEditFields(); } else if (result.toLowerCase(Locale.getDefault()).contains("throttle".toLowerCase())) { // Matches unknown throttle error codes @@ -463,4 +470,9 @@ public class LoginActivity extends AccountAuthenticatorActivity { loginButton.setEnabled(enabled); } } + + public static void startYourself(Context context) { + Intent intent = new Intent(context, LoginActivity.class); + context.startActivity(intent); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java index ad9a0bda4..896158439 100644 --- a/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java +++ b/app/src/main/java/fr/free/nrw/commons/auth/SessionManager.java @@ -5,6 +5,8 @@ import android.accounts.AccountManager; import android.content.Context; import android.content.SharedPreferences; +import javax.annotation.Nullable; + import fr.free.nrw.commons.mwapi.MediaWikiApi; import io.reactivex.Completable; import io.reactivex.Observable; @@ -31,6 +33,7 @@ public class SessionManager { /** * @return Account|null */ + @Nullable public Account getCurrentAccount() { if (currentAccount == null) { AccountManager accountManager = AccountManager.get(context); @@ -81,6 +84,12 @@ public class SessionManager { return sharedPreferences.getBoolean("isUserLoggedIn", false); } + public void forceLogin(Context context) { + if (context != null) { + LoginActivity.startYourself(context); + } + } + public Completable clearAllAccounts() { AccountManager accountManager = AccountManager.get(context); Account[] allAccounts = accountManager.getAccountsByType(ACCOUNT_TYPE); diff --git a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java index ff6ceece4..72de0db70 100644 --- a/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java +++ b/app/src/main/java/fr/free/nrw/commons/caching/CacheController.java @@ -7,18 +7,25 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import fr.free.nrw.commons.upload.MwVolleyApi; +import javax.inject.Inject; +import javax.inject.Singleton; + +import fr.free.nrw.commons.upload.GpsCategoryModel; import timber.log.Timber; +@Singleton public class CacheController { + private final GpsCategoryModel gpsCategoryModel; + private final QuadTree> quadTree; private double x, y; - private QuadTree> quadTree; private double xMinus, xPlus, yMinus, yPlus; private static final int EARTH_RADIUS = 6378137; - public CacheController() { + @Inject + CacheController(GpsCategoryModel gpsCategoryModel) { + this.gpsCategoryModel = gpsCategoryModel; quadTree = new QuadTree<>(-180, -90, +180, +90); } @@ -31,8 +38,8 @@ public class CacheController { public void cacheCategory() { List pointCatList = new ArrayList<>(); - if (MwVolleyApi.GpsCatExists.getGpsCatExists()) { - pointCatList.addAll(MwVolleyApi.getGpsCat()); + if (gpsCategoryModel.getGpsCatExists()) { + pointCatList.addAll(gpsCategoryModel.getCategoryList()); Timber.d("Categories being cached: %s", pointCatList); } else { Timber.d("No categories found, so no categories cached"); @@ -65,7 +72,7 @@ public class CacheController { } //Based on algorithm at http://gis.stackexchange.com/questions/2951/algorithm-for-offsetting-a-latitude-longitude-by-some-amount-of-meters - public void convertCoordRange() { + private void convertCoordRange() { //Position, decimal degrees double lat = y; double lon = x; diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java index e804189ab..93ddb60d5 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategorizationFragment.java @@ -39,7 +39,7 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.mwapi.MediaWikiApi; -import fr.free.nrw.commons.upload.MwVolleyApi; +import fr.free.nrw.commons.upload.GpsCategoryModel; import fr.free.nrw.commons.utils.StringSortingUtils; import fr.free.nrw.commons.utils.ViewUtil; import io.reactivex.Observable; @@ -73,6 +73,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { @Inject @Named("prefs") SharedPreferences prefsPrefs; @Inject @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs; @Inject CategoryDao categoryDao; + @Inject GpsCategoryModel gpsCategoryModel; private RVRendererAdapter categoriesAdapter; private OnCategoriesSaveHandler onCategoriesSaveHandler; @@ -253,7 +254,6 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { } private Observable defaultCategories() { - Observable directCat = directCategories(); if (hasDirectCategories) { Timber.d("Image has direct Cat"); @@ -287,9 +287,7 @@ public class CategorizationFragment extends CommonsDaggerSupportFragment { } private Observable gpsCategories() { - return Observable.fromIterable( - MwVolleyApi.GpsCatExists.getGpsCatExists() - ? MwVolleyApi.getGpsCat() : new ArrayList<>()) + return Observable.fromIterable(gpsCategoryModel.getCategoryList()) .map(name -> new CategoryItem(name, false)); } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java new file mode 100644 index 000000000..3ab3c2c07 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryDetailsActivity.java @@ -0,0 +1,253 @@ +package fr.free.nrw.commons.category; + +import android.content.Context; +import android.content.Intent; +import android.database.DataSetObserver; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.ViewPager; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.FrameLayout; +import android.widget.Toast; + +import java.util.ArrayList; +import java.util.List; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.PageTitle; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.explore.ViewPagerAdapter; +import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.theme.NavigationBaseActivity; + +import static android.widget.Toast.LENGTH_SHORT; + +/** + * This activity displays details of a particular category + * Its generic and simply takes the name of category name in its start intent to load all images, subcategories in + * a particular category on wikimedia commons. + */ + +public class CategoryDetailsActivity extends NavigationBaseActivity + implements MediaDetailPagerFragment.MediaDetailProvider, + AdapterView.OnItemClickListener{ + + + private FragmentManager supportFragmentManager; + private CategoryImagesListFragment categoryImagesListFragment; + private MediaDetailPagerFragment mediaDetails; + private String categoryName; + @BindView(R.id.mediaContainer) FrameLayout mediaContainer; + @BindView(R.id.tabLayout) TabLayout tabLayout; + @BindView(R.id.viewPager) ViewPager viewPager; + + ViewPagerAdapter viewPagerAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_category_details); + ButterKnife.bind(this); + supportFragmentManager = getSupportFragmentManager(); + viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); + viewPager.setAdapter(viewPagerAdapter); + viewPager.setOffscreenPageLimit(2); + tabLayout.setupWithViewPager(viewPager); + setTabs(); + setPageTitle(); + initDrawer(); + forceInitBackButton(); + } + + /** + * This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab, + * Set the fragments according to the tab selected in the viewPager. + */ + private void setTabs() { + List fragmentList = new ArrayList<>(); + List titleList = new ArrayList<>(); + categoryImagesListFragment = new CategoryImagesListFragment(); + SubCategoryListFragment subCategoryListFragment = new SubCategoryListFragment(); + SubCategoryListFragment parentCategoryListFragment = new SubCategoryListFragment(); + categoryName = getIntent().getStringExtra("categoryName"); + if (getIntent() != null && categoryName != null) { + Bundle arguments = new Bundle(); + arguments.putString("categoryName", categoryName); + arguments.putBoolean("isParentCategory", false); + categoryImagesListFragment.setArguments(arguments); + subCategoryListFragment.setArguments(arguments); + Bundle parentCategoryArguments = new Bundle(); + parentCategoryArguments.putString("categoryName", categoryName); + parentCategoryArguments.putBoolean("isParentCategory", true); + parentCategoryListFragment.setArguments(parentCategoryArguments); + } + fragmentList.add(categoryImagesListFragment); + titleList.add("MEDIA"); + fragmentList.add(subCategoryListFragment); + titleList.add("SUBCATEGORIES"); + fragmentList.add(parentCategoryListFragment); + titleList.add("PARENT CATEGORIES"); + viewPagerAdapter.setTabData(fragmentList, titleList); + viewPagerAdapter.notifyDataSetChanged(); + + } + + /** + * Gets the passed categoryName from the intents and displays it as the page title + */ + private void setPageTitle() { + if (getIntent() != null && getIntent().getStringExtra("categoryName") != null) { + setTitle(getIntent().getStringExtra("categoryName")); + } + } + + /** + * This method is called onClick of media inside category details (CategoryImageListFragment). + */ + @Override + public void onItemClick(AdapterView adapterView, View view, int i, long l) { + tabLayout.setVisibility(View.GONE); + viewPager.setVisibility(View.GONE); + mediaContainer.setVisibility(View.VISIBLE); + if (mediaDetails == null || !mediaDetails.isVisible()) { + // set isFeaturedImage true for featured images, to include author field on media detail + mediaDetails = new MediaDetailPagerFragment(false, true); + FragmentManager supportFragmentManager = getSupportFragmentManager(); + supportFragmentManager + .beginTransaction() + .replace(R.id.mediaContainer, mediaDetails) + .addToBackStack(null) + .commit(); + supportFragmentManager.executePendingTransactions(); + } + mediaDetails.showImage(i); + forceInitBackButton(); + } + + + /** + * Consumers should be simply using this method to use this activity. + * @param context A Context of the application package implementing this class. + * @param categoryName Name of the category for displaying its details + */ + public static void startYourself(Context context, String categoryName) { + Intent intent = new Intent(context, CategoryDetailsActivity.class); + intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + intent.putExtra("categoryName", categoryName); + context.startActivity(intent); + } + + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * @param i It is the index of which media object is to be returned which is same as + * current index of viewPager. + * @return Media Object + */ + @Override + public Media getMediaAtPosition(int i) { + if (categoryImagesListFragment.getAdapter() == null) { + // not yet ready to return data + return null; + } else { + return (Media) categoryImagesListFragment.getAdapter().getItem(i); + } + } + + /** + * This method is called on from getCount of MediaDetailPagerFragment + * The viewpager will contain same number of media items as that of media elements in adapter. + * @return Total Media count in the adapter + */ + @Override + public int getTotalMediaCount() { + if (categoryImagesListFragment.getAdapter() == null) { + return 0; + } + return categoryImagesListFragment.getAdapter().getCount(); + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void notifyDatasetChanged() { + + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void registerDataSetObserver(DataSetObserver observer) { + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + + } + + /** + * This method inflates the menu in the toolbar + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.fragment_category_detail, menu); + return super.onCreateOptionsMenu(menu); + } + + /** + * This method handles the logic on ItemSelect in toolbar menu + * Currently only 1 choice is available to open category details page in browser + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + // Handle item selection + switch (item.getItemId()) { + case R.id.menu_browser_current_category: + Intent viewIntent = new Intent(); + viewIntent.setAction(Intent.ACTION_VIEW); + viewIntent.setData(new PageTitle(categoryName).getCanonicalUri()); + //check if web browser available + if (viewIntent.resolveActivity(this.getPackageManager()) != null) { + startActivity(viewIntent); + } else { + Toast toast = Toast.makeText(this, getString(R.string.no_web_browser), LENGTH_SHORT); + toast.show(); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + /** + * This method is called on backPressed of anyFragment in the activity. + * If condition is called when mediaDetailFragment is opened. + */ + @Override + public void onBackPressed() { + if (supportFragmentManager.getBackStackEntryCount() == 1){ + // back to search so show search toolbar and hide navigation toolbar + tabLayout.setVisibility(View.VISIBLE); + viewPager.setVisibility(View.VISIBLE); + mediaContainer.setVisibility(View.GONE); + } + super.onBackPressed(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java index 18749847e..941201235 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImageUtils.java @@ -8,6 +8,7 @@ import org.w3c.dom.NodeList; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collections; import java.util.Date; import java.util.List; @@ -27,12 +28,30 @@ public class CategoryImageUtils { List categoryImages = new ArrayList<>(); for (int i = 0; i < childNodes.getLength(); i++) { Node node = childNodes.item(i); - categoryImages.add(getMediaFromPage(node)); + if (getMediaFromPage(node).getFilename().substring(0,5).equals("File:")){ + categoryImages.add(getMediaFromPage(node)); + } } return categoryImages; } + /** + * The method iterates over the child nodes to return a list of Subcategory name + * sorted alphabetically + * @param childNodes + * @return + */ + public static List getSubCategoryList(NodeList childNodes) { + List subCategories = new ArrayList<>(); + for (int i = 0; i < childNodes.getLength(); i++) { + Node node = childNodes.item(i); + subCategories.add(getMediaFromPage(node).getFilename()); + } + Collections.sort(subCategories); + return subCategories; + } + /** * Creates a new Media object from the XML response as received by the API * @param node diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java index 1f385b258..382134472 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesActivity.java @@ -6,6 +6,9 @@ import android.database.DataSetObserver; import android.os.Bundle; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentTransaction; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.widget.AdapterView; @@ -13,8 +16,9 @@ import butterknife.ButterKnife; import fr.free.nrw.commons.Media; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.explore.SearchActivity; import fr.free.nrw.commons.media.MediaDetailPagerFragment; -import timber.log.Timber; +import fr.free.nrw.commons.theme.NavigationBaseActivity; /** * This activity displays pictures of a particular category @@ -44,6 +48,16 @@ public class CategoryImagesActivity } + /** + * This method is called on backPressed of anyFragment in the activity. + * We are changing the icon here from back to hamburger icon. + */ + @Override + public void onBackPressed() { + initDrawer(); + super.onBackPressed(); + } + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -55,11 +69,6 @@ public class CategoryImagesActivity supportFragmentManager = getSupportFragmentManager(); setCategoryImagesFragment(); supportFragmentManager.addOnBackStackChangedListener(this); - if (savedInstanceState != null) { - mediaDetails = (MediaDetailPagerFragment) supportFragmentManager - .findFragmentById(R.id.fragmentContainer); - - } requestAuthToken(); initDrawer(); setPageTitle(); @@ -95,6 +104,9 @@ public class CategoryImagesActivity public void onBackStackChanged() { } + /** + * This method is called onClick of media inside category details (CategoryImageListFragment). + */ @Override public void onItemClick(AdapterView adapterView, View view, int i, long l) { if (mediaDetails == null || !mediaDetails.isVisible()) { @@ -103,17 +115,34 @@ public class CategoryImagesActivity FragmentManager supportFragmentManager = getSupportFragmentManager(); supportFragmentManager .beginTransaction() - .replace(R.id.fragmentContainer, mediaDetails) + .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) + .add(R.id.fragmentContainer, mediaDetails) .addToBackStack(null) .commit(); - supportFragmentManager.executePendingTransactions(); + // Reason for using hide, add instead of replace is to maintain scroll position after + // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 + // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 supportFragmentManager.executePendingTransactions(); } mediaDetails.showImage(i); + forceInitBackButton(); + } + + /** + * This method is called on backPressed when mediaDetailFragment is opened in the activity. + */ + @Override + protected void onResume() { + if (supportFragmentManager.getBackStackEntryCount()==1){ + //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. + //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 + onBackPressed(); + } + super.onResume(); } /** * Consumers should be simply using this method to use this activity. - * @param context + * @param context A Context of the application package implementing this class. * @param title Page title * @param categoryName Name of the category for displaying its images */ @@ -125,6 +154,12 @@ public class CategoryImagesActivity context.startActivity(intent); } + /** + * This method is called mediaDetailPagerFragment. It returns the Media Object at that Index + * @param i It is the index of which media object is to be returned which is same as + * current index of viewPager. + * @return Media Object + */ @Override public Media getMediaAtPosition(int i) { if (categoryImagesListFragment.getAdapter() == null) { @@ -135,6 +170,11 @@ public class CategoryImagesActivity } } + /** + * This method is called on from getCount of MediaDetailPagerFragment + * The viewpager will contain same number of media items as that of media elements in adapter. + * @return Total Media count in the adapter + */ @Override public int getTotalMediaCount() { if (categoryImagesListFragment.getAdapter() == null) { @@ -143,18 +183,57 @@ public class CategoryImagesActivity return categoryImagesListFragment.getAdapter().getCount(); } + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ @Override public void notifyDatasetChanged() { } + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ @Override public void registerDataSetObserver(DataSetObserver observer) { } + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ @Override public void unregisterDataSetObserver(DataSetObserver observer) { } + + /** + * This method inflates the menu in the toolbar + */ + @Override + public boolean onCreateOptionsMenu(Menu menu) { + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.menu_search, menu); + return super.onCreateOptionsMenu(menu); + } + + /** + * This method handles the logic on ItemSelect in toolbar menu + * Currently only 1 choice is available to open search page of the app + */ + @Override + public boolean onOptionsItemSelected(MenuItem item) { + + // Handle item selection + switch (item.getItemId()) { + case R.id.action_search: + NavigationBaseActivity.startActivityWithFlags(this, SearchActivity.class); + return true; + default: + return super.onOptionsItemSelected(item); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java index 3b6734edd..385662a05 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/category/CategoryImagesListFragment.java @@ -12,7 +12,9 @@ import android.widget.AdapterView; import android.widget.GridView; import android.widget.ListAdapter; import android.widget.ProgressBar; +import android.widget.RelativeLayout; import android.widget.TextView; +import android.widget.Toast; import java.util.List; import java.util.concurrent.TimeUnit; @@ -48,9 +50,9 @@ public class CategoryImagesListFragment extends DaggerFragment { TextView statusTextView; @BindView(R.id.loadingImagesProgressBar) ProgressBar progressBar; @BindView(R.id.categoryImagesList) GridView gridView; - + @BindView(R.id.parentLayout) RelativeLayout parentLayout; private boolean hasMoreImages = true; - private boolean isLoading; + private boolean isLoading = true; private String categoryName = null; @Inject CategoryImageController controller; @@ -123,7 +125,7 @@ public class CategoryImagesListFragment extends DaggerFragment { statusTextView.setVisibility(VISIBLE); statusTextView.setText(getString(R.string.no_internet)); } else { - ViewUtil.showSnackbar(gridView, R.string.no_internet); + ViewUtil.showSnackbar(parentLayout, R.string.no_internet); } } @@ -132,15 +134,20 @@ public class CategoryImagesListFragment extends DaggerFragment { * @param throwable */ private void handleError(Throwable throwable) { - Timber.e(throwable, "Error occurred while loading featured images"); - initErrorView(); + Timber.e(throwable, "Error occurred while loading images inside a category"); + try{ + ViewUtil.showSnackbar(parentLayout, R.string.error_loading_images); + initErrorView(); + }catch (Exception e){ + e.printStackTrace(); + } + } /** * Handles the UI updates for a error scenario */ private void initErrorView() { - ViewUtil.showSnackbar(gridView, R.string.error_loading_images); progressBar.setVisibility(GONE); if (gridAdapter == null || gridAdapter.isEmpty()) { statusTextView.setVisibility(VISIBLE); @@ -152,7 +159,7 @@ public class CategoryImagesListFragment extends DaggerFragment { /** * Initializes the adapter with a list of Media objects - * @param mediaList + * @param mediaList List of new Media to be displayed */ private void setAdapter(List mediaList) { gridAdapter = new GridViewAdapter(this.getContext(), R.layout.layout_category_images, mediaList); @@ -176,6 +183,9 @@ public class CategoryImagesListFragment extends DaggerFragment { isLoading = true; fetchMoreImages(); } + if (!hasMoreImages){ + progressBar.setVisibility(GONE); + } } }); } @@ -201,7 +211,7 @@ public class CategoryImagesListFragment extends DaggerFragment { /** * Handles the success scenario * On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter - * @param collection + * @param collection List of new Media to be displayed */ private void handleSuccess(List collection) { if(collection == null || collection.isEmpty()) { @@ -213,6 +223,10 @@ public class CategoryImagesListFragment extends DaggerFragment { if(gridAdapter == null) { setAdapter(collection); } else { + if (gridAdapter.containsAll(collection)) { + hasMoreImages = false; + return; + } gridAdapter.addItems(collection); } @@ -221,7 +235,13 @@ public class CategoryImagesListFragment extends DaggerFragment { statusTextView.setVisibility(GONE); } + /** + * It return an instance of gridView adapter which helps in extracting media details + * used by the gridView + * @return GridView Adapter + */ public ListAdapter getAdapter() { return gridView.getAdapter(); } + } diff --git a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java index c8e6066f6..f8c54905e 100644 --- a/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java +++ b/app/src/main/java/fr/free/nrw/commons/category/GridViewAdapter.java @@ -42,6 +42,18 @@ public class GridViewAdapter extends ArrayAdapter { notifyDataSetChanged(); } + /** + * Check the first item in the new list with old list and returns true if they are same + * Its triggered on successful response of the fetch images API. + * @param images + */ + public boolean containsAll(List images){ + if (data == null) { + data = new ArrayList<>(); + } + return images.get(0).getFilename().equals(data.get(0).getFilename()); + } + @Override public boolean isEmpty() { return data == null || data.isEmpty(); @@ -66,7 +78,7 @@ public class GridViewAdapter extends ArrayAdapter { MediaWikiImageView imageView = convertView.findViewById(R.id.categoryImageView); TextView fileName = convertView.findViewById(R.id.categoryImageTitle); TextView author = convertView.findViewById(R.id.categoryImageAuthor); - fileName.setText(item.getFilename()); + fileName.setText(item.getDisplayTitle()); setAuthorView(item, author); imageView.setMedia(item); return convertView; diff --git a/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java b/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java new file mode 100644 index 000000000..d3000e9b6 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/category/SubCategoryListFragment.java @@ -0,0 +1,167 @@ +package fr.free.nrw.commons.category; + + +import android.content.Intent; +import android.content.res.Configuration; +import android.os.Bundle; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.explore.categories.SearchCategoriesAdapterFactory; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +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.schedulers.Schedulers; +import timber.log.Timber; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +/** + * Displays the category search screen. + */ + +public class SubCategoryListFragment extends CommonsDaggerSupportFragment { + + private static int TIMEOUT_SECONDS = 15; + + @BindView(R.id.imagesListBox) + RecyclerView categoriesRecyclerView; + @BindView(R.id.imageSearchInProgress) + ProgressBar progressBar; + @BindView(R.id.imagesNotFound) + TextView categoriesNotFoundView; + + private String categoryName = null; + @Inject MediaWikiApi mwApi; + + private RVRendererAdapter categoriesAdapter; + private boolean isParentCategory = true; + + private final SearchCategoriesAdapterFactory adapterFactory = new SearchCategoriesAdapterFactory(item -> { + // Open SubCategory Details page + Intent intent = new Intent(getContext(), CategoryDetailsActivity.class); + intent.putExtra("categoryName", item); + getContext().startActivity(intent); + + }); + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); + ButterKnife.bind(this, rootView); + categoryName = getArguments().getString("categoryName"); + isParentCategory = getArguments().getBoolean("isParentCategory"); + initSubCategoryList(); + if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + } + else{ + categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + } + ArrayList items = new ArrayList<>(); + categoriesAdapter = adapterFactory.create(items); + categoriesRecyclerView.setAdapter(categoriesAdapter); + return rootView; + } + + /** + * Checks for internet connection and then initializes the recycler view with 25 categories of the searched query + * Clearing categoryAdapter every time new keyword is searched so that user can see only new results + */ + public void initSubCategoryList() { + categoriesNotFoundView.setVisibility(GONE); + if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + handleNoInternet(); + return; + } + progressBar.setVisibility(View.VISIBLE); + if (!isParentCategory){ + Observable.fromCallable(() -> mwApi.getSubCategoryList(categoryName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handleSuccess, this::handleError); + }else { + Observable.fromCallable(() -> mwApi.getParentCategoryList(categoryName)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handleSuccess, this::handleError); + } + } + + + /** + * Handles the success scenario + * it initializes the recycler view by adding items to the adapter + * @param subCategoryList + */ + private void handleSuccess(List subCategoryList) { + if(subCategoryList == null || subCategoryList.isEmpty()) { + initEmptyView(); + } + else { + progressBar.setVisibility(View.GONE); + categoriesAdapter.addAll(subCategoryList); + categoriesAdapter.notifyDataSetChanged(); + } + } + + /** + * Logs and handles API error scenario + * @param throwable + */ + private void handleError(Throwable throwable) { + if (!isParentCategory){ + Timber.e(throwable, "Error occurred while loading queried subcategories"); + ViewUtil.showSnackbar(categoriesRecyclerView,R.string.error_loading_categories); + }else { + Timber.e(throwable, "Error occurred while loading queried parentcategories"); + ViewUtil.showSnackbar(categoriesRecyclerView,R.string.error_loading_categories); + } + } + + /** + * Handles the UI updates for a empty results scenario + */ + private void initEmptyView() { + progressBar.setVisibility(GONE); + categoriesNotFoundView.setVisibility(VISIBLE); + if (!isParentCategory){ + categoriesNotFoundView.setText(getString(R.string.no_subcategory_found)); + }else { + categoriesNotFoundView.setText(getString(R.string.no_parentcategory_found)); + } + + } + + /** + * Handles the UI updates for no internet scenario + */ + private void handleNoInternet() { + progressBar.setVisibility(GONE); + ViewUtil.showSnackbar(categoriesRecyclerView, R.string.no_internet); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java index 7861f96de..99009c029 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/Contribution.java @@ -45,6 +45,7 @@ public class Contribution extends Media { private long transferred; private String decimalCoords; private boolean isMultiple; + private String wikiDataEntityId; public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date timestamp, int state, long dataLength, Date dateUploaded, long transferred, @@ -222,4 +223,17 @@ public class Contribution extends Media { throw new RuntimeException("Unrecognized license value: " + license); } + + public String getWikiDataEntityId() { + return wikiDataEntityId; + } + + /** + * When the corresponding wikidata entity is known as in case of nearby uploads, it can be set + * using the setter method + * @param wikiDataEntityId + */ + public void setWikiDataEntityId(String wikiDataEntityId) { + this.wikiDataEntityId = wikiDataEntityId; + } } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java index 37b3d5377..76f3e5a0f 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionController.java @@ -24,6 +24,7 @@ import static android.content.Intent.EXTRA_STREAM; import static fr.free.nrw.commons.contributions.Contribution.SOURCE_CAMERA; import static fr.free.nrw.commons.contributions.Contribution.SOURCE_GALLERY; import static fr.free.nrw.commons.upload.UploadService.EXTRA_SOURCE; +import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; public class ContributionController { @@ -90,7 +91,8 @@ public class ContributionController { fragment.startActivityForResult(pickImageIntent, SELECT_FROM_GALLERY); } - public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload) { + public void handleImagePicked(int requestCode, Intent data, boolean isDirectUpload, String wikiDataEntityId) { + Timber.d("Is direct upload %s and the Wikidata entity ID is %s", isDirectUpload, wikiDataEntityId); FragmentActivity activity = fragment.getActivity(); Timber.d("handleImagePicked() called with onActivityResult()"); Intent shareIntent = new Intent(activity, ShareActivity.class); @@ -102,9 +104,6 @@ public class ContributionController { shareIntent.setType(activity.getContentResolver().getType(imageData)); shareIntent.putExtra(EXTRA_STREAM, imageData); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_GALLERY); - if (isDirectUpload) { - shareIntent.putExtra("isDirectUpload", true); - } break; case SELECT_FROM_CAMERA: //FIXME: Find out appropriate mime type @@ -113,9 +112,6 @@ public class ContributionController { shareIntent.setType("image/jpeg"); shareIntent.putExtra(EXTRA_STREAM, lastGeneratedCaptureUri); shareIntent.putExtra(EXTRA_SOURCE, SOURCE_CAMERA); - if (isDirectUpload) { - shareIntent.putExtra("isDirectUpload", true); - } break; default: @@ -123,6 +119,10 @@ public class ContributionController { } Timber.i("Image selected"); try { + shareIntent.putExtra("isDirectUpload", isDirectUpload); + if (wikiDataEntityId != null && !wikiDataEntityId.equals("")) { + shareIntent.putExtra(WIKIDATA_ENTITY_ID_PREF, wikiDataEntityId); + } activity.startActivity(shareIntent); } catch (SecurityException e) { Timber.e(e, "Security Exception"); diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java index 079cf6477..6d290b1a5 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionDao.java @@ -8,7 +8,6 @@ import android.net.Uri; import android.os.RemoteException; import android.support.annotation.Nullable; import android.text.TextUtils; -import android.util.Log; import java.util.Date; diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java index ad6cff606..e5f6f53bb 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsActivity.java @@ -276,17 +276,25 @@ public class ContributionsActivity .getUploadCount(sessionManager.getCurrentAccount().name) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - uploadCount -> getSupportActionBar().setSubtitle(getResources() - .getQuantityString(R.plurals.contributions_subtitle, - uploadCount, uploadCount)), + .subscribe(this::displayUploadCount, t -> Timber.e(t, "Fetching upload count failed") )); } - public void betaSetUploadCount(int betaUploadCount){ + private void displayUploadCount(Integer uploadCount) { + if (isFinishing() + || getSupportActionBar() == null + || getResources() == null) { + return; + } + getSupportActionBar().setSubtitle(getResources() - .getQuantityString(R.plurals.contributions_subtitle, betaUploadCount, betaUploadCount)); + .getQuantityString(R.plurals.contributions_subtitle, + uploadCount, uploadCount)); + } + + public void betaSetUploadCount(int betaUploadCount) { + displayUploadCount(betaUploadCount); } diff --git a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java index ff400a8dd..0b600c5d0 100644 --- a/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/contributions/ContributionsListFragment.java @@ -117,7 +117,7 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, false); + controller.handleImagePicked(requestCode, data, false, null); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); diff --git a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java index 35305c5ba..b019a6303 100644 --- a/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java +++ b/app/src/main/java/fr/free/nrw/commons/data/DBOpenHelper.java @@ -6,12 +6,13 @@ import android.database.sqlite.SQLiteOpenHelper; import fr.free.nrw.commons.category.CategoryDao; import fr.free.nrw.commons.contributions.ContributionDao; +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; import fr.free.nrw.commons.modifications.ModifierSequenceDao; public class DBOpenHelper extends SQLiteOpenHelper { private static final String DATABASE_NAME = "commons.db"; - private static final int DATABASE_VERSION = 6; + private static final int DATABASE_VERSION = 7; /** * Do not use directly - @Inject an instance where it's needed and let @@ -26,6 +27,7 @@ public class DBOpenHelper extends SQLiteOpenHelper { ContributionDao.Table.onCreate(sqLiteDatabase); ModifierSequenceDao.Table.onCreate(sqLiteDatabase); CategoryDao.Table.onCreate(sqLiteDatabase); + RecentSearchesDao.Table.onCreate(sqLiteDatabase); } @Override @@ -33,5 +35,6 @@ public class DBOpenHelper extends SQLiteOpenHelper { ContributionDao.Table.onUpdate(sqLiteDatabase, from, to); ModifierSequenceDao.Table.onUpdate(sqLiteDatabase, from, to); CategoryDao.Table.onUpdate(sqLiteDatabase, from, to); + RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to); } } diff --git a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java index 51aa85903..70ffec55f 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ActivityBuilderModule.java @@ -4,10 +4,14 @@ import dagger.Module; import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.AboutActivity; import fr.free.nrw.commons.WelcomeActivity; +import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SignupActivity; +import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.category.CategoryImagesActivity; +import fr.free.nrw.commons.explore.SearchActivity; + import fr.free.nrw.commons.nearby.NearbyActivity; import fr.free.nrw.commons.notification.NotificationActivity; import fr.free.nrw.commons.settings.SettingsActivity; @@ -50,4 +54,14 @@ public abstract class ActivityBuilderModule { @ContributesAndroidInjector abstract CategoryImagesActivity bindFeaturedImagesActivity(); + + @ContributesAndroidInjector + abstract SearchActivity bindSearchActivity(); + + @ContributesAndroidInjector + abstract CategoryDetailsActivity bindCategoryDetailsActivity(); + + @ContributesAndroidInjector + abstract AchievementsActivity bindAchievementsActivity(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java index 91f6d4ccb..43721a217 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java +++ b/app/src/main/java/fr/free/nrw/commons/di/CommonsApplicationComponent.java @@ -9,17 +9,18 @@ import dagger.android.support.AndroidSupportInjectionModule; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.contributions.ContributionsSyncAdapter; import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.modifications.ModificationsSyncAdapter; -import fr.free.nrw.commons.settings.SettingsFragment; import fr.free.nrw.commons.nearby.PlaceRenderer; +import fr.free.nrw.commons.upload.FileProcessor; +import fr.free.nrw.commons.settings.SettingsFragment; + @Singleton @Component(modules = { CommonsApplicationModule.class, + NetworkingModule.class, AndroidInjectionModule.class, AndroidSupportInjectionModule.class, ActivityBuilderModule.class, @@ -47,6 +48,8 @@ public interface CommonsApplicationComponent extends AndroidInjector provideLruCache() { return new LruCache<>(1024); } + + @Provides + @Singleton + public WikidataEditListener provideWikidataEditListener() { + return new WikidataEditListenerImpl(); + } } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java index f18c331c5..0db0ff7fb 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/ContentProviderBuilderModule.java @@ -4,6 +4,7 @@ import dagger.Module; import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.category.CategoryContentProvider; import fr.free.nrw.commons.contributions.ContributionsContentProvider; +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; @Module @@ -19,4 +20,7 @@ public abstract class ContentProviderBuilderModule { @ContributesAndroidInjector abstract CategoryContentProvider bindCategoryContentProvider(); + @ContributesAndroidInjector + abstract RecentSearchesContentProvider bindRecentSearchesContentProvider(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java index dfed64871..b14d8feef 100644 --- a/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java +++ b/app/src/main/java/fr/free/nrw/commons/di/FragmentBuilderModule.java @@ -3,8 +3,12 @@ package fr.free.nrw.commons.di; import dagger.Module; import dagger.android.ContributesAndroidInjector; import fr.free.nrw.commons.category.CategorizationFragment; +import fr.free.nrw.commons.category.SubCategoryListFragment; import fr.free.nrw.commons.contributions.ContributionsListFragment; import fr.free.nrw.commons.category.CategoryImagesListFragment; +import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; +import fr.free.nrw.commons.explore.images.SearchImageFragment; +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; import fr.free.nrw.commons.media.MediaDetailFragment; import fr.free.nrw.commons.media.MediaDetailPagerFragment; import fr.free.nrw.commons.nearby.NearbyListFragment; @@ -51,4 +55,16 @@ public abstract class FragmentBuilderModule { @ContributesAndroidInjector abstract CategoryImagesListFragment bindFeaturedImagesListFragment(); + @ContributesAndroidInjector + abstract SubCategoryListFragment bindSubCategoryListFragment(); + + @ContributesAndroidInjector + abstract SearchImageFragment bindBrowseImagesListFragment(); + + @ContributesAndroidInjector + abstract SearchCategoryFragment bindSearchCategoryListFragment(); + + @ContributesAndroidInjector + abstract RecentSearchesFragment bindRecentSearchesFragment(); + } diff --git a/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java new file mode 100644 index 000000000..cd043e950 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/di/NetworkingModule.java @@ -0,0 +1,59 @@ +package fr.free.nrw.commons.di; + +import android.content.Context; +import android.content.SharedPreferences; +import android.support.annotation.NonNull; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +import javax.inject.Named; +import javax.inject.Singleton; + +import dagger.Module; +import dagger.Provides; +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.mwapi.ApacheHttpClientMediaWikiApi; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; + +@Module +@SuppressWarnings({"WeakerAccess", "unused"}) +public class NetworkingModule { + public static final long OK_HTTP_CACHE_SIZE = 10 * 1024 * 1024; + + @Provides + @Singleton + public OkHttpClient provideOkHttpClient() { + return new OkHttpClient.Builder().build(); + } + + @Provides + @Singleton + public MediaWikiApi provideMediaWikiApi(Context context, + @Named("default_preferences") SharedPreferences defaultPreferences, + @Named("category_prefs") SharedPreferences categoryPrefs, + Gson gson) { + return new ApacheHttpClientMediaWikiApi(context, BuildConfig.WIKIMEDIA_API_HOST, BuildConfig.WIKIDATA_API_HOST, defaultPreferences, categoryPrefs, gson); + } + + @Provides + @Named("commons_mediawiki_url") + @NonNull + @SuppressWarnings("ConstantConditions") + public HttpUrl provideMwUrl() { + return HttpUrl.parse(BuildConfig.COMMONS_URL); + } + + /** + * Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere. + * @return returns a singleton Gson instance + */ + @Provides + @Singleton + public Gson provideGson() { + return new GsonBuilder().create(); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java new file mode 100644 index 000000000..f28065eb5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/SearchActivity.java @@ -0,0 +1,243 @@ +package fr.free.nrw.commons.explore; + +import android.database.DataSetObserver; +import android.os.Bundle; +import android.support.design.widget.TabLayout; +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentTransaction; +import android.support.v4.view.ViewPager; +import android.support.v7.widget.Toolbar; +import android.text.TextUtils; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.SearchView; +import android.widget.Toast; + +import com.jakewharton.rxbinding2.view.RxView; +import com.jakewharton.rxbinding2.widget.RxSearchView; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.explore.categories.SearchCategoryFragment; +import fr.free.nrw.commons.explore.images.SearchImageFragment; +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment; +import fr.free.nrw.commons.media.MediaDetailPagerFragment; +import fr.free.nrw.commons.theme.NavigationBaseActivity; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.android.schedulers.AndroidSchedulers; + +/** + * Represents search screen of this app + */ + +public class SearchActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider{ + + @BindView(R.id.toolbar_search) Toolbar toolbar; + @BindView(R.id.searchHistoryContainer) FrameLayout searchHistoryContainer; + @BindView(R.id.mediaContainer) FrameLayout mediaContainer; + @BindView(R.id.searchBox) SearchView searchView; + @BindView(R.id.tabLayout) TabLayout tabLayout; + @BindView(R.id.viewPager) ViewPager viewPager; + + private SearchImageFragment searchImageFragment; + private SearchCategoryFragment searchCategoryFragment; + private RecentSearchesFragment recentSearchesFragment; + private FragmentManager supportFragmentManager; + private MediaDetailPagerFragment mediaDetails; + ViewPagerAdapter viewPagerAdapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_search); + ButterKnife.bind(this); + initDrawer(); + setTitle(getString(R.string.title_activity_search)); + toolbar.setNavigationOnClickListener(v->onBackPressed()); + supportFragmentManager = getSupportFragmentManager(); + setSearchHistoryFragment(); + viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager()); + viewPager.setAdapter(viewPagerAdapter); + tabLayout.setupWithViewPager(viewPager); + setTabs(); + searchView.setQueryHint(getString(R.string.search_commons)); + searchView.onActionViewExpanded(); + searchView.clearFocus(); + + } + + /** + * This method sets the search history fragment. + * Search history fragment is displayed when query is empty. + */ + private void setSearchHistoryFragment() { + recentSearchesFragment = new RecentSearchesFragment(); + FragmentTransaction transaction = supportFragmentManager.beginTransaction(); + transaction.add(R.id.searchHistoryContainer, recentSearchesFragment).commit(); + } + + /** + * Sets the titles in the tabLayout and fragments in the viewPager + */ + public void setTabs() { + List fragmentList = new ArrayList<>(); + List titleList = new ArrayList<>(); + searchImageFragment = new SearchImageFragment(); + searchCategoryFragment= new SearchCategoryFragment(); + fragmentList.add(searchImageFragment); + titleList.add("MEDIA"); + fragmentList.add(searchCategoryFragment); + titleList.add("CATEGORIES"); + + viewPagerAdapter.setTabData(fragmentList, titleList); + viewPagerAdapter.notifyDataSetChanged(); + RxSearchView.queryTextChanges(searchView) + .takeUntil(RxView.detaches(searchView)) + .debounce(500, TimeUnit.MILLISECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( query -> { + //update image list + if (!TextUtils.isEmpty(query)) { + viewPager.setVisibility(View.VISIBLE); + tabLayout.setVisibility(View.VISIBLE); + searchHistoryContainer.setVisibility(View.GONE); + searchImageFragment.updateImageList(query.toString()); + searchCategoryFragment.updateCategoryList(query.toString()); + }else { + viewPager.setVisibility(View.GONE); + tabLayout.setVisibility(View.GONE); + searchHistoryContainer.setVisibility(View.VISIBLE); + recentSearchesFragment.updateRecentSearches(); + // open search history fragment + } + } + ); + } + + /** + * returns Media Object at position + * @param i position of Media in the imagesRecyclerView adapter. + */ + @Override + public Media getMediaAtPosition(int i) { + return searchImageFragment.getImageAtPosition(i); + } + + /** + * returns total number of images present in the imagesRecyclerView adapter. + */ + @Override + public int getTotalMediaCount() { + return searchImageFragment.getTotalImagesCount(); + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void notifyDatasetChanged() { + + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void registerDataSetObserver(DataSetObserver observer) { + + } + + /** + * This method is never called but it was in MediaDetailProvider Interface + * so it needs to be overrided. + */ + @Override + public void unregisterDataSetObserver(DataSetObserver observer) { + + } + + /** + * Open media detail pager fragment on click of image in search results + * @param index item index that should be opened + */ + public void onSearchImageClicked(int index) { + ViewUtil.hideKeyboard(this.findViewById(R.id.searchBox)); + toolbar.setVisibility(View.GONE); + tabLayout.setVisibility(View.GONE); + viewPager.setVisibility(View.GONE); + mediaContainer.setVisibility(View.VISIBLE); + setNavigationBaseToolbarVisibility(true); + if (mediaDetails == null || !mediaDetails.isVisible()) { + // set isFeaturedImage true for featured images, to include author field on media detail + mediaDetails = new MediaDetailPagerFragment(false, true); + FragmentManager supportFragmentManager = getSupportFragmentManager(); + supportFragmentManager + .beginTransaction() + .hide(supportFragmentManager.getFragments().get(supportFragmentManager.getBackStackEntryCount())) + .add(R.id.mediaContainer, mediaDetails) + .addToBackStack(null) + .commit(); + // Reason for using hide, add instead of replace is to maintain scroll position after + // coming back to the search activity. See https://github.com/commons-app/apps-android-commons/issues/1631 + // https://stackoverflow.com/questions/11353075/how-can-i-maintain-fragment-state-when-added-to-the-back-stack/19022550#19022550 + supportFragmentManager.executePendingTransactions(); + } + mediaDetails.showImage(index); + forceInitBackButton(); + } + + /** + * This method is called on Screen Rotation + */ + @Override + protected void onResume() { + if (supportFragmentManager.getBackStackEntryCount()==1){ + //FIXME: Temporary fix for screen rotation inside media details. If we don't call onBackPressed then fragment stack is increasing every time. + //FIXME: Similar issue like this https://github.com/commons-app/apps-android-commons/issues/894 + // This is called on screen rotation when user is inside media details. Ideally it should show Media Details but since we are not saving the state now. We are throwing the user to search screen otherwise the app was crashing. + // + onBackPressed(); + } + super.onResume(); + } + + /** + * This method is called on backPressed of anyFragment in the activity. + * If condition is called when mediaDetailFragment is opened. + */ + @Override + public void onBackPressed() { + if (getSupportFragmentManager().getBackStackEntryCount() == 1){ + // back to search so show search toolbar and hide navigation toolbar + toolbar.setVisibility(View.VISIBLE); + tabLayout.setVisibility(View.VISIBLE); + viewPager.setVisibility(View.VISIBLE); + mediaContainer.setVisibility(View.GONE); + setNavigationBaseToolbarVisibility(false); + }else { + toolbar.setVisibility(View.GONE); + setNavigationBaseToolbarVisibility(true); + } + super.onBackPressed(); + } + + /** + * This method is called on click of a recent search to update query in SearchView. + * @param query Recent Search Query + */ + public void updateText(String query) { + searchView.setQuery(query, true); + // Clear focus of searchView now. searchView.clearFocus(); does not seem to work Check the below link for more details. + // https://stackoverflow.com/questions/6117967/how-to-remove-focus-without-setting-focus-to-another-control/15481511 + viewPager.requestFocus(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/ViewPagerAdapter.java b/app/src/main/java/fr/free/nrw/commons/explore/ViewPagerAdapter.java new file mode 100755 index 000000000..5ebbd4f37 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/ViewPagerAdapter.java @@ -0,0 +1,57 @@ +package fr.free.nrw.commons.explore; + +import android.support.v4.app.Fragment; +import android.support.v4.app.FragmentManager; +import android.support.v4.app.FragmentPagerAdapter; + +import java.util.ArrayList; +import java.util.List; + +/** + * This adapter will be used to display fragments in a ViewPager + */ +public class ViewPagerAdapter extends FragmentPagerAdapter { + private List fragmentList = new ArrayList<>(); + private List fragmentTitleList = new ArrayList<>(); + + public ViewPagerAdapter(FragmentManager manager) { + super(manager); + } + + /** + * This method returns the fragment of the viewpager at a particular position + * @param position + */ + @Override + public Fragment getItem(int position) { + return fragmentList.get(position); + } + + /** + * This method returns the total number of fragments in the viewpager. + * @return size + */ + @Override + public int getCount() { + return fragmentList.size(); + } + + /** + * This method sets the fragment and title list in the viewpager + * @param fragmentList List of all fragments to be displayed in the viewpager + * @param fragmentTitleList List of all titles of the fragments + */ + public void setTabData(List fragmentList, List fragmentTitleList) { + this.fragmentList = fragmentList; + this.fragmentTitleList = fragmentTitleList; + } + + /** + * This method returns the title of the page at a particular position + * @param position + */ + @Override + public CharSequence getPageTitle(int position) { + return fragmentTitleList.get(position); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoriesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoriesAdapterFactory.java new file mode 100644 index 000000000..3ac75e07e --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoriesAdapterFactory.java @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.explore.categories; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +/** + * This class helps in creating adapter for categoriesRecyclerView in SearchCategoryFragment, + * implementing onClicks on categoriesRecyclerView Items + **/ +public class SearchCategoriesAdapterFactory { + private final SearchCategoriesRenderer.CategoryClickedListener listener; + + public SearchCategoriesAdapterFactory(SearchCategoriesRenderer.CategoryClickedListener listener) { + this.listener = listener; + } + + /** + * This method creates a recyclerViewAdapter for Categories. + * @param searchImageItemList List of category name to be displayed + * @return categoriesAdapter + **/ + public RVRendererAdapter create(List searchImageItemList) { + RendererBuilder builder = new RendererBuilder().bind(String.class, new SearchCategoriesRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + searchImageItemList != null ? searchImageItemList : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoriesRenderer.java b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoriesRenderer.java new file mode 100644 index 000000000..631b7b552 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoriesRenderer.java @@ -0,0 +1,56 @@ +package fr.free.nrw.commons.explore.categories; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.pedrogomez.renderers.Renderer; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; + +/** + * presentation logic of individual category in search is handled here + **/ +class SearchCategoriesRenderer extends Renderer { + @BindView(R.id.textView1) TextView tvCategoryName; + + private final CategoryClickedListener listener; + + SearchCategoriesRenderer(CategoryClickedListener listener) { + this.listener = listener; + } + + @Override + protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { + return layoutInflater.inflate(R.layout.item_recent_searches, viewGroup, false); + } + + @Override + protected void setUpView(View view) { + ButterKnife.bind(this, view); + } + + @Override + protected void hookListeners(View view) { + view.setOnClickListener(v -> { + String item = getContent(); + if (listener != null) { + listener.categoryClicked(item); + } + }); + } + + @Override + public void render() { + String item = getContent(); + tvCategoryName.setText(item.replaceFirst("^Category:", "")); + } + + public interface CategoryClickedListener { + void categoryClicked(String item); + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java new file mode 100644 index 000000000..7f2b3ff93 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/categories/SearchCategoryFragment.java @@ -0,0 +1,222 @@ +package fr.free.nrw.commons.explore.categories; + + +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; + +import com.pedrogomez.renderers.RVRendererAdapter; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.inject.Named; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.category.CategoryDetailsActivity; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.explore.recentsearches.RecentSearch; +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +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.schedulers.Schedulers; +import timber.log.Timber; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +/** + * Displays the category search screen. + */ + +public class SearchCategoryFragment extends CommonsDaggerSupportFragment { + + private static int TIMEOUT_SECONDS = 15; + + @BindView(R.id.imagesListBox) + RecyclerView categoriesRecyclerView; + @BindView(R.id.imageSearchInProgress) + ProgressBar progressBar; + @BindView(R.id.imagesNotFound) + TextView categoriesNotFoundView; + String query; + + @Inject RecentSearchesDao recentSearchesDao; + @Inject MediaWikiApi mwApi; + @Inject @Named("default_preferences") SharedPreferences prefs; + + private RVRendererAdapter categoriesAdapter; + private List queryList = new ArrayList<>(); + + private final SearchCategoriesAdapterFactory adapterFactory = new SearchCategoriesAdapterFactory(item -> { + // Called on Click of a individual category Item + // Open Category Details activity + CategoryDetailsActivity.startYourself(getContext(), item); + saveQuery(query); + }); + + /** + * This method saves Search Query in the Recent Searches Database. + * @param query + */ + private void saveQuery(String query) { + RecentSearch recentSearch = recentSearchesDao.find(query); + + // Newly searched query... + if (recentSearch == null) { + recentSearch = new RecentSearch(null, query, new Date()); + } + else { + recentSearch.setLastSearched(new Date()); + } + recentSearchesDao.save(recentSearch); + + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); + ButterKnife.bind(this, rootView); + if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + categoriesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + } + else{ + categoriesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + } + ArrayList items = new ArrayList<>(); + categoriesAdapter = adapterFactory.create(items); + categoriesRecyclerView.setAdapter(categoriesAdapter); + categoriesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + // check if end of recycler view is reached, if yes then add more results to existing results + if (!recyclerView.canScrollVertically(1)) { + addCategoriesToList(query); + } + } + }); + return rootView; + } + + /** + * Checks for internet connection and then initializes the recycler view with 25 categories of the searched query + * Clearing categoryAdapter every time new keyword is searched so that user can see only new results + */ + public void updateCategoryList(String query) { + this.query = query; + categoriesNotFoundView.setVisibility(GONE); + if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + handleNoInternet(); + return; + } + progressBar.setVisibility(View.VISIBLE); + queryList.clear(); + categoriesAdapter.clear(); + Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handleSuccess, this::handleError); + } + + + /** + * Adds more results to existing search results + */ + public void addCategoriesToList(String query) { + this.query = query; + progressBar.setVisibility(View.VISIBLE); + Observable.fromCallable(() -> mwApi.searchCategory(query,queryList.size())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handlePaginationSuccess, this::handleError); + } + + /** + * Handles the success scenario + * it initializes the recycler view by adding items to the adapter + * @param mediaList + */ + private void handlePaginationSuccess(List mediaList) { + queryList.addAll(mediaList); + progressBar.setVisibility(View.GONE); + categoriesAdapter.addAll(mediaList); + categoriesAdapter.notifyDataSetChanged(); + } + + + + /** + * Handles the success scenario + * it initializes the recycler view by adding items to the adapter + * @param mediaList + */ + private void handleSuccess(List mediaList) { + queryList = mediaList; + if(mediaList == null || mediaList.isEmpty()) { + initErrorView(); + } + else { + + progressBar.setVisibility(View.GONE); + categoriesAdapter.addAll(mediaList); + categoriesAdapter.notifyDataSetChanged(); + + // check if user is waiting for 5 seconds if yes then save search query to history. + Handler handler = new Handler(); + handler.postDelayed(() -> saveQuery(query), 5000); + } + } + + /** + * Logs and handles API error scenario + * @param throwable + */ + private void handleError(Throwable throwable) { + Timber.e(throwable, "Error occurred while loading queried categories"); + try { + initErrorView(); + ViewUtil.showSnackbar(categoriesRecyclerView, R.string.error_loading_categories); + }catch (Exception e){ + e.printStackTrace(); + } + + } + + /** + * Handles the UI updates for a error scenario + */ + private void initErrorView() { + progressBar.setVisibility(GONE); + categoriesNotFoundView.setVisibility(VISIBLE); + categoriesNotFoundView.setText(getString(R.string.categories_not_found, query)); + } + + /** + * Handles the UI updates for no internet scenario + */ + private void handleNoInternet() { + progressBar.setVisibility(GONE); + ViewUtil.showSnackbar(categoriesRecyclerView, R.string.no_internet); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java new file mode 100644 index 000000000..a503207e4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImageFragment.java @@ -0,0 +1,245 @@ +package fr.free.nrw.commons.explore.images; + + +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Bundle; +import android.os.Handler; +import android.support.v7.widget.GridLayoutManager; +import android.support.v7.widget.LinearLayoutManager; +import android.support.v7.widget.RecyclerView; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ProgressBar; +import android.widget.TextView; +import com.pedrogomez.renderers.RVRendererAdapter; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.inject.Named; +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.explore.SearchActivity; +import fr.free.nrw.commons.explore.recentsearches.RecentSearch; +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +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.schedulers.Schedulers; +import timber.log.Timber; + +import static android.view.View.GONE; +import static android.view.View.VISIBLE; + +/** + * Displays the image search screen. + */ + +public class SearchImageFragment extends CommonsDaggerSupportFragment { + + private static int TIMEOUT_SECONDS = 15; + + @BindView(R.id.imagesListBox) + RecyclerView imagesRecyclerView; + @BindView(R.id.imageSearchInProgress) + ProgressBar progressBar; + @BindView(R.id.imagesNotFound) + TextView imagesNotFoundView; + String query; + + @Inject RecentSearchesDao recentSearchesDao; + @Inject MediaWikiApi mwApi; + @Inject @Named("default_preferences") SharedPreferences prefs; + + private RVRendererAdapter imagesAdapter; + private List queryList = new ArrayList<>(); + + private final SearchImagesAdapterFactory adapterFactory = new SearchImagesAdapterFactory(item -> { + // Called on Click of a individual media Item + int index = queryList.indexOf(item); + ((SearchActivity)getContext()).onSearchImageClicked(index); + saveQuery(query); + }); + + /** + * This method saves Search Query in the Recent Searches Database. + * @param query + */ + private void saveQuery(String query) { + RecentSearch recentSearch = recentSearchesDao.find(query); + + // Newly searched query... + if (recentSearch == null) { + recentSearch = new RecentSearch(null, query, new Date()); + } + else { + recentSearch.setLastSearched(new Date()); + } + + recentSearchesDao.save(recentSearch); + + } + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false); + ButterKnife.bind(this, rootView); + if(getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){ + imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + } + else{ + imagesRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), 2)); + } + ArrayList items = new ArrayList<>(); + imagesAdapter = adapterFactory.create(items); + imagesRecyclerView.setAdapter(imagesAdapter); + imagesRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrollStateChanged(RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); + // check if end of recycler view is reached, if yes then add more results to existing results + if (!recyclerView.canScrollVertically(1)) { + addImagesToList(query); + } + } + }); + return rootView; + } + + /** + * Checks for internet connection and then initializes the recycler view with 25 images of the searched query + * Clearing imageAdapter every time new keyword is searched so that user can see only new results + */ + public void updateImageList(String query) { + this.query = query; + imagesNotFoundView.setVisibility(GONE); + if(!NetworkUtils.isInternetConnectionEstablished(getContext())) { + handleNoInternet(); + return; + } + progressBar.setVisibility(View.VISIBLE); + queryList.clear(); + imagesAdapter.clear(); + Observable.fromCallable(() -> mwApi.searchImages(query,queryList.size())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handleSuccess, this::handleError); + } + + + /** + * Adds more results to existing search results + */ + public void addImagesToList(String query) { + this.query = query; + progressBar.setVisibility(View.VISIBLE); + Observable.fromCallable(() -> mwApi.searchImages(query,queryList.size())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .subscribe(this::handlePaginationSuccess, this::handleError); + } + + /** + * Handles the success scenario + * it initializes the recycler view by adding items to the adapter + * @param mediaList List of media to be added + */ + private void handlePaginationSuccess(List mediaList) { + queryList.addAll(mediaList); + progressBar.setVisibility(View.GONE); + imagesAdapter.addAll(mediaList); + imagesAdapter.notifyDataSetChanged(); + } + + + + /** + * Handles the success scenario + * it initializes the recycler view by adding items to the adapter + * @param mediaList List of media to be shown + */ + private void handleSuccess(List mediaList) { + queryList = mediaList; + if(mediaList == null || mediaList.isEmpty()) { + initErrorView(); + } + else { + + progressBar.setVisibility(View.GONE); + imagesAdapter.addAll(mediaList); + imagesAdapter.notifyDataSetChanged(); + + // check if user is waiting for 5 seconds if yes then save search query to history. + Handler handler = new Handler(); + handler.postDelayed(() -> saveQuery(query), 5000); + } + } + + /** + * Logs and handles API error scenario + * @param throwable + */ + private void handleError(Throwable throwable) { + Timber.e(throwable, "Error occurred while loading queried images"); + try { + initErrorView(); + ViewUtil.showSnackbar(imagesRecyclerView, R.string.error_loading_images); + }catch (Exception e){ + e.printStackTrace(); + } + } + + /** + * Handles the UI updates for a error scenario + */ + private void initErrorView() { + progressBar.setVisibility(GONE); + imagesNotFoundView.setVisibility(VISIBLE); + imagesNotFoundView.setText(getString(R.string.images_not_found, query)); + } + + /** + * Handles the UI updates for no internet scenario + */ + private void handleNoInternet() { + progressBar.setVisibility(GONE); + ViewUtil.showSnackbar(imagesRecyclerView, R.string.no_internet); + } + + /** + * returns total number of images present in the recyclerview adapter. + */ + public int getTotalImagesCount(){ + if (imagesAdapter == null) { + return 0; + } + else { + return imagesAdapter.getItemCount(); + } + } + + /** + * returns Media Object at position + * @param i position of Media in the recyclerview adapter. + */ + public Media getImageAtPosition(int i) { + if (imagesAdapter.getItem(i).getFilename() == null) { + // not yet ready to return data + return null; + } + else { + return new Media(imagesAdapter.getItem(i).getFilename()); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapterFactory.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapterFactory.java new file mode 100644 index 000000000..94930e261 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesAdapterFactory.java @@ -0,0 +1,35 @@ +package fr.free.nrw.commons.explore.images; + +import com.pedrogomez.renderers.ListAdapteeCollection; +import com.pedrogomez.renderers.RVRendererAdapter; +import com.pedrogomez.renderers.RendererBuilder; + +import java.util.Collections; +import java.util.List; + +import fr.free.nrw.commons.Media; + +/** + * This class helps in creating adapter for imagesRecyclerView in SearchImagesFragment, + * implementing onClicks on imagesRecyclerView Items + **/ +class SearchImagesAdapterFactory { + private final SearchImagesRenderer.ImageClickedListener listener; + + SearchImagesAdapterFactory(SearchImagesRenderer.ImageClickedListener listener) { + this.listener = listener; + } + + /** + * This method creates a recyclerViewAdapter for Media. + * @param searchImageItemList List of Media objects to be displayed + * @return imagesAdapter + **/ + public RVRendererAdapter create(List searchImageItemList) { + RendererBuilder builder = new RendererBuilder() + .bind(Media.class, new SearchImagesRenderer(listener)); + ListAdapteeCollection collection = new ListAdapteeCollection<>( + searchImageItemList != null ? searchImageItemList : Collections.emptyList()); + return new RVRendererAdapter<>(builder, collection); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java new file mode 100644 index 000000000..42c044d70 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/images/SearchImagesRenderer.java @@ -0,0 +1,75 @@ +package fr.free.nrw.commons.explore.images; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import com.pedrogomez.renderers.Renderer; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.Media; +import fr.free.nrw.commons.MediaWikiImageView; +import fr.free.nrw.commons.R; + +/** + * presentation logic of individual image in search is handled here + **/ +class SearchImagesRenderer extends Renderer { + @BindView(R.id.categoryImageTitle) TextView tvImageName; + @BindView(R.id.categoryImageAuthor) TextView categoryImageAuthor; + @BindView(R.id.categoryImageView) + MediaWikiImageView browseImage; + + private final ImageClickedListener listener; + + SearchImagesRenderer(ImageClickedListener listener) { + this.listener = listener; + } + + @Override + protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { + return layoutInflater.inflate(R.layout.layout_category_images, viewGroup, false); + } + + @Override + protected void setUpView(View view) { + ButterKnife.bind(this, view); + } + + @Override + protected void hookListeners(View view) { + view.setOnClickListener(v -> { + Media item = getContent(); + if (listener != null) { + listener.imageClicked(item); + } + }); + } + + @Override + public void render() { + Media item = getContent(); + tvImageName.setText(item.getDisplayTitle()); + browseImage.setMedia(item); + setAuthorView(item, categoryImageAuthor); + } + + interface ImageClickedListener { + void imageClicked(Media item); + } + + /** + * formats author name as "Uploaded by: authorName" and sets it in textview + */ + private void setAuthorView(Media item, TextView author) { + if (item.getCreator() != null && !item.getCreator().equals("")) { + author.setVisibility(View.GONE); + String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by); + author.setText(String.format(uploadedByTemplate, item.getCreator())); + } else { + author.setVisibility(View.VISIBLE); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearch.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearch.java new file mode 100644 index 000000000..c5172bb7c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearch.java @@ -0,0 +1,70 @@ +package fr.free.nrw.commons.explore.recentsearches; + +import android.net.Uri; +import java.util.Date; + +/** + * Represents a recently searched query + * Example - query = "butterfly" + */ +public class RecentSearch { + private Uri contentUri; + private String query; + private Date lastSearched; + + /** + * Constructor + * @param contentUri the content URI for this query + * @param query query name + * @param lastSearched last searched date + */ + public RecentSearch(Uri contentUri, String query, Date lastSearched) { + this.contentUri = contentUri; + this.query = query; + this.lastSearched = lastSearched; + } + + /** + * Gets query name + * @return query name + */ + public String getQuery() { + return query; + } + + /** + * Gets last searched date + * @return Last searched date + */ + public Date getLastSearched() { + // warning: Date objects are mutable. + return (Date)lastSearched.clone(); + } + + + /** + * Updates the last searched date + * @param lastSearched Last searched date + */ + public void setLastSearched(Date lastSearched) { + this.lastSearched = lastSearched; + } + + /** + * Gets the content URI for this query + * @return content URI + */ + public Uri getContentUri() { + return contentUri; + } + + /** + * Modifies the content URI - marking this query as already saved in the database + * + * @param contentUri the content URI + */ + public void setContentUri(Uri contentUri) { + this.contentUri = contentUri; + } + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java new file mode 100644 index 000000000..bf3cf959a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesContentProvider.java @@ -0,0 +1,202 @@ +package fr.free.nrw.commons.explore.recentsearches; + +import android.content.ContentValues; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteQueryBuilder; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.text.TextUtils; + +import javax.inject.Inject; + +import fr.free.nrw.commons.contributions.ContributionDao; +import fr.free.nrw.commons.data.DBOpenHelper; +import fr.free.nrw.commons.di.CommonsDaggerContentProvider; +import timber.log.Timber; + +import static android.content.UriMatcher.NO_MATCH; +import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.ALL_FIELDS; +import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.COLUMN_ID; +import static fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.TABLE_NAME; + + +/** + * This class contains functions for executing queries for + * inserting, searching, deleting, editing recent searches in SqLite DB + **/ +public class RecentSearchesContentProvider extends CommonsDaggerContentProvider { + + public static final String RECENT_SEARCH_AUTHORITY = "fr.free.nrw.commons.explore.recentsearches.contentprovider"; + // For URI matcher + private static final int RECENT_SEARCHES = 1; + private static final int RECENT_SEARCHES_ID = 2; + private static final String BASE_PATH = "recent_searches"; + public static final Uri BASE_URI = Uri.parse("content://" + RECENT_SEARCH_AUTHORITY + "/" + BASE_PATH); + private static final UriMatcher uriMatcher = new UriMatcher(NO_MATCH); + + static { + uriMatcher.addURI(RECENT_SEARCH_AUTHORITY, BASE_PATH, RECENT_SEARCHES); + uriMatcher.addURI(RECENT_SEARCH_AUTHORITY, BASE_PATH + "/#", RECENT_SEARCHES_ID); + } + + public static Uri uriForId(int id) { + return Uri.parse(BASE_URI.toString() + "/" + id); + } + + @Inject DBOpenHelper dbOpenHelper; + + /** + * This functions executes query for searching recent searches in SqLite DB + **/ + @SuppressWarnings("ConstantConditions") + @Override + public Cursor query(@NonNull Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder(); + queryBuilder.setTables(TABLE_NAME); + + int uriType = uriMatcher.match(uri); + + SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); + Cursor cursor; + + switch (uriType) { + case RECENT_SEARCHES: + cursor = queryBuilder.query(db, projection, selection, selectionArgs, + null, null, sortOrder); + break; + case RECENT_SEARCHES_ID: + cursor = queryBuilder.query(db, + ALL_FIELDS, + "_id = ?", + new String[]{uri.getLastPathSegment()}, + null, + null, + sortOrder + ); + break; + default: + throw new IllegalArgumentException("Unknown URI" + uri); + } + + cursor.setNotificationUri(getContext().getContentResolver(), uri); + + return cursor; + } + + @Override + public String getType(@NonNull Uri uri) { + return null; + } + + /** + * This functions executes query for inserting a recentSearch object in SqLite DB + **/ + @SuppressWarnings("ConstantConditions") + @Override + public Uri insert(@NonNull Uri uri, ContentValues contentValues) { + int uriType = uriMatcher.match(uri); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); + long id; + switch (uriType) { + case RECENT_SEARCHES: + id = sqlDB.insert(TABLE_NAME, null, contentValues); + break; + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + getContext().getContentResolver().notifyChange(uri, null); + return Uri.parse(BASE_URI + "/" + id); + } + + /** + * This functions executes query for deleting a recentSearch object in SqLite DB + **/ + @Override + public int delete(@NonNull Uri uri, String s, String[] strings) { + int rows; + int uriType = uriMatcher.match(uri); + SQLiteDatabase db = dbOpenHelper.getReadableDatabase(); + switch (uriType) { + case RECENT_SEARCHES_ID: + Timber.d("Deleting recent searches id %s", uri.getLastPathSegment()); + rows = db.delete(RecentSearchesDao.Table.TABLE_NAME, + "_id = ?", + new String[]{uri.getLastPathSegment()} + ); + break; + default: + throw new IllegalArgumentException("Unknown URI" + uri); + } + getContext().getContentResolver().notifyChange(uri, null); + return rows; + } + + /** + * This functions executes query for inserting multiple recentSearch objects in SqLite DB + **/ + @SuppressWarnings("ConstantConditions") + @Override + public int bulkInsert(@NonNull Uri uri, @NonNull ContentValues[] values) { + Timber.d("Hello, bulk insert! (RecentSearchesContentProvider)"); + int uriType = uriMatcher.match(uri); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); + sqlDB.beginTransaction(); + switch (uriType) { + case RECENT_SEARCHES: + for (ContentValues value : values) { + Timber.d("Inserting! %s", value); + sqlDB.insert(TABLE_NAME, null, value); + } + break; + default: + throw new IllegalArgumentException("Unknown URI: " + uri); + } + sqlDB.setTransactionSuccessful(); + sqlDB.endTransaction(); + getContext().getContentResolver().notifyChange(uri, null); + return values.length; + } + + /** + * This functions executes query for updating a particular recentSearch object in SqLite DB + **/ + @SuppressWarnings("ConstantConditions") + @Override + public int update(@NonNull Uri uri, ContentValues contentValues, String selection, + String[] selectionArgs) { + /* + SQL Injection warnings: First, note that we're not exposing this to the + outside world (exported="false"). Even then, we should make sure to sanitize + all user input appropriately. Input that passes through ContentValues + should be fine. So only issues are those that pass in via concating. + + In here, the only concat created argument is for id. It is cast to an int, + and will error out otherwise. + */ + int uriType = uriMatcher.match(uri); + SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase(); + int rowsUpdated; + switch (uriType) { + case RECENT_SEARCHES_ID: + if (TextUtils.isEmpty(selection)) { + int id = Integer.valueOf(uri.getLastPathSegment()); + rowsUpdated = sqlDB.update(TABLE_NAME, + contentValues, + COLUMN_ID + " = ?", + new String[]{String.valueOf(id)}); + } else { + throw new IllegalArgumentException( + "Parameter `selection` should be empty when updating an ID"); + } + break; + default: + throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType); + } + getContext().getContentResolver().notifyChange(uri, null); + return rowsUpdated; + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java new file mode 100644 index 000000000..4bc84b7b1 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDao.java @@ -0,0 +1,235 @@ +package fr.free.nrw.commons.explore.recentsearches; + +import android.content.ContentProviderClient; +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.os.RemoteException; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.util.Log; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Provider; + +/** + * This class doesn't execute queries in database directly instead it contains the logic behind + * inserting, deleting, searching data from recent searches database. + **/ +public class RecentSearchesDao { + + private final Provider clientProvider; + + @Inject + public RecentSearchesDao(@Named("recentsearch") Provider clientProvider) { + this.clientProvider = clientProvider; + } + + /** + * This method is called on click of media/ categories for storing them in recent searches + * @param recentSearch a recent searches object that is to be added in SqLite DB + */ + public void save(RecentSearch recentSearch) { + ContentProviderClient db = clientProvider.get(); + try { + if (recentSearch.getContentUri() == null) { + recentSearch.setContentUri(db.insert(RecentSearchesContentProvider.BASE_URI, toContentValues(recentSearch))); + } else { + db.update(recentSearch.getContentUri(), toContentValues(recentSearch), null, null); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + db.release(); + } + } + + /** + * This method is called on confirmation of delete recent searches. + * It deletes latest 10 recent searches from the database + * @param recentSearchesStringList list of recent searches to be deleted + */ + public void deleteAll(List recentSearchesStringList) { + ContentProviderClient db = clientProvider.get(); + for (String recentSearchName : recentSearchesStringList) { + try { + RecentSearch recentSearch = find(recentSearchName); + if (recentSearch.getContentUri() == null) { + throw new RuntimeException("tried to delete item with no content URI"); + } else { + Log.d("QUERY_NAME",recentSearch.getContentUri()+"- delete tried"); + db.delete(recentSearch.getContentUri(), null, null); + Log.d("QUERY_NAME",recentSearch.getQuery()+"- query deleted"); + } + } catch (RemoteException e) { + Log.d("Exception",e+"- query deleted"); + throw new RuntimeException(e); + } finally { + db.release(); + } + } + } + + /** + * Find persisted search query in database, based on its name. + * @param name Search query Ex- "butterfly" + * @return recently searched query from database, or null if not found + */ + @Nullable + public RecentSearch find(String name) { + Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); + try { + cursor = db.query( + RecentSearchesContentProvider.BASE_URI, + Table.ALL_FIELDS, + Table.COLUMN_NAME + "=?", + new String[]{name}, + null); + if (cursor != null && cursor.moveToFirst()) { + return fromCursor(cursor); + } + } catch (RemoteException e) { + // This feels lazy, but to hell with checked exceptions. :) + throw new RuntimeException(e); + } finally { + if (cursor != null) { + cursor.close(); + } + db.release(); + } + return null; + } + + /** + * Retrieve recently-searched queries, ordered by descending date. + * @return a list containing recent searches + */ + @NonNull + public List recentSearches(int limit) { + List items = new ArrayList<>(); + Cursor cursor = null; + ContentProviderClient db = clientProvider.get(); + try { + cursor = db.query( RecentSearchesContentProvider.BASE_URI, Table.ALL_FIELDS, + null, new String[]{}, Table.COLUMN_LAST_USED + " DESC"); + // fixme add a limit on the original query instead of falling out of the loop? + while (cursor != null && cursor.moveToNext() && cursor.getPosition() < limit) { + items.add(fromCursor(cursor).getQuery()); + } + } catch (RemoteException e) { + throw new RuntimeException(e); + } finally { + if (cursor != null) { + cursor.close(); + } + db.release(); + } + return items; + } + + + /** + * It creates an Recent Searches object from data stored in the SQLite DB by using cursor + * @param cursor + * @return RecentSearch object + */ + @NonNull + RecentSearch fromCursor(Cursor cursor) { + // Hardcoding column positions! + return new RecentSearch( + RecentSearchesContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(Table.COLUMN_ID))), + cursor.getString(cursor.getColumnIndex(Table.COLUMN_NAME)), + new Date(cursor.getLong(cursor.getColumnIndex(Table.COLUMN_LAST_USED))) + ); + } + + /** + * This class contains the database table architechture for recent searches, + * It also contains queries and logic necessary to the create, update, delete this table. + */ + private ContentValues toContentValues(RecentSearch recentSearch) { + ContentValues cv = new ContentValues(); + cv.put(RecentSearchesDao.Table.COLUMN_NAME, recentSearch.getQuery()); + cv.put(RecentSearchesDao.Table.COLUMN_LAST_USED, recentSearch.getLastSearched().getTime()); + return cv; + } + + /** + * This class contains the database table architechture for recent searches, + * It also contains queries and logic necessary to the create, update, delete this table. + */ + public static class Table { + public static final String TABLE_NAME = "recent_searches"; + public static final String COLUMN_ID = "_id"; + static final String COLUMN_NAME = "name"; + static final String COLUMN_LAST_USED = "last_used"; + + // NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES. + public static final String[] ALL_FIELDS = { + COLUMN_ID, + COLUMN_NAME, + COLUMN_LAST_USED, + }; + + static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME; + + static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " (" + + COLUMN_ID + " INTEGER PRIMARY KEY," + + COLUMN_NAME + " STRING," + + COLUMN_LAST_USED + " INTEGER" + + ");"; + + /** + * This method creates a RecentSearchesTable in SQLiteDatabase + * @param db SQLiteDatabase + */ + public static void onCreate(SQLiteDatabase db) { + db.execSQL(CREATE_TABLE_STATEMENT); + } + + /** + * This method deletes RecentSearchesTable from SQLiteDatabase + * @param db SQLiteDatabase + */ + public static void onDelete(SQLiteDatabase db) { + db.execSQL(DROP_TABLE_STATEMENT); + onCreate(db); + } + + /** + * This method is called on migrating from a older version to a newer version + * @param db SQLiteDatabase + * @param from Version from which we are migrating + * @param to Version to which we are migrating + */ + public static void onUpdate(SQLiteDatabase db, int from, int to) { + if (from == to) { + return; + } + if (from < 6) { + // doesn't exist yet + from++; + onUpdate(db, from, to); + return; + } + if (from == 6) { + // table added in version 7 + onCreate(db); + from++; + onUpdate(db, from, to); + return; + } + if (from == 7) { + from++; + onUpdate(db, from, to); + return; + } + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java new file mode 100644 index 000000000..5c109fbb4 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/explore/recentsearches/RecentSearchesFragment.java @@ -0,0 +1,85 @@ +package fr.free.nrw.commons.explore.recentsearches; + +import android.os.Bundle; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.ImageView; +import android.widget.ListView; +import android.widget.Toast; + +import java.util.List; + +import javax.inject.Inject; + +import butterknife.BindView; +import butterknife.ButterKnife; +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; +import fr.free.nrw.commons.explore.SearchActivity; + + +/** + * Displays the recent searches screen. + */ +public class RecentSearchesFragment extends CommonsDaggerSupportFragment { + @Inject RecentSearchesDao recentSearchesDao; + @BindView(R.id.recent_searches_list) ListView recentSearchesList; + List recentSearches; + ArrayAdapter adapter; + @BindView(R.id.recent_searches_delete_button) + ImageView recent_searches_delete_button; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View rootView = inflater.inflate(R.layout.fragment_search_history, container, false); + ButterKnife.bind(this, rootView); + recentSearches = recentSearchesDao.recentSearches(10); + recent_searches_delete_button.setOnClickListener(v -> { + new AlertDialog.Builder(getContext()) + .setMessage(getString(R.string.delete_recent_searches_dialog)) + .setPositiveButton("YES", (dialog, which) -> { + recentSearchesDao.deleteAll(recentSearches); + Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show(); + recentSearches = recentSearchesDao.recentSearches(10); + adapter = new ArrayAdapter(getContext(),R.layout.item_recent_searches, recentSearches); + recentSearchesList.setAdapter(adapter); + adapter.notifyDataSetChanged(); + dialog.dismiss(); + }) + .setNegativeButton("NO", null) + .create() + .show(); + }); + adapter = new ArrayAdapter(getContext(),R.layout.item_recent_searches, recentSearches); + recentSearchesList.setAdapter(adapter); + recentSearchesList.setOnItemClickListener((parent, view, position, id) -> ( + (SearchActivity)getContext()).updateText(recentSearches.get(position))); + adapter.notifyDataSetChanged(); + return rootView; + } + + /** + * This method is called on back press of activity + * so we are updating the list from database to refresh the recent searches list. + */ + @Override + public void onResume() { + recentSearches = recentSearchesDao.recentSearches(10); + adapter.notifyDataSetChanged(); + super.onResume(); + } + + /** + * This method is called when search query is null to update Recent Searches + */ + public void updateRecentSearches() { + recentSearches = recentSearchesDao.recentSearches(10); + adapter = new ArrayAdapter(getContext(),R.layout.item_recent_searches, recentSearches); + recentSearchesList.setAdapter(adapter); + adapter.notifyDataSetChanged(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java new file mode 100644 index 000000000..9087f9501 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/glide/SvgDecoder.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.glide; + +import android.support.annotation.NonNull; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.ResourceDecoder; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.SimpleResource; +import com.caverock.androidsvg.SVG; +import com.caverock.androidsvg.SVGParseException; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Decodes an SVG internal representation from an {@link InputStream}. + */ +public class SvgDecoder implements ResourceDecoder { + + @Override + public boolean handles(@NonNull InputStream source, @NonNull Options options) { + // TODO: Can we tell? + return true; + } + + public Resource decode(@NonNull InputStream source, int width, int height, + @NonNull Options options) + throws IOException { + try { + SVG svg = SVG.getFromInputStream(source); + return new SimpleResource<>(svg); + } catch (SVGParseException ex) { + throw new IOException("Cannot load SVG from stream", ex); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java new file mode 100644 index 000000000..89910c8fb --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/glide/SvgDrawableTranscoder.java @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.glide; + +import android.graphics.Picture; +import android.graphics.drawable.PictureDrawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import com.bumptech.glide.load.Options; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.SimpleResource; +import com.bumptech.glide.load.resource.transcode.ResourceTranscoder; +import com.caverock.androidsvg.SVG; + +/** + * Convert the {@link SVG}'s internal representation to an Android-compatible one + * ({@link Picture}). + */ +public class SvgDrawableTranscoder implements ResourceTranscoder { + @Nullable + @Override + public Resource transcode(@NonNull Resource toTranscode, + @NonNull Options options) { + SVG svg = toTranscode.get(); + Picture picture = svg.renderToPicture(); + PictureDrawable drawable = new PictureDrawable(picture); + return new SimpleResource<>(drawable); + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java b/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java new file mode 100644 index 000000000..66a3bd6bf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/glide/SvgSoftwareLayerSetter.java @@ -0,0 +1,51 @@ +package fr.free.nrw.commons.glide; + +import android.graphics.drawable.PictureDrawable; +import android.widget.ImageView; + +import com.bumptech.glide.load.DataSource; +import com.bumptech.glide.load.engine.GlideException; +import com.bumptech.glide.request.RequestListener; +import com.bumptech.glide.request.target.ImageViewTarget; +import com.bumptech.glide.request.target.Target; + +/** + * Listener which updates the {@link ImageView} to be software rendered, because + * {@link com.caverock.androidsvg.SVG SVG}/{@link android.graphics.Picture Picture} can't render on + * a hardware backed {@link android.graphics.Canvas Canvas}. + */ +public class SvgSoftwareLayerSetter implements RequestListener { + + /** + * Sets the layer type to none if the load fails + * @param e + * @param model + * @param target + * @param isFirstResource + * @return + */ + @Override + public boolean onLoadFailed(GlideException e, Object model, Target target, + boolean isFirstResource) { + ImageView view = ((ImageViewTarget) target).getView(); + view.setLayerType(ImageView.LAYER_TYPE_NONE, null); + return false; + } + + /** + * Sets the layer type to software when the resource is ready + * @param resource + * @param model + * @param target + * @param dataSource + * @param isFirstResource + * @return + */ + @Override + public boolean onResourceReady(PictureDrawable resource, Object model, + Target target, DataSource dataSource, boolean isFirstResource) { + ImageView view = ((ImageViewTarget) target).getView(); + view.setLayerType(ImageView.LAYER_TYPE_SOFTWARE, null); + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java index 73ded852f..cd1082ba5 100644 --- a/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java +++ b/app/src/main/java/fr/free/nrw/commons/location/LocationServiceManager.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.location; import android.Manifest; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.pm.PackageManager; @@ -10,9 +11,10 @@ import android.location.LocationManager; import android.os.Bundle; import android.support.v4.app.ActivityCompat; import android.support.v4.content.ContextCompat; -import android.util.Log; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import timber.log.Timber; @@ -29,6 +31,7 @@ public class LocationServiceManager implements LocationListener { private Location lastLocation; private final List locationListeners = new CopyOnWriteArrayList<>(); private boolean isLocationManagerRegistered = false; + private Set locationExplanationDisplayed = new HashSet<>(); /** * Constructs a new instance of LocationServiceManager. @@ -51,7 +54,6 @@ public class LocationServiceManager implements LocationListener { /** * Returns whether the location permission is granted. - * * @return true if the location permission is granted */ public boolean isLocationPermissionGranted() { @@ -73,10 +75,23 @@ public class LocationServiceManager implements LocationListener { LOCATION_REQUEST); } + /** + * The permission explanation dialog box is now displayed just once for a particular activity. We are subscribing + * to updates from multiple providers so its important to show the dialog just once. Otherwise it will be displayed + * once for every provider, which in our case currently is 2. + * @param activity + * @return + */ public boolean isPermissionExplanationRequired(Activity activity) { - return !activity.isFinishing() && - ActivityCompat.shouldShowRequestPermissionRationale(activity, - Manifest.permission.ACCESS_FINE_LOCATION); + if (activity.isFinishing()) { + return false; + } + boolean showRequestPermissionRationale = ActivityCompat.shouldShowRequestPermissionRationale(activity, Manifest.permission.ACCESS_FINE_LOCATION); + if (showRequestPermissionRationale && !locationExplanationDisplayed.contains(activity)) { + locationExplanationDisplayed.add(activity); + return true; + } + return false; } /** @@ -84,8 +99,9 @@ public class LocationServiceManager implements LocationListener { * (e.g. when Location permission just granted) * @return last known LatLng */ + @SuppressLint("MissingPermission") public LatLng getLKL() { - if (ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { + if (isLocationPermissionGranted()) { Location lastKL = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); if (lastKL == null) { lastKL = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); @@ -107,9 +123,10 @@ public class LocationServiceManager implements LocationListener { * Registers a LocationManager to listen for current location. */ public void registerLocationManager() { - if (!isLocationManagerRegistered) + if (!isLocationManagerRegistered) { isLocationManagerRegistered = requestLocationUpdatesFromProvider(LocationManager.NETWORK_PROVIDER) && requestLocationUpdatesFromProvider(LocationManager.GPS_PROVIDER); + } } /** @@ -142,7 +159,7 @@ public class LocationServiceManager implements LocationListener { * @return LOCATION_SIGNIFICANTLY_CHANGED if location changed significantly * LOCATION_SLIGHTLY_CHANGED if location changed slightly */ - protected LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { + private LocationChangeType isBetterLocation(Location location, Location currentBestLocation) { if (currentBestLocation == null) { // A new location is always better than no location @@ -267,6 +284,7 @@ public class LocationServiceManager implements LocationListener { LOCATION_SIGNIFICANTLY_CHANGED, //Went out of borders of nearby markers LOCATION_SLIGHTLY_CHANGED, //User might be walking or driving LOCATION_NOT_CHANGED, - PERMISSION_JUST_GRANTED + PERMISSION_JUST_GRANTED, + MAP_UPDATED } } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java index 9614c4f00..e7a21ed09 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.java @@ -42,6 +42,7 @@ import fr.free.nrw.commons.MediaDataExtractor; import fr.free.nrw.commons.MediaWikiImageView; import fr.free.nrw.commons.PageTitle; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.category.CategoryDetailsActivity; import fr.free.nrw.commons.delete.DeleteTask; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.location.LatLng; @@ -56,16 +57,16 @@ import static android.widget.Toast.LENGTH_SHORT; public class MediaDetailFragment extends CommonsDaggerSupportFragment { private boolean editable; - private boolean isFeaturedMedia; + private boolean isCategoryImage; private MediaDetailPagerFragment.MediaDetailProvider detailProvider; private int index; - public static MediaDetailFragment forMedia(int index, boolean editable, boolean isFeaturedMedia) { + public static MediaDetailFragment forMedia(int index, boolean editable, boolean isCategoryImage) { MediaDetailFragment mf = new MediaDetailFragment(); Bundle state = new Bundle(); state.putBoolean("editable", editable); - state.putBoolean("isFeaturedMedia", isFeaturedMedia); + state.putBoolean("isCategoryImage", isCategoryImage); state.putInt("index", index); state.putInt("listIndex", 0); state.putInt("listTop", 0); @@ -128,7 +129,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { super.onSaveInstanceState(outState); outState.putInt("index", index); outState.putBoolean("editable", editable); - outState.putBoolean("isFeaturedMedia", isFeaturedMedia); + outState.putBoolean("isCategoryImage", isCategoryImage); getScrollPosition(); outState.putInt("listTop", initialListTop); @@ -144,12 +145,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (savedInstanceState != null) { editable = savedInstanceState.getBoolean("editable"); - isFeaturedMedia = savedInstanceState.getBoolean("isFeaturedMedia"); + isCategoryImage = savedInstanceState.getBoolean("isCategoryImage"); index = savedInstanceState.getInt("index"); initialListTop = savedInstanceState.getInt("listTop"); } else { editable = getArguments().getBoolean("editable"); - isFeaturedMedia = getArguments().getBoolean("isFeaturedMedia"); + isCategoryImage = getArguments().getBoolean("isCategoryImage"); index = getArguments().getInt("index"); initialListTop = 0; } @@ -161,7 +162,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { ButterKnife.bind(this,view); - if (isFeaturedMedia){ + if (isCategoryImage){ authorLayout.setVisibility(VISIBLE); } else { authorLayout.setVisibility(GONE); @@ -245,6 +246,10 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { @Override protected Boolean doInBackground(Void... voids) { + // Local files have no filename yet + if(media.getFilename() == null) { + return Boolean.FALSE; + } try { extractor.fetch(media.getFilename(), licenseList); return Boolean.TRUE; @@ -328,7 +333,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (!TextUtils.isEmpty(licenseLink(media))) { openWebBrowser(licenseLink(media)); } else { - if(isFeaturedMedia) { + if(isCategoryImage) { Timber.d("Unable to fetch license URL for %s", media.getLicense()); } else { Toast toast = Toast.makeText(getContext(), getString(R.string.null_url), Toast.LENGTH_SHORT); @@ -426,17 +431,11 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { textView.setText(catName); if (categoriesLoaded && categoriesPresent) { textView.setOnClickListener(view -> { + // Open Category Details page String selectedCategoryTitle = "Category:" + catName; - Intent viewIntent = new Intent(); - viewIntent.setAction(Intent.ACTION_VIEW); - viewIntent.setData(new PageTitle(selectedCategoryTitle).getCanonicalUri()); - //check if web browser available - if (viewIntent.resolveActivity(getActivity().getPackageManager()) != null) { - startActivity(viewIntent); - } else { - Toast toast = Toast.makeText(getContext(), getString(R.string.no_web_browser), LENGTH_SHORT); - toast.show(); - } + Intent intent = new Intent(getContext(), CategoryDetailsActivity.class); + intent.putExtra("categoryName", selectedCategoryTitle); + getContext().startActivity(intent); }); } return item; @@ -503,8 +502,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment { if (media.getRequestedDeletion()){ delete.setVisibility(GONE); nominatedForDeletion.setVisibility(VISIBLE); - } - else{ + } else if (!isCategoryImage) { delete.setVisibility(VISIBLE); nominatedForDeletion.setVisibility(GONE); } diff --git a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java index 62d1261cf..fc58ae990 100644 --- a/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/media/MediaDetailPagerFragment.java @@ -9,6 +9,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; +import android.os.Handler; import android.support.design.widget.Snackbar; import android.support.v4.app.ActivityCompat; import android.support.v4.app.Fragment; @@ -38,6 +39,8 @@ import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.di.CommonsDaggerSupportFragment; import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ImageUtils; +import timber.log.Timber; import static android.Manifest.permission.READ_EXTERNAL_STORAGE; import static android.content.Context.DOWNLOAD_SERVICE; @@ -120,7 +123,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple Media m = provider.getMediaAtPosition(pager.getCurrentItem()); switch (item.getItemId()) { case R.id.menu_share_current_image: - // Share - intent set in onCreateOptionsMenu, around line 252 + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, m.getDisplayTitle() + " \n" + m.getFilePageTitle().getCanonicalUri()); + startActivity(Intent.createChooser(shareIntent, "Share image via...")); return true; case R.id.menu_browser_current_image: // View in browser @@ -140,6 +146,10 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple // Download downloadMedia(m); return true; + case R.id.menu_set_as_wallpaper: + // Set wallpaper + setWallpaper(m); + return true; case R.id.menu_retry_current_image: // Retry ((ContributionsActivity) getActivity()).retryUpload(pager.getCurrentItem()); @@ -155,6 +165,19 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple } } + /** + * 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())); + } + /** * Start the media file downloading to the local SD card/storage. * The file can then be opened in Gallery or other apps. @@ -216,19 +239,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple menu.findItem(R.id.menu_share_current_image).setEnabled(true).setVisible(true); menu.findItem(R.id.menu_download_current_image).setEnabled(true).setVisible(true); - // Set ShareActionProvider Intent - ShareActionProvider mShareActionProvider = (ShareActionProvider) MenuItemCompat.getActionProvider(menu.findItem(R.id.menu_share_current_image)); - // On some phones null is returned for some reason: - // https://github.com/commons-app/apps-android-commons/issues/413 - if (mShareActionProvider != null) { - Intent shareIntent = new Intent(Intent.ACTION_SEND); - shareIntent.setType("text/plain"); - shareIntent.putExtra(Intent.EXTRA_TEXT, - m.getDisplayTitle() + " \n" + m.getFilePageTitle().getCanonicalUri()); - mShareActionProvider.setShareIntent(shareIntent); - } - - if (m instanceof Contribution) { + if (m instanceof Contribution ) { Contribution c = (Contribution) m; switch (c.getState()) { case Contribution.STATE_FAILED: @@ -257,7 +268,8 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple } public void showImage(int i) { - pager.setCurrentItem(i); + Handler handler = new Handler(); + handler.postDelayed(() -> pager.setCurrentItem(i), 10); } @Override diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java index 6629d0933..6088b774b 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApi.java @@ -23,8 +23,11 @@ import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; import org.apache.http.params.BasicHttpParams; import org.apache.http.params.CoreProtocolPNames; import org.apache.http.util.EntityUtils; +import org.json.JSONObject; import org.mediawiki.api.ApiResult; import org.mediawiki.api.MWApi; +import org.w3c.dom.Element; +import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.io.IOException; @@ -33,10 +36,12 @@ import java.net.URL; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.TimeZone; import java.util.concurrent.Callable; import fr.free.nrw.commons.BuildConfig; @@ -49,6 +54,10 @@ import fr.free.nrw.commons.notification.NotificationUtils; import in.yuvi.http.fluent.Http; import io.reactivex.Observable; import io.reactivex.Single; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; import timber.log.Timber; import static fr.free.nrw.commons.utils.ContinueUtils.getQueryContinue; @@ -62,6 +71,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { private static final String THUMB_SIZE = "640"; private AbstractHttpClient httpClient; private MWApi api; + private MWApi wikidataApi; private Context context; private SharedPreferences defaultPreferences; private SharedPreferences categoryPreferences; @@ -69,6 +79,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { public ApacheHttpClientMediaWikiApi(Context context, String apiURL, + String wikidatApiURL, SharedPreferences defaultPreferences, SharedPreferences categoryPreferences, Gson gson) { @@ -82,6 +93,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { params.setParameter(CoreProtocolPNames.USER_AGENT, getUserAgent()); httpClient = new DefaultHttpClient(cm, params); api = new MWApi(apiURL, httpClient); + wikidataApi = new MWApi(wikidatApiURL, httpClient); this.defaultPreferences = defaultPreferences; this.categoryPreferences = categoryPreferences; this.gson = gson; @@ -206,6 +218,15 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return api.getEditToken(); } + @Override + public String getCentralAuthToken() throws IOException { + String centralAuthToken = api.action("centralauthtoken") + .get() + .getString("/api/centralauthtoken/@centralauthtoken"); + Timber.d("MediaWiki Central auth token is %s", centralAuthToken); + return centralAuthToken; + } + @Override public boolean fileExistsWithName(String fileName) throws IOException { return api.action("query") @@ -351,6 +372,98 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { }).flatMapObservable(Observable::fromIterable); } + /** + * Get the edit token for making wiki data edits + * https://www.mediawiki.org/wiki/API:Tokens + * @return + * @throws IOException + */ + private String getWikidataEditToken() throws IOException { + return wikidataApi.getEditToken(); + } + + @Override + public String getWikidataCsrfToken() throws IOException { + String wikidataCsrfToken = wikidataApi.action("query") + .param("action", "query") + .param("centralauthtoken", getCentralAuthToken()) + .param("meta", "tokens") + .post() + .getString("/api/query/tokens/@csrftoken"); + Timber.d("Wikidata csrf token is %s", wikidataCsrfToken); + return wikidataCsrfToken; + } + + /** + * Creates a new claim using the wikidata API + * https://www.mediawiki.org/wiki/Wikibase/API + * @param entityId the wikidata entity to be edited + * @param property the property to be edited, for eg P18 for images + * @param snaktype the type of value stored for that property + * @param value the actual value to be stored for the property, for eg filename in case of P18 + * @return returns revisionId if the claim is successfully created else returns null + * @throws IOException + */ + @Nullable + @Override + public String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException { + Timber.d("Filename is %s", value); + ApiResult result = wikidataApi.action("wbcreateclaim") + .param("entity", entityId) + .param("centralauthtoken", getCentralAuthToken()) + .param("token", getWikidataCsrfToken()) + .param("snaktype", snaktype) + .param("property", property) + .param("value", value) + .post(); + + if (result == null || result.getNode("api") == null) { + return null; + } + + Node node = result.getNode("api").getDocument(); + Element element = (Element) node; + + if (element != null && element.getAttribute("success").equals("1")) { + return result.getString("api/pageinfo/@lastrevid"); + } else { + Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info")); + } + return null; + } + + /** + * Adds the wikimedia-commons-app tag to the edits made on wikidata + * @param revisionId + * @return + * @throws IOException + */ + @Nullable + @Override + public boolean addWikidataEditTag(String revisionId) throws IOException { + ApiResult result = wikidataApi.action("tag") + .param("revid", revisionId) + .param("centralauthtoken", getCentralAuthToken()) + .param("token", getWikidataCsrfToken()) + .param("add", "wikimedia-commons-app") + .param("reason", "Add tag for edits made using Android Commons app") + .post(); + + if (result == null || result.getNode("api") == null) { + return false; + } + + Node node = result.getNode("api").getDocument(); + Element element = (Element) node; + + if (element != null && element.getAttribute("status").equals("success")) { + return true; + } else { + Timber.e(result.getString("api/error/@code") + " " + result.getString("api/error/@info")); + } + return false; + } + @Override @NonNull public Observable searchTitles(String title, int searchCatsLimit) { @@ -444,8 +557,8 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { .param("notprop", "list") .param("format", "xml") .param("meta", "notifications") -// .param("meta", "notifications") .param("notformat", "model") + .param("notwikis", "wikidatawiki|commonswiki|enwiki") .get() .getNode("/api/query/notifications/list"); } catch (IOException e) { @@ -464,32 +577,26 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { } /** - * The method takes categoryName as input and returns a List of Media objects - * It uses the generator query API to get the images in a category, 10 at a time. + * The method takes categoryName as input and returns a List of Subcategories + * It uses the generator query API to get the subcategories in a category, 500 at a time. * Uses the query continue values for fetching paginated responses * @param categoryName Category name as defined on commons * @return */ @Override @NonNull - public List getCategoryImages(String categoryName) { + public List getSubCategoryList(String categoryName) { ApiResult apiResult = null; try { MWApi.RequestBuilder requestBuilder = api.action("query") .param("generator", "categorymembers") .param("format", "xml") - .param("gcmtype", "file") + .param("gcmtype","subcat") .param("gcmtitle", categoryName) - .param("prop", "imageinfo") - .param("gcmlimit", "10") + .param("prop", "info") + .param("gcmlimit", "500") .param("iiprop", "url|extmetadata"); - QueryContinue queryContinueValues = getQueryContinueValues(categoryName); - if (queryContinueValues != null) { - requestBuilder.param("continue", queryContinueValues.getContinueParam()); - requestBuilder.param("gcmcontinue", queryContinueValues.getGcmContinueParam()); - } - apiResult = requestBuilder.get(); } catch (IOException e) { Timber.e("Failed to obtain searchCategories", e); @@ -507,13 +614,183 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { return new ArrayList<>(); } - QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument()); - setQueryContinueValues(categoryName, queryContinue); + NodeList childNodes = categoryImagesNode.getDocument().getChildNodes(); + return CategoryImageUtils.getSubCategoryList(childNodes); + } + + /** + * The method takes categoryName as input and returns a List of parent categories + * It uses the generator query API to get the parent categories of a category, 500 at a time. + * @param categoryName Category name as defined on commons + * @return + */ + @Override + @NonNull + public List getParentCategoryList(String categoryName) { + ApiResult apiResult = null; + try { + MWApi.RequestBuilder requestBuilder = api.action("query") + .param("generator", "categories") + .param("format", "xml") + .param("titles", categoryName) + .param("prop", "info") + .param("cllimit", "500") + .param("iiprop", "url|extmetadata"); + + apiResult = requestBuilder.get(); + } catch (IOException e) { + Timber.e("Failed to obtain parent Categories", e); + } + + if (apiResult == null) { + return new ArrayList<>(); + } + + ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + if (categoryImagesNode == null + || categoryImagesNode.getDocument() == null + || categoryImagesNode.getDocument().getChildNodes() == null + || categoryImagesNode.getDocument().getChildNodes().getLength() == 0) { + return new ArrayList<>(); + } + + NodeList childNodes = categoryImagesNode.getDocument().getChildNodes(); + return CategoryImageUtils.getSubCategoryList(childNodes); + } + + + /** + * The method takes categoryName as input and returns a List of Media objects + * It uses the generator query API to get the images in a category, 10 at a time. + * Uses the query continue values for fetching paginated responses + * @param categoryName Category name as defined on commons + * @return + */ + @Override + @NonNull + public List getCategoryImages(String categoryName) { + ApiResult apiResult = null; + try { + MWApi.RequestBuilder requestBuilder = api.action("query") + .param("generator", "categorymembers") + .param("format", "xml") + .param("gcmtype", "file") + .param("gcmtitle", categoryName) + .param("gcmsort", "timestamp")//property to sort by;timestamp + .param("gcmdir", "desc")//in which direction to sort;descending + .param("prop", "imageinfo") + .param("gcmlimit", "10") + .param("iiprop", "url|extmetadata"); + + QueryContinue queryContinueValues = getQueryContinueValues(categoryName); + if (queryContinueValues != null) { + requestBuilder.param("continue", queryContinueValues.getContinueParam()); + requestBuilder.param("gcmcontinue", queryContinueValues.getGcmContinueParam()); + } + apiResult = requestBuilder.get(); + } catch (IOException e) { + Timber.e("Failed to obtain searchCategories", e); + } + + if (apiResult == null) { + return new ArrayList<>(); + } + + ApiResult categoryImagesNode = apiResult.getNode("/api/query/pages"); + if (categoryImagesNode == null + || categoryImagesNode.getDocument() == null + || categoryImagesNode.getDocument().getChildNodes() == null + || categoryImagesNode.getDocument().getChildNodes().getLength() == 0) { + return new ArrayList<>(); + } + + if (apiResult.getNode("/api/continue").getDocument()==null){ + setQueryContinueValues(categoryName, null); + }else { + QueryContinue queryContinue = getQueryContinue(apiResult.getNode("/api/continue").getDocument()); + setQueryContinueValues(categoryName, queryContinue); + } NodeList childNodes = categoryImagesNode.getDocument().getChildNodes(); return CategoryImageUtils.getMediaList(childNodes); } + /** + * This method takes search keyword as input and returns a list of Media objects filtered using search query + * It uses the generator query API to get the images searched using a query, 25 at a time. + * @param query keyword to search images on commons + * @return + */ + @Override + @NonNull + public List searchImages(String query, int offset) { + List imageNodes = null; + try { + imageNodes = api.action("query") + .param("format", "xml") + .param("list", "search") + .param("srwhat", "text") + .param("srnamespace", "6") + .param("srlimit", "25") + .param("sroffset",offset) + .param("srsearch", query) + .get() + .getNodes("/api/query/search/p/@title"); + } catch (IOException e) { + Timber.e("Failed to obtain searchImages", e); + } + + if (imageNodes == null) { + return new ArrayList(); + } + + List images = new ArrayList<>(); + for (ApiResult imageNode : imageNodes) { + String imgName = imageNode.getDocument().getTextContent(); + images.add(new Media(imgName)); + } + + return images; + } + + /** + * This method takes search keyword as input and returns a list of categories objects filtered using search query + * It uses the generator query API to get the categories searched using a query, 25 at a time. + * @param query keyword to search categories on commons + * @return + */ + @Override + @NonNull + public List searchCategory(String query, int offset) { + List categoryNodes = null; + try { + categoryNodes = api.action("query") + .param("format", "xml") + .param("list", "search") + .param("srwhat", "text") + .param("srnamespace", "14") + .param("srlimit", "25") + .param("sroffset",offset) + .param("srsearch", query) + .get() + .getNodes("/api/query/search/p/@title"); + } catch (IOException e) { + Timber.e("Failed to obtain searchCategories", e); + } + + if (categoryNodes == null) { + return new ArrayList(); + } + + List categories = new ArrayList<>(); + for (ApiResult categoryNode : categoryNodes) { + String catName = categoryNode.getDocument().getTextContent(); + categories.add(catName); + } + return categories; + } + + /** * For APIs that return paginated responses, MediaWiki APIs uses the QueryContinue to facilitate fetching of subsequent pages * https://www.mediawiki.org/wiki/API:Raw_query_continue @@ -586,6 +863,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { String resultStatus = result.getString("/api/upload/@result"); if (!resultStatus.equals("Success")) { String errorCode = result.getString("/api/error/@code"); + Timber.e(errorCode); return new UploadResult(resultStatus, errorCode); } else { Date dateUploaded = parseMWDate(result.getString("/api/upload/imageinfo/@timestamp")); @@ -600,7 +878,7 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { @NonNull public Single getUploadCount(String userName) { final String uploadCountUrlTemplate = - wikiMediaToolforgeUrl + "urbanecmbot/uploadsbyuser/uploadsbyuser.py"; + wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/uploadsbyuser.py"; return Single.fromCallable(() -> { String url = String.format( @@ -615,12 +893,108 @@ public class ApacheHttpClientMediaWikiApi implements MediaWikiApi { }); } + /** + * Checks to see if a user is currently blocked from Commons + * @return whether or not the user is blocked from Commons + */ + @Override + public boolean isUserBlockedFromCommons() { + boolean userBlocked = false; + try { + ApiResult result = api.action("query") + .param("action", "query") + .param("format", "xml") + .param("meta", "userinfo") + .param("uiprop", "blockinfo") + .get(); + if (result != null) { + String blockEnd = result.getString("/api/query/userinfo/@blockexpiry"); + if (blockEnd.equals("infinite")) { + userBlocked = true; + } else if (!blockEnd.isEmpty()) { + Date endDate = parseMWDate(blockEnd); + Date current = new Date(); + userBlocked = endDate.after(current); + } + + } + } catch (Exception e) { + e.printStackTrace(); + } + + return userBlocked; + } + + /** + * This takes userName as input, which is then used to fetch the feedback/achievements + * statistics using OkHttp and JavaRx. This function return JSONObject + * @param userName + * @return + */ + @NonNull + @Override + public Single getAchievements(String userName) { + final String fetchAchievementUrlTemplate = + wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py"; + return Single.fromCallable(() -> { + String url = String.format( + Locale.ENGLISH, + fetchAchievementUrlTemplate, + new PageTitle(userName).getText()); + HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); + urlBuilder.addQueryParameter("user", userName); + Log.i("url", urlBuilder.toString()); + Request request = new Request.Builder() + .url(urlBuilder.toString()) + .build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + String jsonData = response.body().string(); + JSONObject jsonObject = new JSONObject(jsonData); + return jsonObject; + }); + + } + + /** + * This takes userName as input, which is then used to fetch the no of images deleted + * using OkHttp and JavaRx. This function return JSONObject + * @param userName + * @return + */ + @NonNull + @Override + public Single getRevertCount(String userName){ + final String fetchRevertCountUrlTemplate = + wikiMediaToolforgeUrl + "urbanecmbot/commonsmisc/feedback.py"; + return Single.fromCallable(() -> { + String url = String.format( + Locale.ENGLISH, + fetchRevertCountUrlTemplate, + new PageTitle(userName).getText()); + HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder(); + urlBuilder.addQueryParameter("user", userName); + urlBuilder.addQueryParameter("fetch","deletedUploads"); + Log.i("url", urlBuilder.toString()); + Request request = new Request.Builder() + .url(urlBuilder.toString()) + .build(); + OkHttpClient client = new OkHttpClient(); + Response response = client.newCall(request).execute(); + String jsonData = response.body().string(); + JSONObject jsonRevertObject = new JSONObject(jsonData); + return jsonRevertObject; + }); + } + private Date parseMWDate(String mwDate) { SimpleDateFormat isoFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.ENGLISH); // Assuming MW always gives me UTC + isoFormat.setTimeZone(TimeZone.getTimeZone("UTC")); try { return isoFormat.parse(mwDate); } catch (ParseException e) { throw new RuntimeException(e); } } + } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java new file mode 100644 index 000000000..031796745 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/CategoryApi.java @@ -0,0 +1,101 @@ +package fr.free.nrw.commons.mwapi; + +import com.google.gson.Gson; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.mwapi.model.ApiResponse; +import fr.free.nrw.commons.mwapi.model.Page; +import fr.free.nrw.commons.mwapi.model.PageCategory; +import io.reactivex.Single; +import okhttp3.HttpUrl; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import okhttp3.ResponseBody; +import timber.log.Timber; + +/** + * Uses the OkHttp library to implement calls to the Commons MediaWiki API to match GPS coordinates + * with nearby Commons categories. Parses the results using GSON to obtain a list of relevant + * categories. Note: that caller is responsible for executing the request() method on a background + * thread. + */ +public class CategoryApi { + + private final OkHttpClient okHttpClient; + private final HttpUrl mwUrl; + private final Gson gson; + + @Inject + public CategoryApi(OkHttpClient okHttpClient, Gson gson, + @Named("commons_mediawiki_url") HttpUrl mwUrl) { + this.okHttpClient = okHttpClient; + this.mwUrl = mwUrl; + this.gson = gson; + } + + public Single> request(String coords) { + return Single.fromCallable(() -> { + HttpUrl apiUrl = buildUrl(coords); + Timber.d("URL: %s", apiUrl.toString()); + + Request request = new Request.Builder().get().url(apiUrl).build(); + Response response = okHttpClient.newCall(request).execute(); + ResponseBody body = response.body(); + if (body == null) { + return Collections.emptyList(); + } + + ApiResponse apiResponse = gson.fromJson(body.charStream(), ApiResponse.class); + Set categories = new LinkedHashSet<>(); + if (apiResponse != null && apiResponse.hasPages()) { + for (Page page : apiResponse.query.pages) { + for (PageCategory category : page.getCategories()) { + categories.add(category.withoutPrefix()); + } + } + } + return new ArrayList<>(categories); + }); + } + + /** + * Builds URL with image coords for MediaWiki API calls + * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 + * + * @param coords Coordinates to build query with + * @return URL for API query + */ + private HttpUrl buildUrl(String coords) { + return mwUrl.newBuilder() + .addPathSegment("w") + .addPathSegment("api.php") + .addQueryParameter("action", "query") + .addQueryParameter("prop", "categories|coordinates|pageprops") + .addQueryParameter("format", "json") + .addQueryParameter("clshow", "!hidden") + .addQueryParameter("coprop", "type|name|dim|country|region|globe") + .addQueryParameter("codistancefrompoint", coords) + .addQueryParameter("generator", "geosearch") + .addQueryParameter("ggscoord", coords) + .addQueryParameter("ggsradius", "10000") + .addQueryParameter("ggslimit", "10") + .addQueryParameter("ggsnamespace", "6") + .addQueryParameter("ggsprop", "type|name|dim|country|region|globe") + .addQueryParameter("ggsprimary", "all") + .addQueryParameter("formatversion", "2") + .build(); + } + +} + + + diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java index c0bd2fd87..78c990372 100644 --- a/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/MediaWikiApi.java @@ -3,6 +3,8 @@ package fr.free.nrw.commons.mwapi; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import org.json.JSONObject; + import java.io.IOException; import java.io.InputStream; import java.util.List; @@ -27,6 +29,10 @@ public interface MediaWikiApi { String getEditToken() throws IOException; + String getWikidataCsrfToken() throws IOException; + + String getCentralAuthToken() throws IOException; + boolean fileExistsWithName(String fileName) throws IOException; boolean pageExists(String pageName) throws IOException; @@ -37,6 +43,16 @@ public interface MediaWikiApi { List getCategoryImages(String categoryName); + List getSubCategoryList(String categoryName); + + List getParentCategoryList(String categoryName); + + @NonNull + List searchImages(String title, int offset); + + @NonNull + List searchCategory(String title, int offset); + @NonNull UploadResult uploadFile(String filename, InputStream file, long dataLength, String pageContents, String editSummary, ProgressListener progressListener) throws IOException; @@ -49,6 +65,12 @@ public interface MediaWikiApi { @Nullable String appendEdit(String editToken, String processedPageContent, String filename, String summary) throws IOException; + @Nullable + String wikidatCreateClaim(String entityId, String property, String snaktype, String value) throws IOException; + + @Nullable + boolean addWikidataEditTag(String revisionId) throws IOException; + @NonNull MediaResult fetchMediaByFilename(String filename) throws IOException; @@ -75,6 +97,14 @@ public interface MediaWikiApi { @NonNull Single getUploadCount(String userName); + boolean isUserBlockedFromCommons(); + + @NonNull + Single getAchievements(String userName); + + @NonNull + Single getRevertCount(String userName); + interface ProgressListener { void onProgress(long transferred, long total); } diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/ApiResponse.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/ApiResponse.java new file mode 100644 index 000000000..7feb90251 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/ApiResponse.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi.model; + +public class ApiResponse { + public Query query; + + public ApiResponse() { + } + + public boolean hasPages() { + return query != null && query.pages != null; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/Page.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Page.java new file mode 100644 index 000000000..d01ba658f --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Page.java @@ -0,0 +1,17 @@ +package fr.free.nrw.commons.mwapi.model; + +import android.support.annotation.NonNull; + +public class Page { + public String title; + public PageCategory[] categories; + public PageCategory category; + + public Page() { + } + + @NonNull + public PageCategory[] getCategories() { + return categories != null ? categories : new PageCategory[0]; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/PageCategory.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/PageCategory.java new file mode 100644 index 000000000..be4b9fd79 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/PageCategory.java @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi.model; + +public class PageCategory { + public String title; + + public PageCategory() { + } + + public String withoutPrefix() { + return title != null ? title.replace("Category:", "") : ""; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/mwapi/model/Query.java b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Query.java new file mode 100644 index 000000000..b87f97cc3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/mwapi/model/Query.java @@ -0,0 +1,10 @@ +package fr.free.nrw.commons.mwapi.model; + +public class Query { + public Page[] pages; + + public Query() { + pages = new Page[0]; + } + +} diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java index 7ab427b2d..9a12c6d39 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/DirectUpload.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.nearby; -import android.content.SharedPreferences; import android.os.Build; import android.support.v4.app.Fragment; import android.support.v4.content.ContextCompat; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java index 7649a2325..d49bf0147 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyActivity.java @@ -38,11 +38,13 @@ import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.location.LatLng; import fr.free.nrw.commons.location.LocationServiceManager; +import fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType; import fr.free.nrw.commons.location.LocationUpdateListener; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.UriSerializer; import fr.free.nrw.commons.utils.ViewUtil; +import fr.free.nrw.commons.wikidata.WikidataEditListener; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; @@ -51,8 +53,12 @@ import timber.log.Timber; import uk.co.deanwild.materialshowcaseview.IShowcaseListener; import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.*; +import static fr.free.nrw.commons.location.LocationServiceManager.LocationChangeType.MAP_UPDATED; -public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener { + +public class NearbyActivity extends NavigationBaseActivity implements LocationUpdateListener, + WikidataEditListener.WikidataP18EditListener { private static final int LOCATION_REQUEST = 1; @@ -72,6 +78,8 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp LocationServiceManager locationManager; @Inject NearbyController nearbyController; + @Inject WikidataEditListener wikidataEditListener; + @Inject @Named("application_preferences") SharedPreferences applicationPrefs; private LatLng curLatLng; @@ -106,6 +114,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp initBottomSheetBehaviour(); initDrawer(); + wikidataEditListener.setAuthenticationStateListener(this); } private void resumeFragment() { @@ -215,7 +224,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp //Still need to check if GPS is enabled checkGps(); lastKnownLocation = locationManager.getLKL(); - refreshView(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED); + refreshView(PERMISSION_JUST_GRANTED); } else { //If permission not granted, go to page that says Nearby Places cannot be displayed hideProgressBar(); @@ -275,7 +284,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp private void checkLocationPermission() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (locationManager.isLocationPermissionGranted()) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } else { // Should we show an explanation? if (locationManager.isPermissionExplanationRequired(this)) { @@ -301,7 +310,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } } else { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -310,7 +319,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp super.onActivityResult(requestCode, resultCode, data); if (requestCode == 1) { Timber.d("User is back from Settings page"); - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } } @@ -318,7 +327,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp protected void onStart() { super.onStart(); locationManager.addLocationListener(this); - locationManager.registerLocationManager(); + registerLocationUpdates(); } @Override @@ -369,8 +378,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override public void onReceive(Context context, Intent intent) { if (NetworkUtils.isInternetConnectionEstablished(NearbyActivity.this)) { - refreshView(LocationServiceManager - .LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } else { ViewUtil.showLongToast(NearbyActivity.this, getString(R.string.no_internet)); } @@ -386,7 +394,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp * * @param locationChangeType defines if location shanged significantly or slightly */ - private void refreshView(LocationServiceManager.LocationChangeType locationChangeType) { + private void refreshView(LocationChangeType locationChangeType) { if (lockNearbyView) { return; } @@ -396,15 +404,16 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } - locationManager.registerLocationManager(); + registerLocationUpdates(); LatLng lastLocation = locationManager.getLastLocation(); - if (curLatLng != null && curLatLng.equals(lastLocation)) { //refresh view only if location has changed + if (curLatLng != null && curLatLng.equals(lastLocation) + && !locationChangeType.equals(MAP_UPDATED)) { //refresh view only if location has changed return; } curLatLng = lastLocation; - if (locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) { + if (locationChangeType.equals(PERMISSION_JUST_GRANTED)) { curLatLng = lastKnownLocation; } @@ -413,8 +422,9 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp return; } - if (locationChangeType.equals(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED) - || locationChangeType.equals(LocationServiceManager.LocationChangeType.PERMISSION_JUST_GRANTED)) { + if (locationChangeType.equals(LOCATION_SIGNIFICANTLY_CHANGED) + || locationChangeType.equals(PERMISSION_JUST_GRANTED) + || locationChangeType.equals(MAP_UPDATED)) { progressBar.setVisibility(View.VISIBLE); //TODO: This hack inserts curLatLng before populatePlaces is called (see #1440). Ideally a proper fix should be found @@ -429,8 +439,14 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp .loadAttractionsFromLocation(curLatLng)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::populatePlaces); - } else if (locationChangeType.equals(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED)) { + .subscribe(this::populatePlaces, + throwable -> { + Timber.d(throwable); + showErrorMessage(getString(R.string.error_fetching_nearby_places)); + progressBar.setVisibility(View.GONE); + }); + } else if (locationChangeType + .equals(LOCATION_SLIGHTLY_CHANGED)) { Gson gson = new GsonBuilder() .registerTypeAdapter(Uri.class, new UriSerializer()) .create(); @@ -440,6 +456,39 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp } } + /** + * This method first checks if the location permissions has been granted and then register the location manager for updates. + */ + private void registerLocationUpdates() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (locationManager.isLocationPermissionGranted()) { + locationManager.registerLocationManager(); + } else { + // Should we show an explanation? + if (locationManager.isPermissionExplanationRequired(this)) { + new AlertDialog.Builder(this) + .setMessage(getString(R.string.location_permission_rationale_nearby)) + .setPositiveButton("OK", (dialog, which) -> { + requestLocationPermissions(); + dialog.dismiss(); + }) + .setNegativeButton("Cancel", (dialog, id) -> { + showLocationPermissionDeniedErrorDialog(); + dialog.cancel(); + }) + .create() + .show(); + + } else { + // No explanation needed, we can request the permission. + requestLocationPermissions(); + } + } + } else { + locationManager.registerLocationManager(); + } + } + private void populatePlaces(NearbyController.NearbyPlacesInfo nearbyPlacesInfo) { List placeList = nearbyPlacesInfo.placeList; LatLng[] boundaryCoordinates = nearbyPlacesInfo.boundaryCoordinates; @@ -453,7 +502,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp if (placeList.size() == 0) { ViewUtil.showSnackbar(findViewById(R.id.container), R.string.no_nearby); } - + bundle.putString("PlaceList", gsonPlaceList); //bundle.putString("CurLatLng", gsonCurLatLng); bundle.putString("BoundaryCoord", gsonBoundaryCoordinates); @@ -520,7 +569,7 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp locationManager.removeLocationListener(this); } else { lockNearbyView = false; - locationManager.registerLocationManager(); + registerLocationUpdates(); locationManager.addLocationListener(this); } } @@ -582,7 +631,12 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp .loadAttractionsFromLocation(curLatLng)) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe(this::populatePlaces); + .subscribe(this::populatePlaces, + throwable -> { + Timber.d(throwable); + showErrorMessage(getString(R.string.error_fetching_nearby_places)); + progressBar.setVisibility(View.GONE); + }); nearbyMapFragment.setBundleForUpdtes(bundle); nearbyMapFragment.updateMapSignificantly(); updateListFragment(); @@ -637,16 +691,24 @@ public class NearbyActivity extends NavigationBaseActivity implements LocationUp @Override public void onLocationChangedSignificantly(LatLng latLng) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SIGNIFICANTLY_CHANGED); + refreshView(LOCATION_SIGNIFICANTLY_CHANGED); } @Override public void onLocationChangedSlightly(LatLng latLng) { - refreshView(LocationServiceManager.LocationChangeType.LOCATION_SLIGHTLY_CHANGED); + refreshView(LOCATION_SLIGHTLY_CHANGED); } public void prepareViewsForSheetPosition(int bottomSheetState) { // TODO } + private void showErrorMessage(String message) { + ViewUtil.showLongToast(NearbyActivity.this, message); + } + + @Override + public void onWikidataEditSuccessful() { + refreshView(MAP_UPDATED); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java index 015d22135..bd042b4d7 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyController.java @@ -7,6 +7,7 @@ import android.support.graphics.drawable.VectorDrawableCompat; import com.mapbox.mapboxsdk.annotations.IconFactory; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -44,7 +45,7 @@ public class NearbyController { * @return NearbyPlacesInfo a variable holds Place list without distance information * and boundary coordinates of current Place List */ - public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) { + public NearbyPlacesInfo loadAttractionsFromLocation(LatLng curLatLng) throws IOException { Timber.d("Loading attractions near %s", curLatLng); NearbyPlacesInfo nearbyPlacesInfo = new NearbyPlacesInfo(); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java index 65bc271e7..8eed81208 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyListFragment.java @@ -35,6 +35,7 @@ import timber.log.Timber; import static android.app.Activity.RESULT_OK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; public class NearbyListFragment extends DaggerFragment { @@ -52,6 +53,11 @@ public class NearbyListFragment extends DaggerFragment { private RecyclerView recyclerView; private ContributionController controller; + + @Inject + @Named("direct_nearby_upload_prefs") + SharedPreferences directPrefs; + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -141,7 +147,7 @@ public class NearbyListFragment extends DaggerFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, true); + controller.handleImagePicked(requestCode, data, true, directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null)); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java index e674b958a..39a8461d1 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyMapFragment.java @@ -58,9 +58,7 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.LoginActivity; -import fr.free.nrw.commons.category.CategoryImagesActivity; import fr.free.nrw.commons.contributions.ContributionController; -import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.UriDeserializer; import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; @@ -68,7 +66,7 @@ import uk.co.deanwild.materialshowcaseview.MaterialShowcaseView; import static android.app.Activity.RESULT_OK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; -import static fr.free.nrw.commons.theme.NavigationBaseActivity.startActivityWithFlags; +import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; public class NearbyMapFragment extends DaggerFragment { @@ -750,7 +748,7 @@ public class NearbyMapFragment extends DaggerFragment { fabCamera.setOnClickListener(view -> { if (fabCamera.isShown()) { - Timber.d("Camera button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + Timber.d("Camera button tapped. Place: %s", place.toString()); storeSharedPrefs(); directUpload.initiateCameraUpload(); } @@ -758,7 +756,7 @@ public class NearbyMapFragment extends DaggerFragment { fabGallery.setOnClickListener(view -> { if (fabGallery.isShown()) { - Timber.d("Gallery button tapped. Image title: " + place.getName() + "Image desc: " + place.getLongDescription()); + Timber.d("Gallery button tapped. Place: %s", place.toString()); storeSharedPrefs(); directUpload.initiateGalleryUpload(); } @@ -770,6 +768,7 @@ public class NearbyMapFragment extends DaggerFragment { editor.putString("Title", place.getName()); editor.putString("Desc", place.getLongDescription()); editor.putString("Category", place.getCategory()); + editor.putString(WIKIDATA_ENTITY_ID_PREF, place.getWikiDataEntityId()); editor.apply(); } @@ -805,7 +804,7 @@ public class NearbyMapFragment extends DaggerFragment { if (resultCode == RESULT_OK) { Timber.d("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); - controller.handleImagePicked(requestCode, data, true); + controller.handleImagePicked(requestCode, data, true, directPrefs.getString(WIKIDATA_ENTITY_ID_PREF, null)); } else { Timber.e("OnActivityResult() parameters: Req code: %d Result code: %d Data: %s", requestCode, resultCode, data); diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java index a2f4b2352..c8d20f753 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/NearbyPlaces.java @@ -17,7 +17,7 @@ import java.util.regex.Pattern; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.location.LatLng; -import fr.free.nrw.commons.utils.FileUtils; +import fr.free.nrw.commons.upload.FileUtils; import timber.log.Timber; public class NearbyPlaces { @@ -40,10 +40,9 @@ public class NearbyPlaces { } } - List getFromWikidataQuery(LatLng curLatLng, String lang) { + List getFromWikidataQuery(LatLng curLatLng, String lang) throws IOException { List places = Collections.emptyList(); - try { // increase the radius gradually to find a satisfactory number of nearby places while (radius <= MAX_RADIUS) { places = getFromWikidataQuery(curLatLng, lang, radius); @@ -54,13 +53,6 @@ public class NearbyPlaces { radius *= RADIUS_MULTIPLIER; } } - } catch (IOException e) { - Timber.d(e.toString()); - // errors tend to be caused by too many results (and time out) - // try a small radius next time - Timber.d("back to initial radius: %f", radius); - radius = INITIAL_RADIUS; - } // make sure we will be able to send at least one request next time if (radius > MAX_RADIUS) { radius = MAX_RADIUS; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java index 9c5138245..285fcf83e 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Place.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Place.java @@ -3,12 +3,14 @@ package fr.free.nrw.commons.nearby; import android.graphics.Bitmap; import android.net.Uri; import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; import java.util.HashMap; import java.util.Map; import fr.free.nrw.commons.R; import fr.free.nrw.commons.location.LatLng; +import timber.log.Timber; public class Place { @@ -50,6 +52,22 @@ public class Place { this.distance = distance; } + /** + * Extracts the entity id from the wikidata link + * @return returns the entity id if wikidata link exists + */ + @Nullable + public String getWikiDataEntityId() { + if (!hasWikidataLink()) { + Timber.d("Wikidata entity ID is null for place with sitelink %s", siteLinks.toString()); + return null; + } + + String wikiDataLink = siteLinks.getWikidataLink().toString(); + Timber.d("Wikidata entity is %s", wikiDataLink); + return wikiDataLink.replace("http://www.wikidata.org/entity/", ""); + } + public boolean hasWikipediaLink() { return !(siteLinks == null || Uri.EMPTY.equals(siteLinks.getWikipediaLink())); } @@ -79,7 +97,18 @@ public class Place { @Override public String toString() { - return String.format("Place(%s@%s)", name, location); + return "Place{" + + "name='" + name + '\'' + + ", label='" + label + '\'' + + ", longDescription='" + longDescription + '\'' + + ", secondaryImageUrl='" + secondaryImageUrl + '\'' + + ", location='" + location + '\'' + + ", category='" + category + '\'' + + ", image='" + image + '\'' + + ", secondaryImage=" + secondaryImage + + ", distance='" + distance + '\'' + + ", siteLinks='" + siteLinks.toString() + '\'' + + '}'; } /** diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java index 8117d44d9..d88a9b729 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/PlaceRenderer.java @@ -26,7 +26,6 @@ import javax.inject.Named; import butterknife.BindView; import butterknife.ButterKnife; -import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.auth.LoginActivity; diff --git a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java index f4b29df77..8993db344 100644 --- a/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java +++ b/app/src/main/java/fr/free/nrw/commons/nearby/Sitelinks.java @@ -58,6 +58,15 @@ public class Sitelinks implements Parcelable { return Uri.parse(sanitisedStringUrl); } + @Override + public String toString() { + return "Sitelinks{" + + "wikipediaLink='" + wikipediaLink + '\'' + + ", commonsLink='" + commonsLink + '\'' + + ", wikidataLink='" + wikidataLink + '\'' + + '}'; + } + private Sitelinks(Sitelinks.Builder builder) { this.wikidataLink = builder.wikidataLink; this.wikipediaLink = builder.wikipediaLink; diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java index dc52f198a..b366c944a 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationActivity.java @@ -16,7 +16,6 @@ import android.widget.RelativeLayout; import com.pedrogomez.renderers.RVRendererAdapter; -import java.lang.ref.WeakReference; import java.util.Collections; import java.util.List; @@ -26,6 +25,7 @@ import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; +import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.theme.NavigationBaseActivity; import fr.free.nrw.commons.utils.NetworkUtils; import fr.free.nrw.commons.utils.ViewUtil; @@ -46,6 +46,8 @@ public class NotificationActivity extends NavigationBaseActivity { @BindView(R.id.container) RelativeLayout relativeLayout; @Inject NotificationController controller; + @Inject + MediaWikiApi mediaWikiApi; private static final String TAG_NOTIFICATION_WORKER_FRAGMENT = "NotificationWorkerFragment"; private NotificationWorkerFragment mNotificationWorkerFragment; @@ -81,7 +83,6 @@ public class NotificationActivity extends NavigationBaseActivity { } } - @SuppressLint("CheckResult") private void addNotifications() { Timber.d("Add notifications"); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java index 17a318e74..6dcfca35d 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationRenderer.java @@ -1,6 +1,7 @@ package fr.free.nrw.commons.notification; -import android.util.Log; +import android.graphics.drawable.PictureDrawable; +import android.text.Html; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -8,17 +9,23 @@ import android.widget.ImageView; import android.widget.TextView; import com.borjabravo.readmoretextview.ReadMoreTextView; +import com.bumptech.glide.RequestBuilder; import com.pedrogomez.renderers.Renderer; import butterknife.BindView; import butterknife.ButterKnife; import fr.free.nrw.commons.R; +import fr.free.nrw.commons.glide.SvgSoftwareLayerSetter; + +import static com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions.withCrossFade; /** * Created by root on 19.12.2017. */ public class NotificationRenderer extends Renderer { + private RequestBuilder requestBuilder; + @BindView(R.id.title) ReadMoreTextView title; @BindView(R.id.time) TextView time; @BindView(R.id.icon) ImageView icon; @@ -41,23 +48,32 @@ public class NotificationRenderer extends Renderer { protected View inflate(LayoutInflater layoutInflater, ViewGroup viewGroup) { View inflatedView = layoutInflater.inflate(R.layout.item_notification, viewGroup, false); ButterKnife.bind(this, inflatedView); + requestBuilder = GlideApp.with(inflatedView.getContext()) + .as(PictureDrawable.class) + .error(R.drawable.round_icon_unknown) + .transition(withCrossFade()) + .listener(new SvgSoftwareLayerSetter()); return inflatedView; } @Override public void render() { Notification notification = getContent(); - String str = notification.notificationText.trim(); - str = str.concat(" "); - title.setText(str); + setTitle(notification.notificationText); time.setText(notification.date); - switch (notification.notificationType) { - case THANK_YOU_EDIT: - icon.setImageResource(R.drawable.ic_edit_black_24dp); - break; - default: - icon.setImageResource(R.drawable.round_icon_unknown); - } + requestBuilder.load(notification.iconUrl).into(icon); + } + + /** + * Cleans up the notification text and sets it as the title + * Clean up is required to fix escaped HTML string and extra white spaces at the beginning of the notification + * @param notificationText + */ + private void setTitle(String notificationText) { + notificationText = notificationText.trim().replaceAll("(^\\h*)|(\\h*$)", ""); + notificationText = Html.fromHtml(notificationText).toString(); + notificationText = notificationText.concat(" "); + title.setText(notificationText); } public interface NotificationClicked{ diff --git a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java index 68c3add1c..e7c87d3f4 100644 --- a/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/notification/NotificationUtils.java @@ -16,12 +16,13 @@ import javax.annotation.Nullable; import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.R; -import static fr.free.nrw.commons.notification.NotificationType.THANK_YOU_EDIT; import static fr.free.nrw.commons.notification.NotificationType.UNKNOWN; public class NotificationUtils { private static final String COMMONS_WIKI = "commonswiki"; + private static final String WIKIDATA_WIKI = "wikidatawiki"; + private static final String WIKIPEDIA_WIKI = "enwiki"; public static boolean isCommonsNotification(Node document) { if (document == null || !document.hasAttributes()) { @@ -31,6 +32,32 @@ public class NotificationUtils { return COMMONS_WIKI.equals(element.getAttribute("wiki")); } + /** + * Returns true if the wiki attribute corresponds to wikidatawiki + * @param document + * @return + */ + public static boolean isWikidataNotification(Node document) { + if (document == null || !document.hasAttributes()) { + return false; + } + Element element = (Element) document; + return WIKIDATA_WIKI.equals(element.getAttribute("wiki")); + } + + /** + * Returns true if the wiki attribute corresponds to enwiki + * @param document + * @return + */ + public static boolean isWikipediaNotification(Node document) { + if (document == null || !document.hasAttributes()) { + return false; + } + Element element = (Element) document; + return WIKIPEDIA_WIKI.equals(element.getAttribute("wiki")); + } + public static NotificationType getNotificationType(Node document) { Element element = (Element) document; String type = element.getAttribute("type"); @@ -68,10 +95,17 @@ public class NotificationUtils { return notifications; } + /** + * Currently the app is interested in showing notifications just from the following three wikis: commons, wikidata, wikipedia + * This function returns true only if the notification belongs to any of the above wikis and is of a known notification type + * @param node + * @return + */ private static boolean isUsefulNotification(Node node) { - return isCommonsNotification(node) - && !getNotificationType(node).equals(UNKNOWN) - && !getNotificationType(node).equals(THANK_YOU_EDIT); + return (isCommonsNotification(node) + || isWikidataNotification(node) + || isWikipediaNotification(node)) + && !getNotificationType(node).equals(UNKNOWN); } public static boolean isBundledNotification(Node document) { @@ -97,7 +131,7 @@ public class NotificationUtils { switch (type) { case THANK_YOU_EDIT: - notificationText = context.getString(R.string.notifications_thank_you_edit); + notificationText = getThankYouEditDescription(document); break; case EDIT_USER_TALK: notificationText = getNotificationText(document); @@ -146,6 +180,16 @@ public class NotificationUtils { return body != null ? body.getTextContent() : ""; } + /** + * Gets the header node returned in the XML document to form the description for thank you edits + * @param document + * @return + */ + private static String getThankYouEditDescription(Node document) { + Node body = getNode(getModel(document), "header"); + return body != null ? body.getTextContent() : ""; + } + private static String getNotificationIconUrl(Node document) { String format = "%s%s"; Node iconUrl = getNode(getModel(document), "iconUrl"); diff --git a/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java b/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java new file mode 100644 index 000000000..5a1e8ae63 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/notification/SvgModule.java @@ -0,0 +1,35 @@ +package fr.free.nrw.commons.notification; + +import android.content.Context; +import android.graphics.drawable.PictureDrawable; +import android.support.annotation.NonNull; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Registry; +import com.bumptech.glide.annotation.GlideModule; +import com.bumptech.glide.module.AppGlideModule; +import com.caverock.androidsvg.SVG; + +import java.io.InputStream; + +import fr.free.nrw.commons.glide.SvgDecoder; +import fr.free.nrw.commons.glide.SvgDrawableTranscoder; + +/** + * Module for the SVG sample app. + */ +@GlideModule +public class SvgModule extends AppGlideModule { + @Override + public void registerComponents(@NonNull Context context, @NonNull Glide glide, + @NonNull Registry registry) { + registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder()) + .append(InputStream.class, SVG.class, new SvgDecoder()); + } + + // Disable manifest parsing to avoid adding similar modules twice. + @Override + public boolean isManifestParsingEnabled() { + return false; + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java index 6dd6056f7..d35170adf 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.java @@ -3,13 +3,10 @@ package fr.free.nrw.commons.settings; import android.Manifest; import android.app.AlertDialog; import android.content.ActivityNotFoundException; -import android.content.ComponentName; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -24,8 +21,6 @@ import android.support.v4.content.FileProvider; import android.widget.Toast; import java.io.File; -import java.util.ArrayList; -import java.util.List; import javax.inject.Inject; import javax.inject.Named; @@ -35,7 +30,7 @@ import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.Utils; import fr.free.nrw.commons.di.ApplicationlessInjection; -import fr.free.nrw.commons.utils.FileUtils; +import fr.free.nrw.commons.upload.FileUtils; public class SettingsFragment extends PreferenceFragment { diff --git a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java index 5bc1ced61..827af6962 100644 --- a/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/theme/NavigationBaseActivity.java @@ -17,6 +17,7 @@ import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -29,6 +30,7 @@ import fr.free.nrw.commons.BuildConfig; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.R; import fr.free.nrw.commons.WelcomeActivity; +import fr.free.nrw.commons.achievements.AchievementsActivity; import fr.free.nrw.commons.auth.AccountUtil; import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.contributions.ContributionsActivity; @@ -67,13 +69,17 @@ public abstract class NavigationBaseActivity extends BaseActivity setDrawerPaneWidth(); setUserName(); Menu nav_Menu = navigationView.getMenu(); + View headerLayout = navigationView.getHeaderView(0); + ImageView userIcon = headerLayout.findViewById(R.id.user_icon); if (prefs.getBoolean("login_skipped", true)) { + userIcon.setVisibility(View.GONE); nav_Menu.findItem(R.id.action_login).setVisible(true); nav_Menu.findItem(R.id.action_home).setVisible(false); nav_Menu.findItem(R.id.action_notifications).setVisible(false); nav_Menu.findItem(R.id.action_settings).setVisible(false); nav_Menu.findItem(R.id.action_logout).setVisible(false); }else { + userIcon.setVisibility(View.VISIBLE); nav_Menu.findItem(R.id.action_login).setVisible(false); nav_Menu.findItem(R.id.action_home).setVisible(true); nav_Menu.findItem(R.id.action_notifications).setVisible(true); @@ -89,12 +95,19 @@ public abstract class NavigationBaseActivity extends BaseActivity View navHeaderView = navigationView.getHeaderView(0); TextView username = navHeaderView.findViewById(R.id.username); - AccountManager accountManager = AccountManager.get(this); Account[] allAccounts = accountManager.getAccountsByType(AccountUtil.ACCOUNT_TYPE); if (allAccounts.length != 0) { username.setText(allAccounts[0].name); } + ImageView userIcon = navHeaderView.findViewById(R.id.user_icon); + userIcon.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + drawerLayout.closeDrawer(navigationView); + AchievementsActivity.startYourself(NavigationBaseActivity.this); + } + }); } public void initBackButton() { @@ -103,6 +116,15 @@ public abstract class NavigationBaseActivity extends BaseActivity toggle.setToolbarNavigationClickListener(v -> onBackPressed()); } + /** + * This method changes the toolbar icon to back regardless of any conditions that + * there is any fragment in the backStack or not + */ + public void forceInitBackButton() { + toggle.setDrawerIndicatorEnabled(false); + toggle.setToolbarNavigationClickListener(v -> onBackPressed()); + } + public void initBack() { setSupportActionBar(toolbar); getSupportActionBar().setDisplayHomeAsUpEnabled(true); @@ -175,8 +197,8 @@ public abstract class NavigationBaseActivity extends BaseActivity .setCancelable(false) .setPositiveButton(R.string.yes, (dialog, which) -> { BaseLogoutListener logoutListener = new BaseLogoutListener(); -// CommonsApplication app = (CommonsApplication) getApplication(); -// app.clearApplicationData(this, logoutListener); + CommonsApplication app = (CommonsApplication) getApplication(); + app.clearApplicationData(this, logoutListener); }) .setNegativeButton(R.string.no, (dialog, which) -> dialog.cancel()) .show(); @@ -185,9 +207,9 @@ public abstract class NavigationBaseActivity extends BaseActivity drawerLayout.closeDrawer(navigationView); NotificationActivity.startYourself(this); return true; - case R.id.action_featured_images: + case R.id.action_explore: drawerLayout.closeDrawer(navigationView); - CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_featured_images), FEATURED_IMAGES_CATEGORY); + CategoryImagesActivity.startYourself(this, getString(R.string.title_activity_explore), FEATURED_IMAGES_CATEGORY); return true; default: Timber.e("Unknown option [%s] selected from the navigation menu", itemId); @@ -215,4 +237,16 @@ public abstract class NavigationBaseActivity extends BaseActivity } context.startActivity(intent); } + + /** + * Handles visibility of navigation base toolbar + * @param show : Used to handle visibility of toolbar + */ + public void setNavigationBaseToolbarVisibility(boolean show){ + if (show){ + toolbar.setVisibility(View.VISIBLE); + }else { + toolbar.setVisibility(View.GONE); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleEditTextPreference.java b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleEditTextPreference.java new file mode 100644 index 000000000..758168d1c --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleEditTextPreference.java @@ -0,0 +1,37 @@ +package fr.free.nrw.commons.ui.LongTitlePreferences; + +import android.content.Context; +import android.preference.EditTextPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +/** + * Created by seannemann on 6/27/2018. + */ + +public class LongTitleEditTextPreference extends EditTextPreference { + public LongTitleEditTextPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LongTitleEditTextPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LongTitleEditTextPreference(Context context) { + + super(context); + } + + @Override + protected void onBindView(View view) + { + super.onBindView(view); + + TextView title= view.findViewById(android.R.id.title); + if (title != null) { + title.setSingleLine(false); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleListPreference.java b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleListPreference.java new file mode 100644 index 000000000..24b97d095 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleListPreference.java @@ -0,0 +1,32 @@ +package fr.free.nrw.commons.ui.LongTitlePreferences; + +import android.content.Context; +import android.preference.ListPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +/** + * Created by seannemann on 6/27/2018. + */ + +public class LongTitleListPreference extends ListPreference { + public LongTitleListPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LongTitleListPreference(Context context) { + super(context); + } + + @Override + protected void onBindView(View view) + { + super.onBindView(view); + + TextView title= view.findViewById(android.R.id.title); + if (title != null) { + title.setSingleLine(false); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitlePreference.java b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitlePreference.java new file mode 100644 index 000000000..0facf0889 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitlePreference.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.ui.LongTitlePreferences; + +import android.content.Context; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +/** + * Created by seannemann on 6/27/2018. + */ + +public class LongTitlePreference extends Preference { + public LongTitlePreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LongTitlePreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LongTitlePreference(Context context) { + super(context); + } + + @Override + protected void onBindView(View view) + { + super.onBindView(view); + + TextView title= view.findViewById(android.R.id.title); + if (title != null) { + title.setSingleLine(false); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitlePreferenceCategory.java b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitlePreferenceCategory.java new file mode 100644 index 000000000..7ba8a1c18 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitlePreferenceCategory.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.ui.LongTitlePreferences; + +import android.content.Context; +import android.preference.PreferenceCategory; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +/** + * Created by seannemann on 6/27/2018. + */ + +public class LongTitlePreferenceCategory extends PreferenceCategory { + public LongTitlePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LongTitlePreferenceCategory(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LongTitlePreferenceCategory(Context context) { + super(context); + } + + @Override + protected void onBindView(View view) + { + super.onBindView(view); + + TextView title= view.findViewById(android.R.id.title); + if (title != null) { + title.setSingleLine(false); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleSwitchPreference.java b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleSwitchPreference.java new file mode 100644 index 000000000..5614e93e9 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/ui/LongTitlePreferences/LongTitleSwitchPreference.java @@ -0,0 +1,36 @@ +package fr.free.nrw.commons.ui.LongTitlePreferences; + +import android.content.Context; +import android.preference.SwitchPreference; +import android.util.AttributeSet; +import android.view.View; +import android.widget.TextView; + +/** + * Created by seannemann on 6/27/2018. + */ + +public class LongTitleSwitchPreference extends SwitchPreference { + public LongTitleSwitchPreference(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public LongTitleSwitchPreference(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public LongTitleSwitchPreference(Context context) { + super(context); + } + + @Override + protected void onBindView(View view) + { + super.onBindView(view); + + TextView title= view.findViewById(android.R.id.title); + if (title != null) { + title.setSingleLine(false); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java index ab9fa5602..5a413e49a 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/DetectUnwantedPicturesAsync.java @@ -1,10 +1,8 @@ package fr.free.nrw.commons.upload; import android.app.Activity; -import android.content.Context; import android.content.Intent; import android.graphics.BitmapRegionDecoder; -import android.net.Uri; import android.os.AsyncTask; import android.support.v7.app.AlertDialog; diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java new file mode 100644 index 000000000..b29d686f5 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileProcessor.java @@ -0,0 +1,262 @@ +package fr.free.nrw.commons.upload; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.ref.WeakReference; +import java.util.Date; +import java.util.List; + +import javax.inject.Inject; +import javax.inject.Named; + +import fr.free.nrw.commons.caching.CacheController; +import fr.free.nrw.commons.di.ApplicationlessInjection; +import fr.free.nrw.commons.mwapi.CategoryApi; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +import static com.mapbox.mapboxsdk.Mapbox.getApplicationContext; + +/** + * Processing of the image file that is about to be uploaded via ShareActivity is done here + */ +public class FileProcessor implements SimilarImageDialogFragment.onResponse { + + @Inject + CacheController cacheController; + @Inject + GpsCategoryModel gpsCategoryModel; + @Inject + CategoryApi apiCall; + @Inject + @Named("default_preferences") + SharedPreferences prefs; + private Uri mediaUri; + private ContentResolver contentResolver; + private GPSExtractor imageObj; + private Context context; + private String decimalCoords; + private boolean haveCheckedForOtherImages = false; + private String filePath; + private boolean useExtStorage; + private boolean cacheFound; + private GPSExtractor tempImageObj; + + FileProcessor(Uri mediaUri, ContentResolver contentResolver, Context context) { + this.mediaUri = mediaUri; + this.contentResolver = contentResolver; + this.context = context; + ApplicationlessInjection.getInstance(context.getApplicationContext()).getCommonsApplicationComponent().inject(this); + useExtStorage = prefs.getBoolean("useExternalStorage", true); + } + + /** + * Gets file path from media URI. + * In older devices getPath() may fail depending on the source URI, creating and using a copy of the file seems to work instead. + * + * @return file path of media + */ + @Nullable + private String getPathOfMediaOrCopy() { + filePath = FileUtils.getPath(context, mediaUri); + Timber.d("Filepath: " + filePath); + if (filePath == null) { + String copyPath = null; + try { + ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r"); + if (descriptor != null) { + if (useExtStorage) { + copyPath = FileUtils.createCopyPath(descriptor); + return copyPath; + } + copyPath = getApplicationContext().getCacheDir().getAbsolutePath() + "/" + new Date().getTime() + ".jpg"; + FileUtils.copy(descriptor.getFileDescriptor(), copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } + } catch (IOException e) { + Timber.w(e, "Error in file " + copyPath); + return null; + } + } + return filePath; + } + + /** + * Processes file coordinates, either from EXIF data or user location + * + * @param gpsEnabled if true use GPS + */ + GPSExtractor processFileCoordinates(boolean gpsEnabled) { + Timber.d("Calling GPSExtractor"); + try { + ParcelFileDescriptor descriptor = contentResolver.openFileDescriptor(mediaUri, "r"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (descriptor != null) { + imageObj = new GPSExtractor(descriptor.getFileDescriptor()); + } + } else { + String filePath = getPathOfMediaOrCopy(); + if (filePath != null) { + imageObj = new GPSExtractor(filePath); + } + } + + decimalCoords = imageObj.getCoords(); + if (decimalCoords == null || !imageObj.imageCoordsExists) { + //Find other photos taken around the same time which has gps coordinates + if (!haveCheckedForOtherImages) + findOtherImages();// Do not do repeat the process + } else { + useImageCoords(); + } + + } catch (FileNotFoundException e) { + Timber.w("File not found: " + mediaUri, e); + } + return imageObj; + } + + String getDecimalCoords() { + return decimalCoords; + } + + /** + * Find other images around the same location that were taken within the last 20 sec + * + */ + private void findOtherImages() { + Timber.d("filePath" + getPathOfMediaOrCopy()); + + long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created + File folder = new File(filePath.substring(0, filePath.lastIndexOf('/'))); + File[] files = folder.listFiles(); + Timber.d("folderTime Number:" + files.length); + + + for (File file : files) { + if (file.lastModified() - timeOfCreation <= (120 * 1000) && file.lastModified() - timeOfCreation >= -(120 * 1000)) { + //Make sure the photos were taken within 20seconds + Timber.d("fild date:" + file.lastModified() + " time of creation" + timeOfCreation); + tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos + ParcelFileDescriptor descriptor = null; + try { + descriptor = contentResolver.openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (descriptor != null) { + tempImageObj = new GPSExtractor(descriptor.getFileDescriptor()); + } + } else { + if (filePath != null) { + tempImageObj = new GPSExtractor(file.getAbsolutePath()); + } + } + + if (tempImageObj != null) { + Timber.d("not null fild EXIF" + tempImageObj.imageCoordsExists + " coords" + tempImageObj.getCoords()); + if (tempImageObj.getCoords() != null && tempImageObj.imageCoordsExists) { + // Current image has gps coordinates and it's not current gps locaiton + Timber.d("This file has image coords:" + file.getAbsolutePath()); + SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); + Bundle args = new Bundle(); + args.putString("originalImagePath", filePath); + args.putString("possibleImagePath", file.getAbsolutePath()); + newFragment.setArguments(args); + newFragment.show(((AppCompatActivity) context).getSupportFragmentManager(), "dialog"); + break; + } + } + } + } + haveCheckedForOtherImages = true; //Finished checking for other images + } + + /** + * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. + * Then initiates the calls to MediaWiki API through an instance of CategoryApi. + */ + @SuppressLint("CheckResult") + public void useImageCoords() { + if (decimalCoords != null) { + Timber.d("Decimal coords of image: %s", decimalCoords); + Timber.d("is EXIF data present:" + imageObj.imageCoordsExists + " from findOther image"); + + // Only set cache for this point if image has coords + if (imageObj.imageCoordsExists) { + double decLongitude = imageObj.getDecLongitude(); + double decLatitude = imageObj.getDecLatitude(); + cacheController.setQtPoint(decLongitude, decLatitude); + } + + List displayCatList = cacheController.findCategory(); + boolean catListEmpty = displayCatList.isEmpty(); + + + // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories + if (catListEmpty) { + cacheFound = false; + apiCall.request(decimalCoords) + .subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + gpsCategoryModel::setCategoryList, + throwable -> { + Timber.e(throwable); + gpsCategoryModel.clear(); + } + ); + Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); + } else { + cacheFound = true; + Timber.d("Cache found, setting categoryList in model to %s", displayCatList); + gpsCategoryModel.setCategoryList(displayCatList); + } + } else { + Timber.d("EXIF: no coords"); + } + } + + boolean isCacheFound() { + return cacheFound; + } + + /** + * Calls the async task that detects if image is fuzzy, too dark, etc + */ + void detectUnwantedPictures() { + String imageMediaFilePath = FileUtils.getPath(context, mediaUri); + DetectUnwantedPicturesAsync detectUnwantedPicturesAsync + = new DetectUnwantedPicturesAsync(new WeakReference((Activity) context), imageMediaFilePath); + detectUnwantedPicturesAsync.execute(); + } + + @Override + public void onPositiveResponse() { + imageObj = tempImageObj; + decimalCoords = imageObj.getCoords();// Not necessary to use gps as image already ha EXIF data + Timber.d("EXIF from tempImageObj"); + useImageCoords(); + } + + @Override + public void onNegativeResponse() { + Timber.d("EXIF from imageObj"); + useImageCoords(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java index 612b86458..0cd45c189 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtils.java @@ -15,18 +15,84 @@ import android.provider.MediaStore; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import java.io.BufferedReader; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.math.BigInteger; import java.nio.channels.FileChannel; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Date; import timber.log.Timber; public class FileUtils { + /** + * Get SHA1 of file from input stream + */ + static String getSHA1(InputStream is) { + + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA1"); + } catch (NoSuchAlgorithmException e) { + Timber.e(e, "Exception while getting Digest"); + return ""; + } + + byte[] buffer = new byte[8192]; + int read; + try { + while ((read = is.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + byte[] md5sum = digest.digest(); + BigInteger bigInt = new BigInteger(1, md5sum); + String output = bigInt.toString(16); + // Fill to 40 chars + output = String.format("%40s", output).replace(' ', '0'); + Timber.i("File SHA1: %s", output); + + return output; + } catch (IOException e) { + Timber.e(e, "IO Exception"); + return ""; + } finally { + try { + is.close(); + } catch (IOException e) { + Timber.e(e, "Exception on closing MD5 input stream"); + } + } + } + + /** + * In older devices getPath() may fail depending on the source URI. Creating and using a copy of the file seems to work instead. + * @return path of copy + */ + @Nullable + static String createCopyPath(ParcelFileDescriptor descriptor) { + try { + String copyPath = Environment.getExternalStorageDirectory().toString() + "/CommonsApp/" + new Date().getTime() + ".jpg"; + File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + newFile.mkdir(); + FileUtils.copy(descriptor.getFileDescriptor(), copyPath); + Timber.d("Filepath (copied): %s", copyPath); + return copyPath; + } catch (IOException e) { + Timber.e(e); + return null; + } + } + /** * Get a file path from a Uri. This will get the the path for Storage Access * Framework Documents, as well as the _data field for the MediaStore and @@ -235,4 +301,80 @@ public class FileUtils { copy(new FileInputStream(source), new FileOutputStream(destination)); } + + /** + * Read and return the content of a resource file as string. + * @param fileName asset file's path (e.g. "/queries/nearby_query.rq") + * @return the content of the file + */ + public static String readFromResource(String fileName) throws IOException { + StringBuilder buffer = new StringBuilder(); + BufferedReader reader = null; + try { + InputStream inputStream = FileUtils.class.getResourceAsStream(fileName); + if (inputStream == null) { + throw new FileNotFoundException(fileName); + } + reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); + String line; + while ((line = reader.readLine()) != null) { + buffer.append(line).append("\n"); + } + } finally { + if (reader != null) { + reader.close(); + } + } + return buffer.toString(); + } + + /** + * Deletes files. + * @param file context + */ + public static boolean deleteFile(File file) { + boolean deletedAll = true; + if (file != null) { + if (file.isDirectory()) { + String[] children = file.list(); + for (String child : children) { + deletedAll = deleteFile(new File(file, child)) && deletedAll; + } + } else { + deletedAll = file.delete(); + } + } + + return deletedAll; + } + + public static File createAndGetAppLogsFile(String logs) { + try { + File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); + if (!commonsAppDirectory.exists()) { + commonsAppDirectory.mkdir(); + } + + File logsFile = new File(commonsAppDirectory,"logs.txt"); + if (logsFile.exists()) { + //old logs file is useless + logsFile.delete(); + } + + logsFile.createNewFile(); + + FileOutputStream outputStream = new FileOutputStream(logsFile); + OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); + outputStreamWriter.append(logs); + outputStreamWriter.close(); + outputStream.flush(); + outputStream.close(); + + return logsFile; + } catch (IOException ioe) { + Timber.e(ioe); + return null; + } + } + } \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java index b9750e350..e45b31f05 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/GPSExtractor.java @@ -1,13 +1,6 @@ package fr.free.nrw.commons.upload; -import android.content.Context; -import android.content.SharedPreferences; -import android.location.Criteria; -import android.location.Location; -import android.location.LocationListener; -import android.location.LocationManager; import android.media.ExifInterface; -import android.os.Bundle; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; @@ -19,31 +12,21 @@ import timber.log.Timber; /** * Extracts geolocation to be passed to API for category suggestions. If a picture with geolocation - * is uploaded, extract latitude and longitude from EXIF data of image. If a picture without - * geolocation is uploaded, retrieve user's location (if enabled in Settings). + * is uploaded, extract latitude and longitude from EXIF data of image. */ public class GPSExtractor { - private final Context context; - private SharedPreferences prefs; private ExifInterface exif; private double decLatitude; private double decLongitude; - private Double currentLatitude = null; - private Double currentLongitude = null; public boolean imageCoordsExists; - private MyLocationListener myLocationListener; - private LocationManager locationManager; /** * Construct from the file descriptor of the image (only for API 24 or newer). * @param fileDescriptor the file descriptor of the image - * @param context the context */ @RequiresApi(24) - public GPSExtractor(@NonNull FileDescriptor fileDescriptor, Context context, SharedPreferences prefs) { - this.context = context; - this.prefs = prefs; + public GPSExtractor(@NonNull FileDescriptor fileDescriptor) { try { exif = new ExifInterface(fileDescriptor); } catch (IOException | IllegalArgumentException e) { @@ -54,65 +37,22 @@ public class GPSExtractor { /** * Construct from the file path of the image. * @param path file path of the image - * @param context the context + * */ - public GPSExtractor(@NonNull String path, Context context, SharedPreferences prefs) { - this.prefs = prefs; + public GPSExtractor(@NonNull String path) { try { exif = new ExifInterface(path); } catch (IOException | IllegalArgumentException e) { Timber.w(e); } - this.context = context; - } - - /** - * Check if user enabled retrieval of their current location in Settings - * @return true if enabled, false if disabled - */ - private boolean gpsPreferenceEnabled() { - boolean gpsPref = prefs.getBoolean("allowGps", false); - Timber.d("Gps pref set to: %b", gpsPref); - return gpsPref; - } - - /** - * Registers a LocationManager to listen for current location - */ - protected void registerLocationManager() { - locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); - Criteria criteria = new Criteria(); - String provider = locationManager.getBestProvider(criteria, true); - myLocationListener = new MyLocationListener(); - - try { - locationManager.requestLocationUpdates(provider, 400, 1, myLocationListener); - Location location = locationManager.getLastKnownLocation(provider); - if (location != null) { - myLocationListener.onLocationChanged(location); - } - } catch (IllegalArgumentException e) { - Timber.e(e, "Illegal argument exception"); - } catch (SecurityException e) { - Timber.e(e, "Security exception"); - } - } - - protected void unregisterLocationManager() { - try { - locationManager.removeUpdates(myLocationListener); - } catch (SecurityException e) { - Timber.e(e, "Security exception"); - } } /** * Extracts geolocation (either of image from EXIF data, or of user) - * @param useGPS set to true if location permissions allowed (by API 23), false if disallowed * @return coordinates as string (needs to be passed as a String in API query) */ @Nullable - public String getCoords(boolean useGPS) { + public String getCoords() { String latitude; String longitude; String latitudeRef; @@ -120,30 +60,9 @@ public class GPSExtractor { String decimalCoords; //If image has no EXIF data and user has enabled GPS setting, get user's location + //TODO: Always return null as a temporary fix for #1599 if (exif == null || exif.getAttribute(ExifInterface.TAG_GPS_LATITUDE) == null) { - if (useGPS) { - registerLocationManager(); - - imageCoordsExists = false; - Timber.d("EXIF data has no location info"); - - //Check what user's preference is for automatic location detection - boolean gpsPrefEnabled = gpsPreferenceEnabled(); - - //Check that currentLatitude and currentLongitude have been - // explicitly set by MyLocationListener - // and do not default to (0.0,0.0) - if (gpsPrefEnabled && currentLatitude != null && currentLongitude != null) { - Timber.d("Current location values: Lat = %f Long = %f", - currentLatitude, currentLongitude); - return String.valueOf(currentLatitude) + "|" + String.valueOf(currentLongitude); - } else { - // No coords found - return null; - } - } else { - return null; - } + return null; } else { //If image has EXIF data, extract image coords imageCoordsExists = true; @@ -166,33 +85,6 @@ public class GPSExtractor { } } - /** - * Listen for user's location when it changes - */ - private class MyLocationListener implements LocationListener { - - @Override - public void onLocationChanged(Location location) { - currentLatitude = location.getLatitude(); - currentLongitude = location.getLongitude(); - } - - @Override - public void onStatusChanged(String provider, int status, Bundle extras) { - Timber.d("%s's status changed to %d", provider, status); - } - - @Override - public void onProviderEnabled(String provider) { - Timber.d("Provider %s enabled", provider); - } - - @Override - public void onProviderDisabled(String provider) { - Timber.d("Provider %s disabled", provider); - } - } - public double getDecLatitude() { return decLatitude; } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java b/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java new file mode 100644 index 000000000..841210453 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/GpsCategoryModel.java @@ -0,0 +1,40 @@ +package fr.free.nrw.commons.upload; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class GpsCategoryModel { + private Set categorySet; + + @Inject + public GpsCategoryModel() { + clear(); + } + + public void clear() { + categorySet = new HashSet<>(); + } + + public boolean getGpsCatExists() { + return !categorySet.isEmpty(); + } + + public List getCategoryList() { + return new ArrayList<>(categorySet); + } + + public void setCategoryList(List categoryList) { + clear(); + categorySet.addAll(categoryList != null ? categoryList : new ArrayList<>()); + } + + public void add(String categoryString) { + categorySet.add(categoryString); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java index 9c31e2b4d..0e2dc7a8e 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleShareActivity.java @@ -47,6 +47,8 @@ import fr.free.nrw.commons.modifications.TemplateRemoveModifier; import fr.free.nrw.commons.mwapi.MediaWikiApi; import timber.log.Timber; +//TODO: We should use this class to see how multiple uploads are handled, and then REMOVE it. + public class MultipleShareActivity extends AuthenticatedActivity implements MediaDetailPagerFragment.MediaDetailProvider, AdapterView.OnItemClickListener, @@ -327,18 +329,18 @@ public class MultipleShareActivity extends AuthenticatedActivity if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { ParcelFileDescriptor fd = getContentResolver().openFileDescriptor(imageUri,"r"); if (fd != null) { - gpsExtractor = new GPSExtractor(fd.getFileDescriptor(),this,prefs); + gpsExtractor = new GPSExtractor(fd.getFileDescriptor()); } } else { String filePath = FileUtils.getPath(this,imageUri); if (filePath != null) { - gpsExtractor = new GPSExtractor(filePath,this,prefs); + gpsExtractor = new GPSExtractor(filePath); } } if (gpsExtractor != null) { //get image coordinates from exif data or user location - return gpsExtractor.getCoords(locationPermitted); + return gpsExtractor.getCoords(); } } catch (FileNotFoundException fnfe) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java index 4390bcef4..028456bb6 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/MultipleUploadListFragment.java @@ -1,6 +1,5 @@ package fr.free.nrw.commons.upload; -import android.app.Activity; import android.content.Context; import android.graphics.Point; import android.net.Uri; @@ -17,7 +16,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.BaseAdapter; import android.widget.EditText; @@ -26,6 +24,8 @@ import android.widget.GridView; import android.widget.RelativeLayout; import android.widget.TextView; +import butterknife.BindView; +import butterknife.ButterKnife; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; @@ -41,9 +41,13 @@ public class MultipleUploadListFragment extends Fragment { void OnMultipleUploadInitiated(); } - private GridView photosGrid; + @BindView(R.id.multipleShareBackground) + GridView photosGrid; + + @BindView(R.id.multipleBaseTitle) + EditText baseTitle; + private PhotoDisplayAdapter photosAdapter; - private EditText baseTitle; private TitleTextWatcher textWatcher = new TitleTextWatcher(); private Point photoSize; @@ -166,9 +170,7 @@ public class MultipleUploadListFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_multiple_uploads_list, container, false); - photosGrid = view.findViewById(R.id.multipleShareBackground); - baseTitle = view.findViewById(R.id.multipleBaseTitle); - + ButterKnife.bind(this,view); photosAdapter = new PhotoDisplayAdapter(); photosGrid.setAdapter(photosAdapter); photosGrid.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity()); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java b/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java deleted file mode 100644 index a530e79e6..000000000 --- a/app/src/main/java/fr/free/nrw/commons/upload/MwVolleyApi.java +++ /dev/null @@ -1,249 +0,0 @@ -package fr.free.nrw.commons.upload; - -import android.content.Context; -import android.net.Uri; - -import com.android.volley.Cache; -import com.android.volley.NetworkResponse; -import com.android.volley.Request; -import com.android.volley.RequestQueue; -import com.android.volley.Response; -import com.android.volley.VolleyError; -import com.android.volley.toolbox.HttpHeaderParser; -import com.android.volley.toolbox.JsonRequest; -import com.android.volley.toolbox.Volley; -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; - -import java.io.UnsupportedEncodingException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import timber.log.Timber; - -/** - * Uses the Volley library to implement asynchronous calls to the Commons MediaWiki API to match - * GPS coordinates with nearby Commons categories. Parses the results using GSON to obtain a list - * of relevant categories. - */ -public class MwVolleyApi { - - private static RequestQueue REQUEST_QUEUE; - private static final Gson GSON = new GsonBuilder().create(); - - private static Set categorySet; - private static List categoryList; - - private static final String MWURL = "https://commons.wikimedia.org/"; - private final Context context; - - public MwVolleyApi(Context context) { - this.context = context; - categorySet = new HashSet<>(); - } - - public static List getGpsCat() { - return categoryList; - } - - public static void setGpsCat(List cachedList) { - categoryList = new ArrayList<>(); - categoryList.addAll(cachedList); - Timber.d("Setting GPS cats from cache: %s", categoryList); - } - - public void request(String coords) { - String apiUrl = buildUrl(coords); - Timber.d("URL: %s", apiUrl); - - JsonRequest request = new QueryRequest(apiUrl, - new LogResponseListener<>(), new LogResponseErrorListener()); - getQueue().add(request); - } - - /** - * Builds URL with image coords for MediaWiki API calls - * Example URL: https://commons.wikimedia.org/w/api.php?action=query&prop=categories|coordinates|pageprops&format=json&clshow=!hidden&coprop=type|name|dim|country|region|globe&codistancefrompoint=38.11386944444445|13.356263888888888&generator=geosearch&redirects=&ggscoord=38.11386944444445|1.356263888888888&ggsradius=100&ggslimit=10&ggsnamespace=6&ggsprop=type|name|dim|country|region|globe&ggsprimary=all&formatversion=2 - * @param coords Coordinates to build query with - * @return URL for API query - */ - private String buildUrl(String coords) { - - Uri.Builder builder = Uri.parse(MWURL).buildUpon(); - - builder.appendPath("w") - .appendPath("api.php") - .appendQueryParameter("action", "query") - .appendQueryParameter("prop", "categories|coordinates|pageprops") - .appendQueryParameter("format", "json") - .appendQueryParameter("clshow", "!hidden") - .appendQueryParameter("coprop", "type|name|dim|country|region|globe") - .appendQueryParameter("codistancefrompoint", coords) - .appendQueryParameter("generator", "geosearch") - .appendQueryParameter("ggscoord", coords) - .appendQueryParameter("ggsradius", "10000") - .appendQueryParameter("ggslimit", "10") - .appendQueryParameter("ggsnamespace", "6") - .appendQueryParameter("ggsprop", "type|name|dim|country|region|globe") - .appendQueryParameter("ggsprimary", "all") - .appendQueryParameter("formatversion", "2"); - - return builder.toString(); - } - - private synchronized RequestQueue getQueue() { - if (REQUEST_QUEUE == null) { - REQUEST_QUEUE = Volley.newRequestQueue(context); - } - return REQUEST_QUEUE; - } - - private static class LogResponseListener implements Response.Listener { - - @Override - public void onResponse(T response) { - Timber.d(response.toString()); - } - } - - private static class LogResponseErrorListener implements Response.ErrorListener { - - @Override - public void onErrorResponse(VolleyError error) { - Timber.e(error.toString()); - } - } - - private static class QueryRequest extends JsonRequest { - - public QueryRequest(String url, - Response.Listener listener, - Response.ErrorListener errorListener) { - super(Request.Method.GET, url, null, listener, errorListener); - } - - @Override - protected Response parseNetworkResponse(NetworkResponse response) { - String json = parseString(response); - QueryResponse queryResponse = GSON.fromJson(json, QueryResponse.class); - return Response.success(queryResponse, cacheEntry(response)); - } - - private Cache.Entry cacheEntry(NetworkResponse response) { - return HttpHeaderParser.parseCacheHeaders(response); - } - - private String parseString(NetworkResponse response) { - try { - return new String(response.data, HttpHeaderParser.parseCharset(response.headers)); - } catch (UnsupportedEncodingException e) { - return new String(response.data); - } - } - } - - public static class GpsCatExists { - private static boolean gpsCatExists; - - public static void setGpsCatExists(boolean gpsCat) { - gpsCatExists = gpsCat; - } - - public static boolean getGpsCatExists() { - return gpsCatExists; - } - } - - private static class QueryResponse { - private Query query = new Query(); - - private String printSet() { - if (categorySet == null || categorySet.isEmpty()) { - GpsCatExists.setGpsCatExists(false); - Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists()); - return "No collection of categories"; - } else { - GpsCatExists.setGpsCatExists(true); - Timber.d("gpsCatExists=%b", GpsCatExists.getGpsCatExists()); - return "CATEGORIES FOUND" + categorySet.toString(); - } - - } - - @Override - public String toString() { - if (query != null) { - return "query=" + query.toString() + "\n" + printSet(); - } else { - return "No pages found"; - } - } - } - - private static class Query { - private Page [] pages; - - @Override - public String toString() { - StringBuilder builder = new StringBuilder("pages=" + "\n"); - if (pages != null) { - for (Page page : pages) { - builder.append(page.toString()); - builder.append("\n"); - } - builder.replace(builder.length() - 1, builder.length(), ""); - return builder.toString(); - } else { - return "No pages found"; - } - } - } - - public static class Page { - private int pageid; - private int ns; - private String title; - private Category[] categories; - private Category category; - - public Page() { - } - - @Override - public String toString() { - - StringBuilder builder = new StringBuilder("PAGEID=" + pageid + " ns=" + ns + " title=" + title + "\n" + " CATEGORIES= "); - - if (categories == null || categories.length == 0) { - builder.append("no categories exist\n"); - } else { - for (Category category : categories) { - builder.append(category.toString()); - builder.append("\n"); - if (category != null) { - String categoryString = category.toString().replace("Category:", ""); - categorySet.add(categoryString); - } - } - } - - categoryList = new ArrayList<>(categorySet); - builder.replace(builder.length() - 1, builder.length(), ""); - return builder.toString(); - } - } - - private static class Category { - private String title; - - @Override - public String toString() { - return title; - } - } -} - - - diff --git a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java index 6a59c8e30..f9f289d33 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/ShareActivity.java @@ -1,90 +1,73 @@ package fr.free.nrw.commons.upload; import android.Manifest; -import android.app.Activity; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; +import android.app.Activity; import android.content.ContentResolver; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.graphics.Bitmap; -import android.graphics.BitmapRegionDecoder; import android.graphics.Point; import android.graphics.Rect; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.provider.MediaStore; import android.support.annotation.NonNull; -import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import android.support.design.widget.FloatingActionButton; -import android.support.design.widget.Snackbar; import android.support.graphics.drawable.VectorDrawableCompat; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.FragmentManager; import android.support.v4.content.ContextCompat; -import android.support.v4.graphics.BitmapCompat; -import android.support.v7.app.AlertDialog; -import android.util.Log; +import android.view.KeyEvent; import android.view.MenuItem; import android.view.View; -import android.view.WindowManager; import android.view.animation.DecelerateInterpolator; -import android.view.inputmethod.InputMethodManager; -import android.widget.TextView; +import android.widget.FrameLayout; import android.widget.Toast; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; import com.github.chrisbanes.photoview.PhotoView; - -import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.lang.ref.WeakReference; -import java.math.BigInteger; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Date; import java.util.List; import javax.inject.Inject; import javax.inject.Named; +import butterknife.BindView; import butterknife.ButterKnife; +import butterknife.OnClick; import fr.free.nrw.commons.R; import fr.free.nrw.commons.auth.AuthenticatedActivity; +import fr.free.nrw.commons.auth.LoginActivity; import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.caching.CacheController; import fr.free.nrw.commons.category.CategorizationFragment; import fr.free.nrw.commons.category.OnCategoriesSaveHandler; import fr.free.nrw.commons.contributions.Contribution; -import fr.free.nrw.commons.contributions.ContributionsActivity; import fr.free.nrw.commons.modifications.CategoryModifier; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.modifications.ModifierSequence; import fr.free.nrw.commons.modifications.ModifierSequenceDao; import fr.free.nrw.commons.modifications.TemplateRemoveModifier; - -import fr.free.nrw.commons.utils.ImageUtils; +import fr.free.nrw.commons.mwapi.CategoryApi; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; -import android.support.design.widget.FloatingActionButton; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.DUPLICATE_PROCEED; import static fr.free.nrw.commons.upload.ExistingFileAsync.Result.NO_DUPLICATE; -import static java.lang.Long.min; +import static fr.free.nrw.commons.upload.FileUtils.getSHA1; +import static fr.free.nrw.commons.wikidata.WikidataConstants.WIKIDATA_ENTITY_ID_PREF; /** * Activity for the title/desc screen after image is selected. Also starts processing image @@ -93,14 +76,12 @@ import static java.lang.Long.min; public class ShareActivity extends AuthenticatedActivity implements SingleUploadFragment.OnUploadActionInitiated, - OnCategoriesSaveHandler,SimilarImageDialogFragment.onResponse { - - private static final int REQUEST_PERM_ON_CREATE_STORAGE = 1; - private static final int REQUEST_PERM_ON_CREATE_LOCATION = 2; - private static final int REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION = 3; + OnCategoriesSaveHandler { private static final int REQUEST_PERM_ON_SUBMIT_STORAGE = 4; - private CategorizationFragment categorizationFragment; - + //Had to make them class variables, to extract out the click listeners, also I see no harm in this + final Rect startBounds = new Rect(); + final Rect finalBounds = new Rect(); + final Point globalOffset = new Point(); @Inject MediaWikiApi mwApi; @Inject @@ -112,93 +93,127 @@ public class ShareActivity @Inject ModifierSequenceDao modifierSequenceDao; @Inject + CategoryApi apiCall; + @Inject @Named("default_preferences") SharedPreferences prefs; + @Inject + GpsCategoryModel gpsCategoryModel; + + @BindView(R.id.container) + FrameLayout flContainer; + @BindView(R.id.backgroundImage) + SimpleDraweeView backgroundImageView; + @BindView(R.id.media_map) + FloatingActionButton mapButton; + @BindView(R.id.media_upload_zoom_in) + FloatingActionButton zoomInButton; + @BindView(R.id.media_upload_zoom_out) + FloatingActionButton zoomOutButton; + @BindView(R.id.main_fab) + FloatingActionButton mainFab; + @BindView(R.id.expanded_image) + PhotoView expandedImageView; private String source; private String mimeType; - + private CategorizationFragment categorizationFragment; private Uri mediaUri; private Contribution contribution; - private SimpleDraweeView backgroundImageView; - private FloatingActionButton maps_fragment; - - private boolean cacheFound; - - private GPSExtractor imageObj; - private GPSExtractor tempImageObj; + private GPSExtractor gpsObj; private String decimalCoords; - + private FileProcessor fileObj; private boolean useNewPermissions = false; private boolean storagePermitted = false; private boolean locationPermitted = false; - private String title; private String description; - private Snackbar snackbar; + private String wikiDataEntityId; private boolean duplicateCheckPassed = false; - - private boolean haveCheckedForOtherImages = false; private boolean isNearbyUpload = false; - private Animator CurrentAnimator; private long ShortAnimationDuration; - private FloatingActionButton zoomInButton; - private FloatingActionButton zoomOutButton; - private FloatingActionButton mainFab; private boolean isFABOpen = false; + private float startScaleFinal; + private boolean isZoom = false; /** * Called when user taps the submit button. + * Requests Storage permission, if needed. */ + @Override public void uploadActionInitiated(String title, String description) { this.title = title; this.description = description; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - // Check for Storage permission that is required for upload. - // Do not allow user to proceed without permission, otherwise will crash - if (needsToRequestStoragePermission()) { - requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERM_ON_SUBMIT_STORAGE); + + if (sessionManager.getCurrentAccount() != null) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + // Check for Storage permission that is required for upload. + // Do not allow user to proceed without permission, otherwise will crash + if (needsToRequestStoragePermission()) { + requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, + REQUEST_PERM_ON_SUBMIT_STORAGE); + } else { + uploadBegins(); + } } else { uploadBegins(); } - } else { - uploadBegins(); + } + else //Send user to login activity + { + Toast.makeText(this, "You need to login first!", Toast.LENGTH_SHORT).show(); + Intent loginIntent = new Intent(ShareActivity.this, LoginActivity.class); + startActivity(loginIntent); } } + /** + * Checks whether storage permissions need to be requested. + * Permissions are needed if the file is not owned by this application, (e.g. shared from the Gallery) + * + * @return true if file is not owned by this application and permission hasn't been granted beforehand + */ @RequiresApi(16) private boolean needsToRequestStoragePermission() { - // We need to ask storage permission when - // the file is not owned by this application, (e.g. shared from the Gallery) - // and permission is not obtained. return !FileUtils.isSelfOwned(getApplicationContext(), mediaUri) && (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED); } + + /** + * Called after permission checks are done. + * Gets file metadata for category suggestions, displays toast, caches categories found, calls uploadController + */ + private void uploadBegins() { - getFileMetadata(locationPermitted); + fileObj.processFileCoordinates(locationPermitted); Toast startingToast = Toast.makeText(this, R.string.uploading_started, Toast.LENGTH_LONG); startingToast.show(); - if (!cacheFound) { + if (!fileObj.isCacheFound()) { //Has to be called after apiCall.request() cacheController.cacheCategory(); Timber.d("Cache the categories found"); } - uploadController.startUpload(title, mediaUri, description, mimeType, source, decimalCoords, c -> { - ShareActivity.this.contribution = c; - showPostUpload(); - }); - } + uploadController.startUpload(title,mediaUri,description,mimeType,source,decimalCoords,wikiDataEntityId,c -> + + { + ShareActivity.this.contribution = c; + showPostUpload(); + }); +} + + /** + * Starts CategorizationFragment after uploadBegins. + */ private void showPostUpload() { if (categorizationFragment == null) { @@ -209,6 +224,11 @@ public class ShareActivity .commit(); } + /** + * Send categories to modifications queue after they are selected + * + * @param categories categories selected + */ @Override public void onCategoriesSave(List categories) { if (categories.size() > 0) { @@ -246,9 +266,6 @@ public class ShareActivity finish(); } - protected boolean isNearbyUpload() { - return isNearbyUpload; - } @Override public void onCreate(Bundle savedInstanceState) { @@ -257,7 +274,6 @@ public class ShareActivity setContentView(R.layout.activity_share); ButterKnife.bind(this); initBack(); - backgroundImageView = (SimpleDraweeView) findViewById(R.id.backgroundImage); backgroundImageView.setHierarchy(GenericDraweeHierarchyBuilder .newInstance(getResources()) .setPlaceholderImage(VectorDrawableCompat.create(getResources(), @@ -266,7 +282,38 @@ public class ShareActivity R.drawable.ic_error_outline_black_24dp, getTheme())) .build()); - //Receive intent from ContributionController.java when user selects picture to upload + receiveImageIntent(); + + if (savedInstanceState != null) { + contribution = savedInstanceState.getParcelable("contribution"); + } + + requestAuthToken(); + Timber.d("Uri: %s", mediaUri.toString()); + Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); + + SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); + categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); + if (shareView == null && categorizationFragment == null) { + shareView = new SingleUploadFragment(); + getSupportFragmentManager() + .beginTransaction() + .add(R.id.single_upload_fragment_container, shareView, "shareView") + .commitAllowingStateLoss(); + } + uploadController.prepareService(); + + ContentResolver contentResolver = this.getContentResolver(); + fileObj = new FileProcessor(mediaUri, contentResolver, this); + checkIfFileExists(); + gpsObj = fileObj.processFileCoordinates(locationPermitted); + decimalCoords = fileObj.getDecimalCoords(); + } + + /** + * Receive intent from ContributionController.java when user selects picture to upload + */ + private void receiveImageIntent() { Intent intent = getIntent(); if (Intent.ACTION_SEND.equals(intent.getAction())) { @@ -279,6 +326,8 @@ public class ShareActivity if (intent.hasExtra("isDirectUpload")) { Timber.d("This was initiated by a direct upload from Nearby"); isNearbyUpload = true; + wikiDataEntityId = intent.getStringExtra(WIKIDATA_ENTITY_ID_PREF); + Timber.d("Received wikiDataEntityId from contribution controller %s", wikiDataEntityId); } mimeType = intent.getType(); } @@ -286,481 +335,121 @@ public class ShareActivity if (mediaUri != null) { backgroundImageView.setImageURI(mediaUri); } - - mainFab = (FloatingActionButton) findViewById(R.id.main_fab); - /* - * called when upper arrow floating button - */ - mainFab.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if(!isFABOpen){ - showFABMenu(); - }else{ - closeFABMenu(); - } - } - }); - - - - zoomInButton = (FloatingActionButton) findViewById(R.id.media_upload_zoom_in); - try { - zoomInButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - zoomImageFromThumb(backgroundImageView, mediaUri); - } - }); - } catch (Exception e){ - Log.i("exception", e.toString()); - } - zoomOutButton = (FloatingActionButton) findViewById(R.id.media_upload_zoom_out); - - if (savedInstanceState != null) { - contribution = savedInstanceState.getParcelable("contribution"); - } - - requestAuthToken(); - - Timber.d("Uri: %s", mediaUri.toString()); - Timber.d("Ext storage dir: %s", Environment.getExternalStorageDirectory()); - - useNewPermissions = false; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - useNewPermissions = true; - - if (!needsToRequestStoragePermission()) { - storagePermitted = true; - } - if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { - locationPermitted = true; - } - } - - // Check storage permissions if marshmallow or newer - if (useNewPermissions && (!storagePermitted || !locationPermitted)) { - if (!storagePermitted && !locationPermitted) { - String permissionRationales = - getResources().getString(R.string.read_storage_permission_rationale) + "\n" - + getResources().getString(R.string.location_permission_rationale); - snackbar = requestPermissionUsingSnackBar( - permissionRationales, - new String[]{ - Manifest.permission.READ_EXTERNAL_STORAGE, - Manifest.permission.ACCESS_FINE_LOCATION}, - REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION); - View snackbarView = snackbar.getView(); - TextView textView = (TextView) snackbarView.findViewById(android.support.design.R.id.snackbar_text); - textView.setMaxLines(3); - } else if (!storagePermitted) { - requestPermissionUsingSnackBar( - getString(R.string.read_storage_permission_rationale), - new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, - REQUEST_PERM_ON_CREATE_STORAGE); - } else if (!locationPermitted) { - requestPermissionUsingSnackBar( - getString(R.string.location_permission_rationale), - new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, - REQUEST_PERM_ON_CREATE_LOCATION); - } - } - performPreUploadProcessingOfFile(); - - - SingleUploadFragment shareView = (SingleUploadFragment) getSupportFragmentManager().findFragmentByTag("shareView"); - categorizationFragment = (CategorizationFragment) getSupportFragmentManager().findFragmentByTag("categorization"); - if (shareView == null && categorizationFragment == null) { - shareView = new SingleUploadFragment(); - getSupportFragmentManager() - .beginTransaction() - .add(R.id.single_upload_fragment_container, shareView, "shareView") - .commitAllowingStateLoss(); - } - uploadController.prepareService(); - maps_fragment = (FloatingActionButton) findViewById(R.id.media_map); - maps_fragment.setVisibility(View.VISIBLE); - if( imageObj == null || imageObj.imageCoordsExists != true){ - maps_fragment.setVisibility(View.INVISIBLE); - } - - - maps_fragment.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - if( imageObj != null && imageObj.imageCoordsExists == true) { - Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + imageObj.getDecLatitude() + "," + imageObj.getDecLongitude()); - Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); - mapIntent.setPackage("com.google.android.apps.maps"); - startActivity(mapIntent); - } - } - }); } - /* + + /** * Function to display the zoom and map FAB */ - private void showFABMenu(){ - isFABOpen=true; + private void showFABMenu() { + isFABOpen = true; - if( imageObj != null && imageObj.imageCoordsExists == true) - maps_fragment.setVisibility(View.VISIBLE); + if (gpsObj != null && gpsObj.imageCoordsExists) + mapButton.setVisibility(View.VISIBLE); zoomInButton.setVisibility(View.VISIBLE); mainFab.animate().rotationBy(180); - maps_fragment.animate().translationY(-getResources().getDimension(R.dimen.second_fab)); + mapButton.animate().translationY(-getResources().getDimension(R.dimen.second_fab)); zoomInButton.animate().translationY(-getResources().getDimension(R.dimen.first_fab)); } - /* - * function to close the zoom and map FAB + /** + * Function to close the zoom and map FAB */ - private void closeFABMenu(){ - isFABOpen=false; + private void closeFABMenu() { + isFABOpen = false; mainFab.animate().rotationBy(-180); - maps_fragment.animate().translationY(0); + mapButton.animate().translationY(0); zoomInButton.animate().translationY(0).setListener(new Animator.AnimatorListener() { @Override public void onAnimationStart(Animator animator) { - } @Override public void onAnimationEnd(Animator animator) { - if(!isFABOpen){ - maps_fragment.setVisibility(View.GONE); + if (!isFABOpen) { + mapButton.setVisibility(View.GONE); zoomInButton.setVisibility(View.GONE); } - } @Override public void onAnimationCancel(Animator animator) { - } @Override public void onAnimationRepeat(Animator animator) { - } }); } + /** + * Checks if upload was initiated via Nearby + * + * @return true if upload was initiated via Nearby + */ + protected boolean isNearbyUpload() { + return isNearbyUpload; + } + /** + * Handles submit button permission request (for storage) + * + * @param requestCode type of request + * @param permissions permissions requested + * @param grantResults grant results + */ @Override - public void onRequestPermissionsResult(int requestCode, - @NonNull String[] permissions, @NonNull int[] grantResults) { + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { switch (requestCode) { - case REQUEST_PERM_ON_CREATE_STORAGE: { - if (grantResults.length >= 1 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - backgroundImageView.setImageURI(mediaUri); - storagePermitted = true; - performPreUploadProcessingOfFile(); - } - return; - } - case REQUEST_PERM_ON_CREATE_LOCATION: { - if (grantResults.length >= 1 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - locationPermitted = true; - performPreUploadProcessingOfFile(); - } - return; - } - case REQUEST_PERM_ON_CREATE_STORAGE_AND_LOCATION: { - if (grantResults.length >= 2 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - backgroundImageView.setImageURI(mediaUri); - storagePermitted = true; - performPreUploadProcessingOfFile(); - } - if (grantResults.length >= 2 - && grantResults[1] == PackageManager.PERMISSION_GRANTED) { - locationPermitted = true; - performPreUploadProcessingOfFile(); - } - return; - } // Storage (from submit button) - this needs to be separate from (1) because only the // submit button should bring user to next screen case REQUEST_PERM_ON_SUBMIT_STORAGE: { - if (grantResults.length >= 1 - && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - //It is OK to call this at both (1) and (4) because if perm had been granted at - //snackbar, user should not be prompted at submit button - performPreUploadProcessingOfFile(); + if (grantResults.length >= 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + checkIfFileExists(); //Uploading only begins if storage permission granted from arrow icon uploadBegins(); - snackbar.dismiss(); } - return; } } } - private void performPreUploadProcessingOfFile() { + /** + * Check if file user wants to upload already exists on Commons + */ + private void checkIfFileExists() { if (!useNewPermissions || storagePermitted) { if (!duplicateCheckPassed) { //Test SHA1 of image to see if it matches SHA1 of a file on Commons try { InputStream inputStream = getContentResolver().openInputStream(mediaUri); - Timber.d("Input stream created from %s", mediaUri.toString()); String fileSHA1 = getSHA1(inputStream); + Timber.d("Input stream created from %s", mediaUri.toString()); Timber.d("File SHA1 is: %s", fileSHA1); ExistingFileAsync fileAsyncTask = new ExistingFileAsync(new WeakReference(this), fileSHA1, new WeakReference(this), result -> { Timber.d("%s duplicate check: %s", mediaUri.toString(), result); - duplicateCheckPassed = (result == DUPLICATE_PROCEED - || result == NO_DUPLICATE); - /* - TODO: 16/9/17 should we run DetectUnwantedPicturesAsync if DUPLICATE_PROCEED is returned? Since that means - we are processing images that are already on server???... - */ - + duplicateCheckPassed = (result == DUPLICATE_PROCEED || result == NO_DUPLICATE); if (duplicateCheckPassed) { - //image can be uploaded, so now check if its a useless picture or not - performUnwantedPictureDetectionProcess(); + //image is not a duplicate, so now check if its a unwanted picture or not + fileObj.detectUnwantedPictures(); } - - },mwApi); + }, mwApi); fileAsyncTask.execute(); } catch (IOException e) { - Timber.d(e, "IO Exception: "); + Timber.e(e, "IO Exception: "); } } - - getFileMetadata(locationPermitted); } else { Timber.w("not ready for preprocessing: useNewPermissions=%s storage=%s location=%s", useNewPermissions, storagePermitted, locationPermitted); } } - private void performUnwantedPictureDetectionProcess() { - String imageMediaFilePath = FileUtils.getPath(this,mediaUri); - DetectUnwantedPicturesAsync detectUnwantedPicturesAsync - = new DetectUnwantedPicturesAsync(new WeakReference(this) - , imageMediaFilePath); - - detectUnwantedPicturesAsync.execute(); - } - - /* - * to display permission snackbar in share activity - */ - private Snackbar requestPermissionUsingSnackBar(String rationale, - final String[] perms, - final int code) { - Snackbar snackbar = Snackbar.make(findViewById(android.R.id.content), rationale, - Snackbar.LENGTH_INDEFINITE).setAction(R.string.ok, - view -> ActivityCompat.requestPermissions(ShareActivity.this, perms, code)); - snackbar.show(); - return snackbar; - } - - @Nullable - private String getPathOfMediaOrCopy() { - String filePath = FileUtils.getPath(getApplicationContext(), mediaUri); - Timber.d("Filepath: " + filePath); - if (filePath == null) { - // in older devices getPath() may fail depending on the source URI - // creating and using a copy of the file seems to work instead. - // TODO: there might be a more proper solution than this - String copyPath = null; - try { - ParcelFileDescriptor descriptor - = getContentResolver().openFileDescriptor(mediaUri, "r"); - if (descriptor != null) { - boolean useExtStorage = prefs.getBoolean("useExternalStorage", true); - if (useExtStorage) { - copyPath = Environment.getExternalStorageDirectory().toString() - + "/CommonsApp/" + new Date().getTime() + ".jpg"; - File newFile = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); - newFile.mkdir(); - FileUtils.copy( - descriptor.getFileDescriptor(), - copyPath); - Timber.d("Filepath (copied): %s", copyPath); - return copyPath; - } - copyPath = getApplicationContext().getCacheDir().getAbsolutePath() - + "/" + new Date().getTime() + ".jpg"; - FileUtils.copy( - descriptor.getFileDescriptor(), - copyPath); - Timber.d("Filepath (copied): %s", copyPath); - return copyPath; - } - } catch (IOException e) { - Timber.w(e, "Error in file " + copyPath); - return null; - } - } - return filePath; - } - - /** - * Gets coordinates for category suggestions, either from EXIF data or user location - * - * @param gpsEnabled if true use GPS - */ - private void getFileMetadata(boolean gpsEnabled) { - Timber.d("Calling GPSExtractor"); - try { - if (imageObj == null) { - ParcelFileDescriptor descriptor - = getContentResolver().openFileDescriptor(mediaUri, "r"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (descriptor != null) { - imageObj = new GPSExtractor(descriptor.getFileDescriptor(), this, prefs); - } - } else { - String filePath = getPathOfMediaOrCopy(); - if (filePath != null) { - imageObj = new GPSExtractor(filePath, this, prefs); - } - } - } - - if (imageObj != null) { - // Gets image coords from exif data or user location - decimalCoords = imageObj.getCoords(gpsEnabled); - if(decimalCoords==null || !imageObj.imageCoordsExists){ -// Check if the location is from GPS or EXIF -// Find other photos taken around the same time which has gps coordinates - Timber.d("EXIF:false"); - Timber.d("EXIF call"+(imageObj==tempImageObj)); - if(!haveCheckedForOtherImages) - findOtherImages(gpsEnabled);// Do not do repeat the process - } - else { -// As the selected image has GPS data in EXIF go ahead with the same. - useImageCoords(); - } - } - } catch (FileNotFoundException e) { - Timber.w("File not found: " + mediaUri, e); - } - } - - private void findOtherImages(boolean gpsEnabled) { - Timber.d("filePath"+getPathOfMediaOrCopy()); - String filePath = getPathOfMediaOrCopy(); - long timeOfCreation = new File(filePath).lastModified();//Time when the original image was created - File folder = new File(filePath.substring(0,filePath.lastIndexOf('/'))); - File[] files = folder.listFiles(); - Timber.d("folderTime Number:"+files.length); - - for(File file : files){ - if(file.lastModified()-timeOfCreation<=(120*1000) && file.lastModified()-timeOfCreation>=-(120*1000)){ - //Make sure the photos were taken within 20seconds - Timber.d("fild date:"+file.lastModified()+ " time of creation"+timeOfCreation); - tempImageObj = null;//Temporary GPSExtractor to extract coords from these photos - ParcelFileDescriptor descriptor - = null; - try { - descriptor = getContentResolver().openFileDescriptor(Uri.parse(file.getAbsolutePath()), "r"); - } catch (FileNotFoundException e) { - e.printStackTrace(); - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - if (descriptor != null) { - tempImageObj = new GPSExtractor(descriptor.getFileDescriptor(),this, prefs); - } - } else { - if (filePath != null) { - tempImageObj = new GPSExtractor(file.getAbsolutePath(), this, prefs); - } - } - - if(tempImageObj!=null){ - Timber.d("not null fild EXIF"+tempImageObj.imageCoordsExists +" coords"+tempImageObj.getCoords(gpsEnabled)); - if(tempImageObj.getCoords(gpsEnabled)!=null && tempImageObj.imageCoordsExists){ -// Current image has gps coordinates and it's not current gps locaiton - Timber.d("This fild has image coords:"+ file.getAbsolutePath()); -// Create a dialog fragment for the suggestion - FragmentManager fragmentManager = getSupportFragmentManager(); - SimilarImageDialogFragment newFragment = new SimilarImageDialogFragment(); - Bundle args = new Bundle(); - args.putString("originalImagePath",filePath); - args.putString("possibleImagePath",file.getAbsolutePath()); - newFragment.setArguments(args); - newFragment.show(fragmentManager, "dialog"); - break; - } - - } - - } - } - haveCheckedForOtherImages = true; //Finished checking for other images - return; - } - - @Override - public void onPostiveResponse() { - imageObj = tempImageObj; - decimalCoords = imageObj.getCoords(false);// Not necessary to use gps as image already ha EXIF data - Timber.d("EXIF from tempImageObj"); - useImageCoords(); - } - - @Override - public void onNegativeResponse() { - Timber.d("EXIF from imageObj"); - useImageCoords(); - - } - - /** - * Initiates retrieval of image coordinates or user coordinates, and caching of coordinates. - * Then initiates the calls to MediaWiki API through an instance of MwVolleyApi. - */ - public void useImageCoords() { - if (decimalCoords != null) { - Timber.d("Decimal coords of image: %s", decimalCoords); - Timber.d("is EXIF data present:"+imageObj.imageCoordsExists+" from findOther image:"+(imageObj==tempImageObj)); - - // Only set cache for this point if image has coords - if (imageObj.imageCoordsExists) { - double decLongitude = imageObj.getDecLongitude(); - double decLatitude = imageObj.getDecLatitude(); - cacheController.setQtPoint(decLongitude, decLatitude); - } - - MwVolleyApi apiCall = new MwVolleyApi(this); - - List displayCatList = cacheController.findCategory(); - boolean catListEmpty = displayCatList.isEmpty(); - - // If no categories found in cache, call MediaWiki API to match image coords with nearby Commons categories - if (catListEmpty) { - cacheFound = false; - apiCall.request(decimalCoords); - Timber.d("displayCatList size 0, calling MWAPI %s", displayCatList); - } else { - cacheFound = true; - Timber.d("Cache found, setting categoryList in MwVolleyApi to %s", displayCatList); - MwVolleyApi.setGpsCat(displayCatList); - } - }else{ - Timber.d("EXIF: no coords"); - } - - } - @Override public void onPause() { super.onPause(); - try { - imageObj.unregisterLocationManager(); - Timber.d("Unregistered locationManager"); - } catch (NullPointerException e) { - Timber.d("locationManager does not exist, not unregistered"); - } } @Override @@ -783,132 +472,32 @@ public class ShareActivity return super.onOptionsItemSelected(item); } - /* - * Get SHA1 of file from input stream + /** + * Allows zooming in to the image about to be uploaded. Called when zoom FAB is tapped */ - private String getSHA1(InputStream is) { - - MessageDigest digest; - try { - digest = MessageDigest.getInstance("SHA1"); - } catch (NoSuchAlgorithmException e) { - Timber.e(e, "Exception while getting Digest"); - return ""; - } - - byte[] buffer = new byte[8192]; - int read; - try { - while ((read = is.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - byte[] md5sum = digest.digest(); - BigInteger bigInt = new BigInteger(1, md5sum); - String output = bigInt.toString(16); - // Fill to 40 chars - output = String.format("%40s", output).replace(' ', '0'); - Timber.i("File SHA1: %s", output); - - return output; - } catch (IOException e) { - Timber.e(e, "IO Exception"); - return ""; - } finally { - try { - is.close(); - } catch (IOException e) { - Timber.e(e, "Exception on closing MD5 input stream"); - } - } - } - - /* - * function to provide pinch zoom - */ - private void zoomImageFromThumb(final View thumbView, Uri imageuri ) { - // If there's an animation in progress, cancel it - // immediately and proceed with this one. + private void zoomImageFromThumb(final View thumbView, Uri imageuri) { + // If there's an animation in progress, cancel it immediately and proceed with this one. if (CurrentAnimator != null) { CurrentAnimator.cancel(); } + isZoom = true; ViewUtil.hideKeyboard(ShareActivity.this.findViewById(R.id.titleEdit | R.id.descEdit)); closeFABMenu(); mainFab.setVisibility(View.GONE); + InputStream input = null; - Bitmap scaled = null; try { input = this.getContentResolver().openInputStream(imageuri); } catch (FileNotFoundException e) { e.printStackTrace(); } - BitmapRegionDecoder decoder = null; - try { - decoder = BitmapRegionDecoder.newInstance(input, false); - } catch (IOException e) { - e.printStackTrace(); - } - Bitmap bitmap = decoder.decodeRegion(new Rect(10, 10, 50, 50), null); - try { - //Compress the Image - System.gc(); - Runtime rt = Runtime.getRuntime(); - long maxMemory = rt.freeMemory(); - bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), imageuri); - int bitmapByteCount= BitmapCompat.getAllocationByteCount(bitmap); - long height = bitmap.getHeight(); - long width = bitmap.getWidth(); - long calHeight = (long) ((height * maxMemory)/(bitmapByteCount * 1.1)); - long calWidth = (long) ((width * maxMemory)/(bitmapByteCount * 1.1)); - scaled = Bitmap.createScaledBitmap(bitmap,(int) Math.min(width,calWidth), (int) Math.min(height,calHeight), true); - } catch (IOException e) { - } catch (NullPointerException e){ - scaled = bitmap; - } + + Zoom zoomObj = new Zoom(thumbView, flContainer, this.getContentResolver()); + Bitmap scaledImage = zoomObj.createScaledImage(input, imageuri); + // Load the high-resolution "zoomed-in" image. - PhotoView expandedImageView = (PhotoView) findViewById( - R.id.expanded_image); - expandedImageView.setImageBitmap(scaled); - - - - // Calculate the starting and ending bounds for the zoomed-in image. - // This step involves lots of math. Yay, math. - final Rect startBounds = new Rect(); - final Rect finalBounds = new Rect(); - final Point globalOffset = new Point(); - - // The start bounds are the global visible rectangle of the thumbnail, - // and the final bounds are the global visible rectangle of the container - // view. Also set the container view's offset as the origin for the - // bounds, since that's the origin for the positioning animation - // properties (X, Y). - thumbView.getGlobalVisibleRect(startBounds); - findViewById(R.id.container) - .getGlobalVisibleRect(finalBounds, globalOffset); - startBounds.offset(-globalOffset.x, -globalOffset.y); - finalBounds.offset(-globalOffset.x, -globalOffset.y); - - // Adjust the start bounds to be the same aspect ratio as the final - // bounds using the "center crop" technique. This prevents undesirable - // stretching during the animation. Also calculate the start scaling - // factor (the end scaling factor is always 1.0). - float startScale; - if ((float) finalBounds.width() / finalBounds.height() - > (float) startBounds.width() / startBounds.height()) { - // Extend start bounds horizontally - startScale = (float) startBounds.height() / finalBounds.height(); - float startWidth = startScale * finalBounds.width(); - float deltaWidth = (startWidth - startBounds.width()) / 2; - startBounds.left -= deltaWidth; - startBounds.right += deltaWidth; - } else { - // Extend start bounds vertically - startScale = (float) startBounds.width() / finalBounds.width(); - float startHeight = startScale * finalBounds.height(); - float deltaHeight = (startHeight - startBounds.height()) / 2; - startBounds.top -= deltaHeight; - startBounds.bottom += deltaHeight; - } + expandedImageView.setImageBitmap(scaledImage); + float startScale = zoomObj.adjustStartEndBounds(startBounds, finalBounds, globalOffset); // Hide the thumbnail and show the zoomed-in view. When the animation // begins, it will position the zoomed-in view in the place of the @@ -927,15 +516,10 @@ public class ShareActivity // Construct and run the parallel animation of the four translation and // scale properties (X, Y, SCALE_X, and SCALE_Y). AnimatorSet set = new AnimatorSet(); - set - .play(ObjectAnimator.ofFloat(expandedImageView, View.X, - startBounds.left, finalBounds.left)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, - startBounds.top, finalBounds.top)) - .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, - startScale, 1f)) - .with(ObjectAnimator.ofFloat(expandedImageView, - View.SCALE_Y, startScale, 1f)); + set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left, finalBounds.left)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top, finalBounds.top)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScale, 1f)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScale, 1f)); set.setDuration(ShortAnimationDuration); set.setInterpolator(new DecelerateInterpolator()); set.addListener(new AnimatorListenerAdapter() { @@ -955,53 +539,91 @@ public class ShareActivity // Upon clicking the zoomed-in image, it should zoom back down // to the original bounds and show the thumbnail instead of // the expanded image. - final float startScaleFinal = startScale; - zoomOutButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - if (CurrentAnimator != null) { - CurrentAnimator.cancel(); - } - zoomOutButton.setVisibility(View.GONE); - mainFab.setVisibility(View.VISIBLE); - - // Animate the four positioning/sizing properties in parallel, - // back to their original values. - AnimatorSet set = new AnimatorSet(); - set.play(ObjectAnimator - .ofFloat(expandedImageView, View.X, startBounds.left)) - .with(ObjectAnimator - .ofFloat(expandedImageView, - View.Y,startBounds.top)) - .with(ObjectAnimator - .ofFloat(expandedImageView, - View.SCALE_X, startScaleFinal)) - .with(ObjectAnimator - .ofFloat(expandedImageView, - View.SCALE_Y, startScaleFinal)); - set.setDuration(ShortAnimationDuration); - set.setInterpolator(new DecelerateInterpolator()); - set.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - thumbView.setAlpha(1f); - expandedImageView.setVisibility(View.GONE); - CurrentAnimator = null; - } - - @Override - public void onAnimationCancel(Animator animation) { - thumbView.setAlpha(1f); - expandedImageView.setVisibility(View.GONE); - CurrentAnimator = null; - } - }); - set.start(); - CurrentAnimator = set; - - } - - }); + startScaleFinal = startScale; } + /** + * Called when user taps the ^ FAB button, expands to show Zoom and Map + */ + @OnClick(R.id.main_fab) + public void onMainFabClicked() { + if (!isFABOpen) { + showFABMenu(); + } else { + closeFABMenu(); + } + } + + @OnClick(R.id.media_upload_zoom_in) + public void onZoomInFabClicked() { + try { + zoomImageFromThumb(backgroundImageView, mediaUri); + } catch (Exception e) { + Timber.e(e); + } + } + + @OnClick(R.id.media_upload_zoom_out) + public void onZoomOutFabClicked() { + if (CurrentAnimator != null) { + CurrentAnimator.cancel(); + } + isZoom = false; + zoomOutButton.setVisibility(View.GONE); + mainFab.setVisibility(View.VISIBLE); + + // Animate the four positioning/sizing properties in parallel, + // back to their original values. + AnimatorSet set = new AnimatorSet(); + set.play(ObjectAnimator.ofFloat(expandedImageView, View.X, startBounds.left)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.Y, startBounds.top)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_X, startScaleFinal)) + .with(ObjectAnimator.ofFloat(expandedImageView, View.SCALE_Y, startScaleFinal)); + + set.setDuration(ShortAnimationDuration); + set.setInterpolator(new DecelerateInterpolator()); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + //background image view is thumbView + backgroundImageView.setAlpha(1f); + expandedImageView.setVisibility(View.GONE); + CurrentAnimator = null; + } + + @Override + public void onAnimationCancel(Animator animation) { + //background image view is thumbView + backgroundImageView.setAlpha(1f); + expandedImageView.setVisibility(View.GONE); + CurrentAnimator = null; + } + }); + set.start(); + CurrentAnimator = set; + } + + @OnClick(R.id.media_map) + public void onFabShowMapsClicked() { + if (gpsObj != null && gpsObj.imageCoordsExists) { + Uri gmmIntentUri = Uri.parse("google.streetview:cbll=" + gpsObj.getDecLatitude() + "," + gpsObj.getDecLongitude()); + Intent mapIntent = new Intent(Intent.ACTION_VIEW, gmmIntentUri); + mapIntent.setPackage("com.google.android.apps.maps"); + startActivity(mapIntent); + } + } + + @Override + public boolean onKeyDown(int keyCode, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_BACK: + if(isZoom) { + onZoomOutFabClicked(); + return true; + } + } + return super.onKeyDown(keyCode,event); + + } } + diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java index a8f336927..59b8a1223 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SimilarImageDialogFragment.java @@ -13,6 +13,9 @@ import android.view.ViewGroup; import android.view.Window; import android.widget.Button; +import butterknife.BindView; +import butterknife.ButterKnife; +import butterknife.OnClick; import com.facebook.drawee.generic.GenericDraweeHierarchyBuilder; import com.facebook.drawee.view.SimpleDraweeView; import com.facebook.imagepipeline.listener.RequestListener; @@ -29,29 +32,33 @@ import fr.free.nrw.commons.R; */ public class SimilarImageDialogFragment extends DialogFragment { + + @BindView(R.id.orginalImage) SimpleDraweeView originalImage; + @BindView(R.id.possibleImage) SimpleDraweeView possibleImage; + @BindView(R.id.postive_button) Button positiveButton; + @BindView(R.id.negative_button) Button negativeButton; onResponse mOnResponse;//Implemented interface from shareActivity Boolean gotResponse = false; + public SimilarImageDialogFragment() { } public interface onResponse{ - public void onPostiveResponse(); + public void onPositiveResponse(); + public void onNegativeResponse(); } + @Override public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_similar_image_dialog, container, false); + ButterKnife.bind(this,view); Set requestListeners = new HashSet<>(); requestListeners.add(new RequestLoggingListener()); - originalImage =(SimpleDraweeView) view.findViewById(R.id.orginalImage); - possibleImage =(SimpleDraweeView) view.findViewById(R.id.possibleImage); - positiveButton = (Button) view.findViewById(R.id.postive_button); - negativeButton = (Button) view.findViewById(R.id.negative_button); - originalImage.setHierarchy(GenericDraweeHierarchyBuilder .newInstance(getResources()) .setPlaceholderImage(VectorDrawableCompat.create(getResources(), @@ -70,22 +77,6 @@ public class SimilarImageDialogFragment extends DialogFragment { originalImage.setImageURI(Uri.fromFile(new File(getArguments().getString("originalImagePath")))); possibleImage.setImageURI(Uri.fromFile(new File(getArguments().getString("possibleImagePath")))); - negativeButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mOnResponse.onNegativeResponse(); - gotResponse = true; - dismiss(); - } - }); - positiveButton.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View view) { - mOnResponse.onPostiveResponse(); - gotResponse = true; - dismiss(); - } - }); return view; } @@ -105,8 +96,23 @@ public class SimilarImageDialogFragment extends DialogFragment { @Override public void onDismiss(DialogInterface dialog) { // I user dismisses dialog by pressing outside the dialog. - if(!gotResponse) + if (!gotResponse) { mOnResponse.onNegativeResponse(); + } super.onDismiss(dialog); } + + @OnClick(R.id.negative_button) + public void onNegativeButtonClicked() { + mOnResponse.onNegativeResponse(); + gotResponse = true; + dismiss(); + } + + @OnClick(R.id.postive_button) + public void onPositiveButtonClicked() { + mOnResponse.onPositiveResponse(); + gotResponse = true; + dismiss(); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java index a32fb7b42..f2fef1ddf 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/SingleUploadFragment.java @@ -4,6 +4,7 @@ import android.annotation.SuppressLint; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; +import android.net.Uri; import android.os.Bundle; import android.preference.PreferenceManager; import android.support.annotation.NonNull; @@ -13,6 +14,7 @@ import android.text.Editable; import android.text.Html; import android.text.TextWatcher; import android.text.method.LinkMovementMethod; +import android.util.Log; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuInflater; @@ -29,6 +31,7 @@ import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; +import java.util.Locale; import javax.inject.Inject; import javax.inject.Named; @@ -74,13 +77,13 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { //What happens when the 'submit' icon is tapped case R.id.menu_upload_single: - if (titleEdit.getText().toString().isEmpty()) { + if (titleEdit.getText().toString().trim().isEmpty()) { Toast.makeText(getContext(), R.string.add_title_toast, Toast.LENGTH_LONG).show(); return false; } - String title = titleEdit.getText().toString(); - String desc = descEdit.getText().toString(); + String title = titleEdit.getText().toString().trim(); + String desc = descEdit.getText().toString().trim(); //Save the title/desc in short-lived cache so next time this fragment is loaded, we can access these prefs.edit() @@ -342,4 +345,17 @@ public class SingleUploadFragment extends CommonsDaggerSupportFragment { .create() .show(); } + + /** + * To launch the Commons:Licensing + * @param view + */ + @OnClick(R.id.licenseInfo) + public void launchLicenseInfo(View view){ + Log.i("Language", Locale.getDefault().getLanguage()); + UrlLicense urlLicense = new UrlLicense(); + urlLicense.initialize(); + String url = urlLicense.getLicenseUrl(Locale.getDefault().getLanguage()); + Utils.handleWebUrl(getActivity() , Uri.parse(url)); + } } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java index 32554da0f..dc87a5f05 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadController.java @@ -1,5 +1,7 @@ package fr.free.nrw.commons.upload; +import android.annotation.SuppressLint; +import android.accounts.Account; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; @@ -13,6 +15,7 @@ import android.os.AsyncTask; import android.os.IBinder; import android.provider.MediaStore; import android.text.TextUtils; +import android.widget.Toast; import java.io.BufferedInputStream; import java.io.IOException; @@ -22,9 +25,15 @@ import java.util.concurrent.Executors; import fr.free.nrw.commons.CommonsApplication; import fr.free.nrw.commons.HandlerService; + +import fr.free.nrw.commons.auth.LoginActivity; + +import fr.free.nrw.commons.R; + import fr.free.nrw.commons.auth.SessionManager; import fr.free.nrw.commons.contributions.Contribution; import fr.free.nrw.commons.settings.Prefs; +import fr.free.nrw.commons.utils.ViewUtil; import timber.log.Timber; public class UploadController { @@ -82,28 +91,50 @@ public class UploadController { /** * Starts a new upload task. - * * @param title the title of the contribution * @param mediaUri the media URI of the contribution * @param description the description of the contribution * @param mimeType the MIME type of the contribution * @param source the source of the contribution * @param decimalCoords the coordinates in decimal. (e.g. "37.51136|-77.602615") + * @param wikiDataEntityId * @param onComplete the progress tracker */ - public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, ContributionUploadProgress onComplete) { + public void startUpload(String title, Uri mediaUri, String description, String mimeType, String source, String decimalCoords, String wikiDataEntityId, ContributionUploadProgress onComplete) { Contribution contribution; + + //TODO: Modify this to include coords + contribution = new Contribution(mediaUri, null, title, description, -1, + null, null, sessionManager.getCurrentAccount().name, + CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); + + + contribution.setTag("mimeType", mimeType); + contribution.setSource(source); + + //Calls the next overloaded method + startUpload(contribution, onComplete); + + Timber.d("Wikidata entity ID received from Share activity is %s", wikiDataEntityId); //TODO: Modify this to include coords + Account currentAccount = sessionManager.getCurrentAccount(); + if(currentAccount == null) { + Timber.d("Current account is null"); + ViewUtil.showLongToast(context, context.getString(R.string.user_not_logged_in)); + sessionManager.forceLogin(context); + return; + } contribution = new Contribution(mediaUri, null, title, description, -1, - null, null, sessionManager.getCurrentAccount().name, + null, null, currentAccount.name, CommonsApplication.DEFAULT_EDIT_SUMMARY, decimalCoords); + contribution.setTag("mimeType", mimeType); contribution.setSource(source); + contribution.setWikiDataEntityId(wikiDataEntityId); + - //Calls the next overloaded method - startUpload(contribution, onComplete); } /** @@ -112,6 +143,7 @@ public class UploadController { * @param contribution the contribution object * @param onComplete the progress tracker */ + @SuppressLint("StaticFieldLeak") public void startUpload(final Contribution contribution, final ContributionUploadProgress onComplete) { //Set creator, desc, and license if (TextUtils.isEmpty(contribution.getCreator())) { diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java index 94c005256..19bf6c2d3 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java +++ b/app/src/main/java/fr/free/nrw/commons/upload/UploadService.java @@ -7,7 +7,6 @@ import android.app.PendingIntent; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Intent; -import android.content.SharedPreferences; import android.graphics.BitmapFactory; import android.os.Bundle; import android.support.v4.app.NotificationCompat; @@ -23,7 +22,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.inject.Inject; -import javax.inject.Named; import fr.free.nrw.commons.HandlerService; import fr.free.nrw.commons.R; @@ -36,6 +34,7 @@ import fr.free.nrw.commons.contributions.ContributionsContentProvider; import fr.free.nrw.commons.modifications.ModificationsContentProvider; import fr.free.nrw.commons.mwapi.MediaWikiApi; import fr.free.nrw.commons.mwapi.UploadResult; +import fr.free.nrw.commons.wikidata.WikidataEditService; import timber.log.Timber; public class UploadService extends HandlerService { @@ -49,8 +48,8 @@ public class UploadService extends HandlerService { public static final String EXTRA_CAMPAIGN = EXTRA_PREFIX + ".campaign"; @Inject MediaWikiApi mwApi; + @Inject WikidataEditService wikidataEditService; @Inject SessionManager sessionManager; - @Inject @Named("default_preferences") SharedPreferences prefs; @Inject ContributionDao contributionDao; private NotificationManager notificationManager; @@ -137,6 +136,7 @@ public class UploadService extends HandlerService { @Override public void queue(int what, Contribution contribution) { + Timber.d("Upload service queue has contribution with wiki data entity id as %s", contribution.getWikiDataEntityId()); switch (what) { case ACTION_UPLOAD_FILE: @@ -231,10 +231,10 @@ public class UploadService extends HandlerService { Timber.d("Successfully revalidated token!"); } else { Timber.d("Unable to revalidate :("); - // TODO: Put up a new notification, ask them to re-login stopForeground(true); Toast failureToast = Toast.makeText(this, R.string.authentication_failed, Toast.LENGTH_LONG); failureToast.show(); + sessionManager.forceLogin(this); return; } } @@ -251,8 +251,11 @@ public class UploadService extends HandlerService { String resultStatus = uploadResult.getResultStatus(); if (!resultStatus.equals("Success")) { + Timber.d("Contribution upload failed. Wikidata entity won't be edited"); showFailedNotification(contribution); } else { + Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s", contribution.getWikiDataEntityId()); + wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), filename); contribution.setFilename(uploadResult.getCanonicalFilename()); contribution.setImageUrl(uploadResult.getImageUrl()); contribution.setState(Contribution.STATE_COMPLETED); diff --git a/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java new file mode 100644 index 000000000..875dde26a --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/UrlLicense.java @@ -0,0 +1,72 @@ +package fr.free.nrw.commons.upload; + +import java.util.HashMap; +import java.util.Locale; + +/** + * This is a Util class which provides the necessary token to open the Commons License + * info in the user language + */ +public class UrlLicense { + HashMap urlLicense = new HashMap(); + public void initialize(){ + urlLicense.put("en","https://commons.wikimedia.org/wiki/Commons:Licensing"); + urlLicense.put("ar","https://commons.wikimedia.org/wiki/Commons:Licensing/ar"); + urlLicense.put("ast","https://commons.wikimedia.org/wiki/Commons:Licensing/ast"); + urlLicense.put("az","https://commons.wikimedia.org/wiki/Commons:Licensing/az"); + urlLicense.put("be","https://commons.wikimedia.org/wiki/Commons:Licensing/be"); + urlLicense.put("bg","https://commons.wikimedia.org/wiki/Commons:Licensing/bg"); + urlLicense.put("bn","https://commons.wikimedia.org/wiki/Commons:Licensing/bn"); + urlLicense.put("ca","https://commons.wikimedia.org/wiki/Commons:Licensing/ca"); + urlLicense.put("cs","https://commons.wikimedia.org/wiki/Commons:Licensing/cs"); + urlLicense.put("da","https://commons.wikimedia.org/wiki/Commons:Licensing/da"); + urlLicense.put("de","https://commons.wikimedia.org/wiki/Commons:Licensing/de"); + urlLicense.put("el","https://commons.wikimedia.org/wiki/Commons:Licensing/el"); + urlLicense.put("eo","https://commons.wikimedia.org/wiki/Commons:Licensing/eo"); + urlLicense.put("es","https://commons.wikimedia.org/wiki/Commons:Licensing/es"); + urlLicense.put("eu","https://commons.wikimedia.org/wiki/Commons:Licensing/eu"); + urlLicense.put("fa","https://commons.wikimedia.org/wiki/Commons:Licensing/fa"); + urlLicense.put("fi","https://commons.wikimedia.org/wiki/Commons:Licensing/fi"); + urlLicense.put("fr","https://commons.wikimedia.org/wiki/Commons:Licensing/fr"); + urlLicense.put("gl","https://commons.wikimedia.org/wiki/Commons:Licensing/gl"); + urlLicense.put("gsw","https://commons.wikimedia.org/wiki/Commons:Licensing/gsw"); + urlLicense.put("he","https://commons.wikimedia.org/wiki/Commons:Licensing/he"); + urlLicense.put("hi","https://commons.wikimedia.org/wiki/Commons:Licensing/hi"); + urlLicense.put("hu","https://commons.wikimedia.org/wiki/Commons:Licensing/hu"); + urlLicense.put("id","https://commons.wikimedia.org/wiki/Commons:Licensing/id"); + urlLicense.put("is","https://commons.wikimedia.org/wiki/Commons:Licensing/is"); + urlLicense.put("it","https://commons.wikimedia.org/wiki/Commons:Licensing/it"); + urlLicense.put("ja","https://commons.wikimedia.org/wiki/Commons:Licensing/ja"); + urlLicense.put("ka","https://commons.wikimedia.org/wiki/Commons:Licensing/ka"); + urlLicense.put("km","https://commons.wikimedia.org/wiki/Commons:Licensing/km"); + urlLicense.put("ko","https://commons.wikimedia.org/wiki/Commons:Licensing/ko"); + urlLicense.put("ku","https://commons.wikimedia.org/wiki/Commons:Licensing/ku"); + urlLicense.put("mk","https://commons.wikimedia.org/wiki/Commons:Licensing/mk"); + urlLicense.put("mr","https://commons.wikimedia.org/wiki/Commons:Licensing/mr"); + urlLicense.put("ms","https://commons.wikimedia.org/wiki/Commons:Licensing/ms"); + urlLicense.put("my","https://commons.wikimedia.org/wiki/Commons:Licensing/my"); + urlLicense.put("nl","https://commons.wikimedia.org/wiki/Commons:Licensing/nl"); + urlLicense.put("oc","https://commons.wikimedia.org/wiki/Commons:Licensing/oc"); + urlLicense.put("pl","https://commons.wikimedia.org/wiki/Commons:Licensing/pl"); + urlLicense.put("pt","https://commons.wikimedia.org/wiki/Commons:Licensing/pt"); + urlLicense.put("pt-br","https://commons.wikimedia.org/wiki/Commons:Licensing/pt-br"); + urlLicense.put("ro","https://commons.wikimedia.org/wiki/Commons:Licensing/ro"); + urlLicense.put("ru","https://commons.wikimedia.org/wiki/Commons:Licensing/ru"); + urlLicense.put("scn","https://commons.wikimedia.org/wiki/Commons:Licensing/scn"); + urlLicense.put("sk","https://commons.wikimedia.org/wiki/Commons:Licensing/sk"); + urlLicense.put("sl","https://commons.wikimedia.org/wiki/Commons:Licensing/sl"); + urlLicense.put("sv","https://commons.wikimedia.org/wiki/Commons:Licensing/sv"); + urlLicense.put("tr","https://commons.wikimedia.org/wiki/Commons:Licensing/tr"); + urlLicense.put("uk","https://commons.wikimedia.org/wiki/Commons:Licensing/uk"); + urlLicense.put("ur","https://commons.wikimedia.org/wiki/Commons:Licensing/ur"); + urlLicense.put("vi","https://commons.wikimedia.org/wiki/Commons:Licensing/vi"); + urlLicense.put("zh","https://commons.wikimedia.org/wiki/Commons:Licensing/zh"); + } + public String getLicenseUrl ( String language){ + if(urlLicense.containsKey(language)) { + return urlLicense.get(language); + } else { + return urlLicense.get("en"); + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java b/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java new file mode 100644 index 000000000..438c7f77b --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/upload/Zoom.java @@ -0,0 +1,115 @@ +package fr.free.nrw.commons.upload; + +import android.content.ContentResolver; +import android.graphics.Bitmap; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Point; +import android.graphics.Rect; +import android.net.Uri; +import android.provider.MediaStore; +import android.support.v4.graphics.BitmapCompat; +import android.view.View; +import android.widget.FrameLayout; + +import java.io.IOException; +import java.io.InputStream; + +import timber.log.Timber; + +/** + * Contains utility methods for the Zoom function in ShareActivity. + */ +public class Zoom { + + private View thumbView; + private ContentResolver contentResolver; + private FrameLayout flContainer; + + Zoom(View thumbView, FrameLayout flContainer, ContentResolver contentResolver) { + this.thumbView = thumbView; + this.contentResolver = contentResolver; + this.flContainer = flContainer; + } + + /** + * Create a scaled bitmap to display the zoomed-in image + * @param input the input stream corresponding to the uploaded image + * @param imageUri the uploaded image's URI + * @return a zoomable bitmap + */ + Bitmap createScaledImage(InputStream input, Uri imageUri) { + + Bitmap scaled = null; + BitmapRegionDecoder decoder = null; + Bitmap bitmap = null; + + try { + decoder = BitmapRegionDecoder.newInstance(input, false); + bitmap = decoder.decodeRegion(new Rect(10, 10, 50, 50), null); + } catch (IOException e) { + Timber.e(e); + } catch (NullPointerException e) { + Timber.e(e); + } + try { + //Compress the Image + System.gc(); + Runtime rt = Runtime.getRuntime(); + long maxMemory = rt.freeMemory(); + bitmap = MediaStore.Images.Media.getBitmap(contentResolver, imageUri); + int bitmapByteCount = BitmapCompat.getAllocationByteCount(bitmap); + long height = bitmap.getHeight(); + long width = bitmap.getWidth(); + long calHeight = (long) ((height * maxMemory) / (bitmapByteCount * 1.1)); + long calWidth = (long) ((width * maxMemory) / (bitmapByteCount * 1.1)); + scaled = Bitmap.createScaledBitmap(bitmap, (int) Math.min(width, calWidth), (int) Math.min(height, calHeight), true); + } catch (IOException e) { + Timber.e(e); + } catch (NullPointerException e) { + Timber.e(e); + scaled = bitmap; + } + return scaled; + } + + /** + * Calculate the starting and ending bounds for the zoomed-in image. + * Also set the container view's offset as the origin for the + * bounds, since that's the origin for the positioning animation + * properties (X, Y). + * @param startBounds the global visible rectangle of the thumbnail + * @param finalBounds the global visible rectangle of the container view + * @param globalOffset the container view's offset + * @return scaled start bounds + */ + float adjustStartEndBounds(Rect startBounds, Rect finalBounds, Point globalOffset) { + + thumbView.getGlobalVisibleRect(startBounds); + flContainer.getGlobalVisibleRect(finalBounds, globalOffset); + startBounds.offset(-globalOffset.x, -globalOffset.y); + finalBounds.offset(-globalOffset.x, -globalOffset.y); + + // Adjust the start bounds to be the same aspect ratio as the final + // bounds using the "center crop" technique. This prevents undesirable + // stretching during the animation. Also calculate the start scaling + // factor (the end scaling factor is always 1.0). + float startScale; + if ((float) finalBounds.width() / finalBounds.height() + > (float) startBounds.width() / startBounds.height()) { + // Extend start bounds horizontally + startScale = (float) startBounds.height() / finalBounds.height(); + float startWidth = startScale * finalBounds.width(); + float deltaWidth = (startWidth - startBounds.width()) / 2; + startBounds.left -= deltaWidth; + startBounds.right += deltaWidth; + } else { + // Extend start bounds vertically + startScale = (float) startBounds.width() / finalBounds.width(); + float startHeight = startScale * finalBounds.height(); + float deltaHeight = (startHeight - startBounds.height()) / 2; + startBounds.top -= deltaHeight; + startBounds.bottom += deltaHeight; + } + return startScale; + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java deleted file mode 100644 index d56a7b608..000000000 --- a/app/src/main/java/fr/free/nrw/commons/utils/FileUtils.java +++ /dev/null @@ -1,92 +0,0 @@ -package fr.free.nrw.commons.utils; - -import android.os.Environment; - -import java.io.BufferedReader; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; - -import timber.log.Timber; - -public class FileUtils { - /** - * Read and return the content of a resource file as string. - * - * @param fileName asset file's path (e.g. "/queries/nearby_query.rq") - * @return the content of the file - */ - public static String readFromResource(String fileName) throws IOException { - StringBuilder buffer = new StringBuilder(); - BufferedReader reader = null; - try { - InputStream inputStream = FileUtils.class.getResourceAsStream(fileName); - if (inputStream == null) { - throw new FileNotFoundException(fileName); - } - reader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8")); - String line; - while ((line = reader.readLine()) != null) { - buffer.append(line).append("\n"); - } - } finally { - if (reader != null) { - reader.close(); - } - } - return buffer.toString(); - } - - /** - * Deletes files. - * @param file context - */ - public static boolean deleteFile(File file) { - boolean deletedAll = true; - if (file != null) { - if (file.isDirectory()) { - String[] children = file.list(); - for (String child : children) { - deletedAll = deleteFile(new File(file, child)) && deletedAll; - } - } else { - deletedAll = file.delete(); - } - } - - return deletedAll; - } - - public static File createAndGetAppLogsFile(String logs) { - try { - File commonsAppDirectory = new File(Environment.getExternalStorageDirectory().toString() + "/CommonsApp"); - if (!commonsAppDirectory.exists()) { - commonsAppDirectory.mkdir(); - } - - File logsFile = new File(commonsAppDirectory,"logs.txt"); - if (logsFile.exists()) { - //old logs file is useless - logsFile.delete(); - } - - logsFile.createNewFile(); - - FileOutputStream outputStream = new FileOutputStream(logsFile); - OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); - outputStreamWriter.append(logs); - outputStreamWriter.close(); - outputStream.flush(); - outputStream.close(); - - return logsFile; - } catch (IOException ioe) { - Timber.e(ioe); - return null; - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java index 4f6a6d456..79dad33e5 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ImageUtils.java @@ -1,10 +1,27 @@ package fr.free.nrw.commons.utils; +import android.app.WallpaperManager; +import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapRegionDecoder; import android.graphics.Color; import android.graphics.Rect; +import android.net.Uri; +import android.support.annotation.Nullable; +import com.facebook.common.executors.CallerThreadExecutor; +import com.facebook.common.references.CloseableReference; +import com.facebook.datasource.DataSource; +import com.facebook.drawee.backends.pipeline.Fresco; +import com.facebook.imagepipeline.core.ImagePipeline; +import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber; +import com.facebook.imagepipeline.image.CloseableImage; +import com.facebook.imagepipeline.request.ImageRequest; +import com.facebook.imagepipeline.request.ImageRequestBuilder; + +import java.io.IOException; + +import fr.free.nrw.commons.R; import timber.log.Timber; /** @@ -132,4 +149,52 @@ public class ImageUtils { return isImageDark; } + + /** + * Downloads the image from the URL and sets it as the phone's wallpaper + * Fails silently if download or setting wallpaper fails. + * @param context + * @param imageUrl + */ + public static void setWallpaperFromImageUrl(Context context, Uri imageUrl) { + Timber.d("Trying to set wallpaper from url %s", imageUrl.toString()); + ImageRequest imageRequest = ImageRequestBuilder + .newBuilderWithSource(imageUrl) + .setAutoRotateEnabled(true) + .build(); + + ImagePipeline imagePipeline = Fresco.getImagePipeline(); + final DataSource> + dataSource = imagePipeline.fetchDecodedImage(imageRequest, context); + + dataSource.subscribe(new BaseBitmapDataSubscriber() { + + @Override + public void onNewResultImpl(@Nullable Bitmap bitmap) { + if (dataSource.isFinished() && bitmap != null){ + Timber.d("Bitmap loaded from url %s", imageUrl.toString()); + setWallpaper(context, Bitmap.createBitmap(bitmap)); + dataSource.close(); + } + } + + @Override + public void onFailureImpl(DataSource dataSource) { + Timber.d("Error getting bitmap from image url %s", imageUrl.toString()); + if (dataSource != null) { + dataSource.close(); + } + } + }, CallerThreadExecutor.getInstance()); + } + + private static void setWallpaper(Context context, Bitmap bitmap) { + WallpaperManager wallpaperManager = WallpaperManager.getInstance(context); + try { + wallpaperManager.setBitmap(bitmap); + ViewUtil.showLongToast(context, context.getString(R.string.wallpaper_set_successfully)); + } catch (IOException e) { + Timber.e(e,"Error setting wallpaper"); + } + } } diff --git a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java index 0c22a40a2..af71e826d 100644 --- a/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java +++ b/app/src/main/java/fr/free/nrw/commons/utils/ViewUtil.java @@ -15,12 +15,19 @@ public class ViewUtil { public static final String SHOWCASE_VIEW_ID_3 = "SHOWCASE_VIEW_ID_3"; public static void showSnackbar(View view, int messageResourceId) { - Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show(); + if(view.getContext() == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Snackbar.make(view, messageResourceId, Snackbar.LENGTH_SHORT).show()); } public static void showLongToast(Context context, String text) { - Toast.makeText(context, text, - Toast.LENGTH_LONG).show(); + if (context == null) { + return; + } + + ExecutorUtils.uiExecutor().execute(() -> Toast.makeText(context, text, Toast.LENGTH_LONG).show()); } public static boolean isPortrait(Context context) { diff --git a/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java new file mode 100644 index 000000000..82bad3f09 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/widget/PicOfDayAppWidget.java @@ -0,0 +1,80 @@ +package fr.free.nrw.commons.widget; + +import android.appwidget.AppWidgetManager; +import android.appwidget.AppWidgetProvider; +import android.content.Context; +import android.widget.RemoteViews; + +import com.prof.rssparser.Article; +import com.prof.rssparser.Parser; +import com.squareup.picasso.Picasso; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; + +import fr.free.nrw.commons.BuildConfig; +import fr.free.nrw.commons.R; + +/** + * Implementation of App Widget functionality. + */ +public class PicOfDayAppWidget extends AppWidgetProvider { + + static void updateAppWidget(Context context, AppWidgetManager appWidgetManager, + int appWidgetId) { + + // Construct the RemoteViews object + RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.pic_of_day_app_widget); + + String urlString = BuildConfig.WIKIMEDIA_API_POTD; + Parser parser = new Parser(); + parser.execute(urlString); + parser.onFinish(new Parser.OnTaskCompleted() { + @Override + public void onTaskCompleted(ArrayList
list) { + String desc = list.get(list.size() - 1).getDescription(); + if (desc != null) { + Document document = Jsoup.parse(desc); + Elements elements = document.select("img"); + String imageUrl = elements.get(0).attr("src"); + if (imageUrl != null && imageUrl.length() > 0) { + Picasso.get().load(imageUrl).into(views, R.id.appwidget_image, new int[]{appWidgetId}); + } + } + + } + + @Override + public void onError() { + } + }); + + // Instruct the widget manager to update the widget + appWidgetManager.updateAppWidget(appWidgetId, views); + } + + @Override + public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { + // There may be multiple widgets active, so update all of them + for (int appWidgetId : appWidgetIds) { + updateAppWidget(context, appWidgetManager, appWidgetId); + } + } + + @Override + public void onEnabled(Context context) { + // Enter relevant functionality for when the first widget is created + } + + @Override + public void onDisabled(Context context) { + // Enter relevant functionality for when the last widget is disabled + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java new file mode 100644 index 000000000..e7e929dac --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataConstants.java @@ -0,0 +1,5 @@ +package fr.free.nrw.commons.wikidata; + +public class WikidataConstants { + public static final String WIKIDATA_ENTITY_ID_PREF = "WikiDataEntityId"; +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java new file mode 100644 index 000000000..30fb26ddc --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListener.java @@ -0,0 +1,16 @@ +package fr.free.nrw.commons.wikidata; + +public abstract class WikidataEditListener { + + protected WikidataP18EditListener wikidataP18EditListener; + + public abstract void onSuccessfulWikidataEdit(); + + public void setAuthenticationStateListener(WikidataP18EditListener wikidataP18EditListener) { + this.wikidataP18EditListener = wikidataP18EditListener; + } + + public interface WikidataP18EditListener { + void onWikidataEditSuccessful(); + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java new file mode 100644 index 000000000..407c24711 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditListenerImpl.java @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.wikidata; + +/** + * Listener for wikidata edits + */ +public class WikidataEditListenerImpl extends WikidataEditListener { + + public WikidataEditListenerImpl() { + } + + /** + * Fired when wikidata P18 edit is successful. If there's an active listener, then it is fired + */ + @Override + public void onSuccessfulWikidataEdit() { + if (wikidataP18EditListener != null) { + wikidataP18EditListener.onWikidataEditSuccessful(); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java new file mode 100644 index 000000000..d07bb56f3 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/wikidata/WikidataEditService.java @@ -0,0 +1,140 @@ +package fr.free.nrw.commons.wikidata; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.SharedPreferences; + +import java.util.Locale; + +import javax.inject.Inject; +import javax.inject.Named; +import javax.inject.Singleton; + +import fr.free.nrw.commons.R; +import fr.free.nrw.commons.mwapi.MediaWikiApi; +import fr.free.nrw.commons.utils.ViewUtil; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.schedulers.Schedulers; +import timber.log.Timber; + +/** + * This class is meant to handle the Wikidata edits made through the app + * It will talk with MediaWikiApi to make necessary API calls, log the edits and fire listeners + * on successful edits + */ +@Singleton +public class WikidataEditService { + + private final Context context; + private final MediaWikiApi mediaWikiApi; + private final WikidataEditListener wikidataEditListener; + private final SharedPreferences directPrefs; + + @Inject + public WikidataEditService(Context context, + MediaWikiApi mediaWikiApi, + WikidataEditListener wikidataEditListener, + @Named("direct_nearby_upload_prefs") SharedPreferences directPrefs) { + this.context = context; + this.mediaWikiApi = mediaWikiApi; + this.wikidataEditListener = wikidataEditListener; + this.directPrefs = directPrefs; + } + + /** + * Create a P18 claim and log the edit with custom tag + * @param wikidataEntityId + * @param fileName + */ + public void createClaimWithLogging(String wikidataEntityId, String fileName) { + if(wikidataEntityId == null) { + Timber.d("Skipping creation of claim as Wikidata entity ID is null"); + return; + } + + if(fileName == null) { + Timber.d("Skipping creation of claim as fileName entity ID is null"); + return; + } + + editWikidataProperty(wikidataEntityId, fileName); + } + + /** + * Edits the wikidata entity by adding the P18 property to it. + * Adding the P18 edit requires calling the wikidata API to create a claim against the entity + * + * @param wikidataEntityId + * @param fileName + */ + @SuppressLint("CheckResult") + private void editWikidataProperty(String wikidataEntityId, String fileName) { + Timber.d("Upload successful with wiki data entity id as %s", wikidataEntityId); + Timber.d("Attempting to edit Wikidata property %s", wikidataEntityId); + Observable.fromCallable(() -> { + String propertyValue = getFileName(fileName); + return mediaWikiApi.wikidatCreateClaim(wikidataEntityId, "P18", "value", propertyValue); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(revisionId -> handleClaimResult(wikidataEntityId, revisionId), throwable -> { + Timber.e(throwable, "Error occurred while making claim"); + ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); + }); + } + + private void handleClaimResult(String wikidataEntityId, String revisionId) { + if (revisionId != null) { + wikidataEditListener.onSuccessfulWikidataEdit(); + showSuccessToast(); + logEdit(revisionId); + } else { + Timber.d("Unable to make wiki data edit for entity %s", wikidataEntityId); + ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure)); + } + } + + /** + * Log the Wikidata edit by adding Wikimedia Commons App tag to the edit + * @param revisionId + */ + @SuppressLint("CheckResult") + private void logEdit(String revisionId) { + Observable.fromCallable(() -> mediaWikiApi.addWikidataEditTag(revisionId)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(result -> { + if (result) { + Timber.d("Wikidata edit was tagged successfully"); + } else { + Timber.d("Wikidata edit couldn't be tagged"); + } + }, throwable -> { + Timber.e(throwable, "Error occurred while adding tag to the edit"); + }); + } + + /** + * Show a success toast when the edit is made successfully + */ + private void showSuccessToast() { + String title = directPrefs.getString("Title", ""); + String successStringTemplate = context.getString(R.string.successful_wikidata_edit); + String successMessage = String.format(Locale.getDefault(), successStringTemplate, title); + ViewUtil.showLongToast(context, successMessage); + } + + /** + * Formats and returns the filename as accepted by the wiki base API + * https://www.mediawiki.org/wiki/Wikibase/API#wbcreateclaim + * + * @param fileName + * @return + */ + private String getFileName(String fileName) { + fileName = String.format("\"%s\"", fileName.replace("File:", "")); + Timber.d("Wikidata property name is %s", fileName); + return fileName; + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_primary_24dp.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_primary_24dp.png new file mode 100644 index 000000000..fe508b450 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_arrow_back_primary_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_delete_grey_700_24dp.png b/app/src/main/res/drawable-hdpi/ic_delete_grey_700_24dp.png new file mode 100644 index 000000000..de4a147fe Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_delete_grey_700_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png new file mode 100644 index 000000000..42407d292 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notifications_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_notifications_none_black_24dp.png b/app/src/main/res/drawable-hdpi/ic_notifications_none_black_24dp.png new file mode 100644 index 000000000..ddbd001d5 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_notifications_none_black_24dp.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png new file mode 100644 index 000000000..a3210742d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-ldpi/ic_explore_24dp.xml b/app/src/main/res/drawable-ldpi/ic_explore_24dp.xml new file mode 100644 index 000000000..6b0553ff1 --- /dev/null +++ b/app/src/main/res/drawable-ldpi/ic_explore_24dp.xml @@ -0,0 +1,27 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable-mdpi/badge.xml b/app/src/main/res/drawable-mdpi/badge.xml new file mode 100644 index 000000000..3792e33cf --- /dev/null +++ b/app/src/main/res/drawable-mdpi/badge.xml @@ -0,0 +1,33 @@ + + + + + + + diff --git a/app/src/main/res/drawable-mdpi/ic_arrow_back_primary_24dp.png b/app/src/main/res/drawable-mdpi/ic_arrow_back_primary_24dp.png new file mode 100644 index 000000000..07e187a26 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_arrow_back_primary_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_delete_grey_700_24dp.png b/app/src/main/res/drawable-mdpi/ic_delete_grey_700_24dp.png new file mode 100644 index 000000000..b9d9082c8 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_delete_grey_700_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png new file mode 100644 index 000000000..8e71d5461 Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notifications_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_notifications_none_black_24dp.png b/app/src/main/res/drawable-mdpi/ic_notifications_none_black_24dp.png new file mode 100644 index 000000000..2c5d658aa Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_notifications_none_black_24dp.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png new file mode 100644 index 000000000..97351f02f Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_arrow_back_primary_24dp.png b/app/src/main/res/drawable-xhdpi/ic_arrow_back_primary_24dp.png new file mode 100644 index 000000000..91d7ddcbf Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_arrow_back_primary_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_delete_grey_700_24dp.png b/app/src/main/res/drawable-xhdpi/ic_delete_grey_700_24dp.png new file mode 100644 index 000000000..fa573b1e3 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_delete_grey_700_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png new file mode 100644 index 000000000..19c2eeee1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notifications_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_notifications_none_black_24dp.png b/app/src/main/res/drawable-xhdpi/ic_notifications_none_black_24dp.png new file mode 100644 index 000000000..56a194cc1 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_notifications_none_black_24dp.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png new file mode 100644 index 000000000..f9d04518b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_arrow_back_primary_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_primary_24dp.png new file mode 100644 index 000000000..ec21c6d59 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_arrow_back_primary_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_delete_grey_700_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_delete_grey_700_24dp.png new file mode 100644 index 000000000..e6fc65fc0 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_delete_grey_700_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png new file mode 100644 index 000000000..dc4c15d4d Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notifications_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_notifications_none_black_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_notifications_none_black_24dp.png new file mode 100644 index 000000000..8467b3e1e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_notifications_none_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png new file mode 100644 index 000000000..722ff8195 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_primary_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_primary_24dp.png new file mode 100644 index 000000000..7bac2fce5 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_arrow_back_primary_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_delete_grey_700_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_delete_grey_700_24dp.png new file mode 100644 index 000000000..10e270287 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_delete_grey_700_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png new file mode 100644 index 000000000..2360a350b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notifications_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_notifications_none_black_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_notifications_none_black_24dp.png new file mode 100644 index 000000000..f0237c57e Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_notifications_none_black_24dp.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png b/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png new file mode 100644 index 000000000..47cc04fbe Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_search_white_24dp.png differ diff --git a/app/src/main/res/drawable/featured.xml b/app/src/main/res/drawable/featured.xml new file mode 100644 index 000000000..e971c3446 --- /dev/null +++ b/app/src/main/res/drawable/featured.xml @@ -0,0 +1,1069 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml b/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml new file mode 100644 index 000000000..e3f68ad02 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_outline_blue_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_person_black_24dp.xml b/app/src/main/res/drawable/ic_person_black_24dp.xml new file mode 100644 index 000000000..d7366bda0 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_black_24dp.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_star_black_24dp.xml b/app/src/main/res/drawable/ic_star_black_24dp.xml deleted file mode 100644 index a87ca098d..000000000 --- a/app/src/main/res/drawable/ic_star_black_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_thanks.xml b/app/src/main/res/drawable/ic_thanks.xml new file mode 100644 index 000000000..480b45c1b --- /dev/null +++ b/app/src/main/res/drawable/ic_thanks.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/layout/activity_achievements.xml b/app/src/main/res/layout/activity_achievements.xml new file mode 100644 index 000000000..293234f09 --- /dev/null +++ b/app/src/main/res/layout/activity_achievements.xml @@ -0,0 +1,324 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_category_details.xml b/app/src/main/res/layout/activity_category_details.xml new file mode 100644 index 000000000..04b50074a --- /dev/null +++ b/app/src/main/res/layout/activity_category_details.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml new file mode 100644 index 000000000..dce1ef163 --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,93 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/drawer_header.xml b/app/src/main/res/layout/drawer_header.xml index 9bd3ae3a3..53a2684a0 100644 --- a/app/src/main/res/layout/drawer_header.xml +++ b/app/src/main/res/layout/drawer_header.xml @@ -1,5 +1,6 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_browse_image.xml b/app/src/main/res/layout/fragment_browse_image.xml new file mode 100644 index 000000000..af6a7860b --- /dev/null +++ b/app/src/main/res/layout/fragment_browse_image.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_category_images.xml b/app/src/main/res/layout/fragment_category_images.xml index 001f0a780..f317b2f23 100644 --- a/app/src/main/res/layout/fragment_category_images.xml +++ b/app/src/main/res/layout/fragment_category_images.xml @@ -1,6 +1,7 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_single_upload.xml b/app/src/main/res/layout/fragment_single_upload.xml index 37e43ee81..00cc1d194 100644 --- a/app/src/main/res/layout/fragment_single_upload.xml +++ b/app/src/main/res/layout/fragment_single_upload.xml @@ -55,11 +55,23 @@ - + android:layout_height="wrap_content"> + + +
+ + \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_image_detail.xml b/app/src/main/res/menu/fragment_image_detail.xml index e864dddb2..e0970e256 100644 --- a/app/src/main/res/menu/fragment_image_detail.xml +++ b/app/src/main/res/menu/fragment_image_detail.xml @@ -4,7 +4,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-ab/strings.xml b/app/src/main/res/values-ab/strings.xml index 76caf25de..2f91f2074 100644 --- a/app/src/main/res/values-ab/strings.xml +++ b/app/src/main/res/values-ab/strings.xml @@ -14,7 +14,7 @@ Аҭалара қәҿиарала имҩаҧысит! Асистемахь аҭалараан агха! Афаил ҧшаам. Даҽа фаилк шәахәаҧш. - Аутентификациа агха! + Аутентификациа агха! Аҭагалара иалагоуп! %1$s иҭагалоуп! Шәақәыӷәӷәа иҭагалоу афаил ахәаҧшраз diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 6d9c94c1d..40f7e2176 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -15,8 +15,12 @@ * مشعل الحربي --> + المظهر عام + التعليقات + الموقع كومنز + الإعدادات اسم المستخدم كلمة السر @@ -27,7 +31,7 @@ تم الدخول بشكل صحيح! فشل تسجيل الدخول الملف غير موجود. فضلا اختر ملفا آخر. - فشل الاستيقان! + فشل الاستيقان! بدأ الرفع! رُفع %1$s! انقر لعرض ملفك المرفوع @@ -54,8 +58,6 @@ العنوان الوصف لا يمكن تسجيل الدخول - فشل في شبكة الاتصال - لا يمكن تسجيل الدخول - فضلا تحقق من اسم المستخدم - لا يمكن تسجيل الدخول - فضلا تحقق من كلمة السر الكثير من المحاولات غير الناجحة. الرجاء المحاولة مرة أخرى في بضع دقائق. عذراً، لقد تم منع هذا المستخدم على كومنز يجب توفير رمز التحقق المزدوج. @@ -67,6 +69,7 @@ تصنيفات البحث احفظ أنعش + القائمة عطل نظام الملاحة العالمي GPS بجهازك. أترغب في التنشيط؟ تفعيل GPS لا مرفوعات بعد @@ -76,6 +79,7 @@ تصنيفات الإعدادات سجّل + الصور المختارة حول برنامج مفتوح المصدر منشور <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">برخصة أباتشي النسخة 2</a> المصدر على <a href=\"https://github.com/commons-app/apps-android-commons\">غِتهب</a>. العلل في <a href=\" https://github.com/commons-app/apps-android-commons/issues\">بغزِلا</a>. @@ -91,9 +95,10 @@ إلغاء ستُرخص الصورة برخصة %1$s. نزّل - الرخصة + الرخصة الافتراضية الوضع الليلي استخدم النمط الغامق + CC0 الرجاء عدم رفع - صور ذاتية أو صور لأصدقاء\n- صور محملة من الانترنت\n- لقطات شاشة ذات ملكية خاصة مثال رفع: @@ -118,6 +123,7 @@ العنوان عنوان الوسيط الوصف + المؤلف تاريخ الرفع الترخيص الإحداثيات @@ -130,6 +136,22 @@ تحديد حد الرفع الحالي التحقق المزدودج 2FA غير مدعوم حاليا أترغب فعلا في الخروج؟ + شعار كومنز + موقع كومنز + صفحة فيسبوك الخاصة بكومنز + صورة الخلفية + لم يتم العثور على صورة + رفع صورة + جبل زوا + لاماس + جسر قوس قزح + تيوليب + لا صور سيلفي + صورة ذات حقوق + مرحبا في ويكيبيديا + حقوق نسخ الترحيب + بيت أوبرا سيدني + إلغاء أفتح أغلق الرئيسية @@ -141,8 +163,28 @@ تسجيل الخروج مقدمة تعليمية إشعارات + مميزة + لم يتم العثور على وصف صفحة ملف كومنز + عنصر ويكي بيانات + مقالة ويكيبيديا + خطأ أثناء تخزين الصور + إعطاء السماح + استخدم تخزينا خارجيا + تسجيل الدخول إلى حسابك + أرسل ملف السجل + ترشيح للحذف + الصورة تم ترشيحها للحذف. + عرض في المتصفح + الموقع لم يتغير. + الموقع غير متوفر. + الحصول على تعليمات الاتجاه + قراءة المقالة مرحبا بكم في ويكيمديا كومنز، %1$s! نحن سعداء لأنك هنا. %1$s رسالة على صفحة الحديث + شكرا لك لعمل تعديل %1$s ذكر لك على %2$s. + شارك التطبيق + صورة اليوم + صورة اليوم diff --git a/app/src/main/res/values-ast/strings.xml b/app/src/main/res/values-ast/strings.xml index 532a4897e..cfc0314c1 100644 --- a/app/src/main/res/values-ast/strings.xml +++ b/app/src/main/res/values-ast/strings.xml @@ -21,7 +21,7 @@ ¡Identificación correuta! ¡Falló l\'aniciu de sesión! Nun s\'alcontró\'l ficheru. Tenta con otru. - ¡Falló la identificación! + Falló la identificación, anicia sesión nuevamente Principió la xuba ¡%1$s xubíu! Toque pa ver la xuba @@ -49,8 +49,7 @@ Apurre un títulu pa esti ficheru Descripción Nun se pudo aniciar sesión – error de rede - Nun se pudo aniciar sesión – por favor compruebe\'l so nome d\'usuariu - Nun se pudo aniciar sesión – por favor compruebe la so contraseña + Nun pudo aniciase sesión. Revisa\'l nome d\'usuariu y la contraseña Demasiaos intentos incorreutos. Téntalo otra vuelta n\'unos minutos. Sentímoslo, esti usuariu ta bloquiáu en Commons Tienes de dar el códigu d\'identificación de dos factores. @@ -84,6 +83,7 @@ Categoríes Configuración Date d\'alta + Imáxenes destacaes Tocante a La app de Wikimedia Commons ye software de códigu abiertu, creáu y calteníu por becaos y voluntarios de la comunidá de Wikimedia. La Fundación Wikimedia nun participa na creación, desendolcu nin caltenimientu de la app. Crea una nueva <a href=\"https://github.com/commons-app/apps-android-commons/issues\">incidencia en GitHub</a> pa informar de problemes y suxerencies. @@ -103,7 +103,7 @@ Llicencia predeterminada Usar un títulu/descripción anterior Llograr automáticamente l\'allugamientu actual - Recuperar l\'allugamientu actual pa ufiertar suxerencies de categoríes si la imaxe nun tien etiquetes xeográfiques + Recupera la posición actual si la imaxe nun tien etiquetes xeográfiques, y marca la imaxe con ella. Atención: Esto revelará\'l to allugamientu actual. Mou nocherniegu Usar tema escuru Reconocimientu-CompartirIgual 4.0 @@ -168,7 +168,9 @@ Títulu Títulu del mediu Descripción - Equí va la descripción del mediu. Esto pué ser llargo enforma, y necesitará espardese per delles llinies. Sicasí, esperamos que se vea bien. + Equí va la descripción del mediu. Esta podría ser llarga enforma, y necesitará espardese per delles llinies. Sicasí, esperamos que se vea bien. + Autor + El nome d\'usuariu del autor de la imaxe destacada va equí. Data d\'unviu Llicencia Coordenaes @@ -211,13 +213,14 @@ Salir Tutorial Avisos + Destacada Los sitios cercanos nun pueden amosase ensin los permisos d\'allugamientu nun s\'atoparon descripciones Páxina del ficheru en Commons Elementu de WikiData Artículu de Wikipedia Error al poner les fotos na caché - Un títulu descriptivu únicu pal ficheru, que sirvirá para da-y nome al ficheru. Se pue usar llinguax normal con espacios. Nun amiestes la estensión del ficheru + Un títulu descriptivu únicu pal ficheru, que sirvirá para da-y nome al mesmu. Puede usase llinguaxe normal con espacios. Nun incluyas la estensión del ficheru Por favor, describi l\'elementu multimedia tantu como sía posible: ¿ónde se tomó?, ¿qué amuesa?, ¿cuál ye\'l contestu? Por favor, describi los oxetos o persones. Revela la información que nun pueda aldovinase de mou cenciellu, por casu el momentu del día si ye un paisaxe. Si\'l mediu amuesa daqué desacostumao, esplica qué lo fai raro. Esta imaxe ye escura enforma, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu. Esta imaxe ta borrosa, ¿tas seguru de que quies xubila? Wikimedia Commons ye sólo pa imáxenes con valor enciclopédicu. @@ -259,4 +262,20 @@ Siguir Encaboxar Retentar + Entendílo + Estos son sitios cercanos a ti que precisen imaxes para ilustrar los sos artículos de Wikipedia + Tocando esti botón amuésase la llista d\'esos llugares + Puedes xubir una imaxe pa cualquier sitiu dende la galería o la cámara + Nun s\'alcontró nenguna imaxe + Asocedió un error al cargar les imáxenes. + Xubida por: %1$s + Compartir app + Nun s\'especificaron les coordenaes al escoyer la imaxe + Error al llograr los llugares cercanos. + Semeya del día + Semeya del día + Añadióse correutamente la imaxe a %1$s en Wikidata. + Nun pudo anovase la entidá de Wikidata correspondiente. + Definir fondu + Fondu definíu correutamente diff --git a/app/src/main/res/values-b+sr+Latn/strings.xml b/app/src/main/res/values-b+sr+Latn/strings.xml index 0cd72a167..557be7714 100644 --- a/app/src/main/res/values-b+sr+Latn/strings.xml +++ b/app/src/main/res/values-b+sr+Latn/strings.xml @@ -16,7 +16,7 @@ Uspešno ste prijavljeni. Prijavljivanje nije uspelo. Datoteka nije pronađena. Pokušajte sa drugom datotekom. - Provera identiteta nije uspela. + Provera identiteta nije uspela. Otpremanje je započeto. Datoteka „%1$s“ je otpremljena. Tapnite da biste videli otpremanje @@ -43,8 +43,6 @@ Naslov Opis Ne mogu da vas prijavim – mreža ne radi - Ne mogu da vas prijavim – proverite svoje korisničko ime - Ne mogu da vas prijavim – proverite svoju lozinku Previše neuspešnih pokušaja. Probajte ponovo za nekoliko minuta. Nažalost, korisnik je blokiran na Ostavi Morate uneti svoj dvofaktorski kod za autentifikaciju. @@ -96,7 +94,7 @@ Licenca Koristi prethodan naslov/opis Automatski detektuj trenutnu lokaciju - Primi trenutnu lokaciju da bi predložili kategoriju ako slika nije geografski označena + Primi trenutnu lokaciju da bi predložili kategoriju ako slika nije geografski označena Noćni režim Koristiti tamnu temu Autorstvo-Deliti pod istim uslovima 4.0 diff --git a/app/src/main/res/values-ba/strings.xml b/app/src/main/res/values-ba/strings.xml index 293dd3f73..f91a69803 100644 --- a/app/src/main/res/values-ba/strings.xml +++ b/app/src/main/res/values-ba/strings.xml @@ -23,7 +23,7 @@ Танышыу уңышлы үтте Танылыу хатаһы Файл табылманы. Башҡа файлды эҙлә. - Кем икәнегеҙ танылманы! + Кем икәнегеҙ танылманы! Тейәү башланды! %1$s тейәлде! Ошонда баҫып тейәлгән файлды ҡара @@ -50,8 +50,6 @@ Был файлдың атамаһын күрһәт Тасуирлама Инеп булмай - интернет хатаһы - Инмәнең - ҡулланыусы исемеңде тикшер - Инмәнең - серһуҙеңде тикшер Күп тапҡыр яңылыштың. Зинһар, бер-нисә минуттан тағы ла инеп ҡара Ғәфү итегеҙ, әммә был исемдәге ҡатнашыусыға Викискладҡа инеү тыйылған Ике тапҡыр раҫлай торған шәхси кодты яҙырға кәрәк @@ -102,7 +100,7 @@ Нығытылған рөхсәтнамә Алдағы атама/һәрәтләмәне ҡулланыу Автомат рәүешендә сираттағы урынды алыу - Әгәр рәсемдең геотегтары булмаһа, категориялар үҙенән-үҙе тәҡдим ителһен өсөн сираттағы урынды алырға + Әгәр рәсемдең геотегтары булмаһа, категориялар үҙенән-үҙе тәҡдим ителһен өсөн сираттағы урынды алырға Төнгө режим Ҡараңғы теманы ҡулланыу Attribution-ShareAlike 4.0 diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index cfaf021f9..d6a675bf2 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -18,7 +18,7 @@ Успешно влизане. Неуспешно влизане! Файлът не е намерен. Моля, опитайте с друг файл. - Неуспешен опит за удостоверяване! + Неуспешен опит за удостоверяване! Качването започна! Файл %1$s е качен! Докоснете, за да видите качения файл @@ -32,8 +32,6 @@ Заглавие Описание Неуспешно влизане – проблем в мрежата - Неуспешно влизане – моля проверете потребителското си име - Неуспешно влизане – моля проверете паролата си Качване Изменения Качване diff --git a/app/src/main/res/values-bn/error.xml b/app/src/main/res/values-bn/error.xml index 2c929f8b5..66984d45f 100644 --- a/app/src/main/res/values-bn/error.xml +++ b/app/src/main/res/values-bn/error.xml @@ -3,6 +3,7 @@ * Aftabuzzaman * Bellayet * Sankarshan +* আফতাবুজ্জামান --> কমন্স ক্র্যাশ করেছে diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index 679c50803..1cc3692b4 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -9,8 +9,11 @@ * Rasal Lia * Sankarshan * Tauhid16 +* আফতাবুজ্জামান --> + অন্বেষণ করুন + অন্বেষণ অবয়ব সাধারণ প্রতিক্রিয়া @@ -18,6 +21,7 @@ কমন্স সেটিং + কমন্সে আপলোড করুন ব্যবহারকারী নাম পাসওয়ার্ড আপনার কমন্স বিটা অ্যাকাউন্টে প্রবেশ করুন @@ -29,7 +33,7 @@ প্রবেশ সফল! প্রবেশ ব্যর্থ :( ফাইল পাওয়া যায়নি। আরেকটি ফাইল চেষ্টা করুন। - প্রমাণীকরণ ব্যর্থ হয়েছে! + প্রমাণীকরণ ব্যর্থ হয়েছে, আবার প্রবেশ করুন আপলোড আরম্ভ হয়েছে! %1$s আপলোড হয়েছে! আপনার আপলোড দেখতে টোকা দিন @@ -57,8 +61,6 @@ এই ফাইলটির জন্য একটি শিরোনাম প্রদান করুন বিবরণ প্রবেশ করা যাচ্ছে না - নেটওয়ার্ক ব্যর্থতা - প্রবেশ করা যাচ্ছে না - অনুগ্রহ করে আপনার ব্যবহারকারী নাম পরীক্ষা করুন। - প্রবেশ করা যাচ্ছে না - অনুগ্রহ করে আপনার পাসওয়ার্ড পরীক্ষা করুন খুব বেশি অসফল প্রচেষ্টা। অনুগ্রহ করে কয়েক মিনিট পরে আবারও চেষ্টা করুন। দুঃখিত, এই ব্যবহারকারীকে কমন্সে বাধা দেয়া হয়েছে অাপনাকে অবশ্যই অাপনার দু\'স্তরের সত্যায়নকরণ কোড দিতে হবে। @@ -93,6 +95,7 @@ সেটিং নিবন্ধন করুন নির্বাচিত ছবি + বিষয়শ্রেণী পরিচিতি উইকিমিডিয়া কমন্স অ্যাপ হচ্ছে একটি উন্মুক্ত উৎস সম্বলিত অ্যাপ যা উইকিমিডিয়া সম্প্রদায়ের ব্যবহারকারী ও সেচ্ছাসেবকবৃন্দ কর্তৃক তৈরিকৃত এবং পরিচালিত। উইকিমিডিয়া ফাউন্ডেশন এই অ্যাপ তৈরি, উন্নয়ন বা রক্ষণাবেক্ষণে জড়িত নয়। কোন সমস্যা ও পরামর্শের জন্য <a href=\"https://github.com/commons-app/apps-android-commons/issues\">গিটহাব ইস্যু</a> তৈরি করুন। @@ -112,7 +115,7 @@ পূর্বনির্ধারিত লাইসেন্স পূর্ববর্তী শিরোনাম/বিবরণ ব্যবহার করুন স্বয়ংক্রিয়ভাবে বর্তমান অবস্থান পান - বিষয়শ্রেণীর পরামর্শ দিতে বর্তমান অবস্থান পান যদি ছবিতে ভূ-ট্যাগ না থেকে থাকে + বিষয়শ্রেণীর পরামর্শ দিতে বর্তমান অবস্থান পান যদি ছবিতে ভূ-ট্যাগ না থেকে থাকে রাত্রি মোড কালো থিম ব্যবহার করুন অ্যাট্রিবিউশন-শেয়ারঅ্যালাইক ৪.০ @@ -258,5 +261,14 @@ ইন্টারনেট অনুপলব্ধ ইন্টারনেট উপলব্ধ কোন বিজ্ঞপ্তি পাওয়া যায়নি + ভাষাসমূহ + বাতিল পুনঃচেষ্টা করুন + বুঝেছি! + কোন চিত্র পাওয়া যায়নি! + আপলোড করেছেন: %1$s + কাছাকাছি স্থানগুলি আনতে ত্রুটি। + অনুসন্ধান + কমন্সে অনুসন্ধান করুন + অনুসন্ধান diff --git a/app/src/main/res/values-br/strings.xml b/app/src/main/res/values-br/strings.xml index 8383f7cbe..90ede0b76 100644 --- a/app/src/main/res/values-br/strings.xml +++ b/app/src/main/res/values-br/strings.xml @@ -3,6 +3,7 @@ * Dishual * Fohanno * Fulup +* Gwendal * Gwenn-Ael * Y-M D --> @@ -24,7 +25,7 @@ Kevreet oc\'h ! Kudenn gevreañ ! N\'eo ket bet kavet ar restr. Klask gant unan all. - Dilesadur c\'hwitet! + Dilesadur c\'hwitet! Kroget da enporzhiañ! %1$s bet enporzhiet ! Pouezit evit gwelet hoc\'h enporzhiadenn @@ -52,8 +53,6 @@ Roit un titl d\'ar restr-mañ, mar plij Deskrivadur Ne c\'haller ket kevreañ - rouedad sac\'het - Ne c\'haller ket kevreañ - Gwiriit hoc\'h anv implijer, mar plij - Ne c\'haller ket kevreañ - Gwiriit ho ker tremen, mar plij Re a daolioù-esae. Klaskit en-dro a-benn ur pennadig amzer. Hon digarezit, prennet eo bet an implijer-mañ e Commons Rankout a rit reiñ ho kod dilesa gant daou faktor. @@ -104,7 +103,7 @@ Aotre-implijout dre ziouer Ober gant an titl/deskrivadur kent Tapout al lec\'hiadur red ent emgefre - Kavout al lec\'hiadur red evit pourchas kinnigoù rummadoù ma n\'eo ket douarlec\'hiet ar skeudenn. + Kavout al lec\'hiadur red evit pourchas kinnigoù rummadoù ma n\'eo ket douarlec\'hiet ar skeudenn. Mod noz Ober gant an tem teñval Deroadur-RannañHeñvel 4.0 @@ -219,4 +218,5 @@ N\'eo ket cheñchet al lec\'hiadur. Kaout urzhioù Lenn ar pennad + Skeudenn an deiz diff --git a/app/src/main/res/values-bs/strings.xml b/app/src/main/res/values-bs/strings.xml index 6c0097089..1a8253fa3 100644 --- a/app/src/main/res/values-bs/strings.xml +++ b/app/src/main/res/values-bs/strings.xml @@ -16,7 +16,7 @@ Prijavljivanje uspješno! Prijavljivanje nije uspjelo! Datoteka nije pronađena. Pokušajte drugu. - Provjera identiteta nije uspjela! + Provjera identiteta nije uspjela! Postavljanje je započelo! Datoteka %1$s je postavljena! Dodirnite da biste vidjeli datoteku @@ -43,8 +43,6 @@ Naslov Opis Ne mogu Vas prijaviti – mreža ne radi - Ne mogu Vas prijaviti – provjerite svoje korisničko ime - Ne mogu Vas prijaviti – provjerite svoje lozinku Napravili ste previše grešaka u prijavi. Pokušajte ponovo za nekoliko minuta. Žao nam je, korisnik je blokiran na Commonsu Morate upisati kôd za potvrdu u 2 koraka. @@ -95,7 +93,7 @@ Licenca Koristi prethodni naziv/opis Automatski dobavi trenutnu lokaciju - Dobavi trenutnu lokaciju za davanje prijedloga o kategorijama ako nema geooznaku + Dobavi trenutnu lokaciju za davanje prijedloga o kategorijama ako nema geooznaku Noćni režim Koristi tamnu temu Autorstvo-Dijeliti pod istim uslovima 4.0 diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index f7f40f427..98a120f00 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -23,7 +23,7 @@ S\'ha iniciat sessió correctament! Error en iniciar la sessió! No s\'ha trobat el fitxer. Proveu-ho amb un altre fitxer. - L\'autenticació ha fallat! + L’autenticació ha fallat. Torneu a provar d’iniciar una sessió. Ha començat la càrrega! S’ha pujat %1$s. Prem per veure la teva càrrega @@ -50,8 +50,6 @@ Títol Descripció No s\'ha pogut iniciar la sessió – error de xarxa - No s\'ha pogut iniciar la sessió – si et plau comprova el teu nom d\'usuari - No s’ha pogut iniciar la sessió. Comproveu la vostra contrasenya Massa intents erronis – Proveu-ho de nou d\'aquí uns minuts. Ho sentim, aquest usuari ha estat blocat a Commons Heu de proporcionar el vostre codi d\'autenticació de dos factors. @@ -192,4 +190,5 @@ Gràcies per fer una modificació %1$s us ha mencionat a %2$s. Preguntes freqüents + No s’ha trobat cap imatge. diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 6e2453d6d..192656956 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -33,7 +33,7 @@ Přihlášení uspělo! Přihlášení se nezdařilo! Soubor nebyl nalezen. Prosím, zkuste jiný soubor. - Ověření se nezdařilo! + Ověření se nezdařilo, prosím přihlaste se znovu Nahrávání začalo! %1$s nahráno! Klepnutím zobrazíte upload @@ -61,8 +61,6 @@ Vložte prosím název tohoto souboru Popis Nelze se přihlásit - selhání sítě - Nelze se přihlásit - prosím zkontrolujte své uživatelské jméno - Nelze se přihlásit - zkontrolujte prosím své heslo Příliš mnoho neúspěšných pokusů. Zkuste to prosím znovu za několik minut. Omlouváme se, tento uživatel byl na Commons zablokován Prosím vložte kód pro své dvoufázové ověření. @@ -115,7 +113,7 @@ Výchozí licence Použít předchozí název a popis Automaticky získat aktuální polohu - Nabídnout kategorie na základě aktuální polohy (pokud není obrázek opatřen souřadnicemi) + Nabídnout kategorie na základě aktuální polohy (pokud není obrázek opatřen souřadnicemi) Noční režim Použít tmavý režim Uveďte autora-Zachovejte licenci 4.0 diff --git a/app/src/main/res/values-csb/strings.xml b/app/src/main/res/values-csb/strings.xml index 2d7b99904..3fab9c6fb 100644 --- a/app/src/main/res/values-csb/strings.xml +++ b/app/src/main/res/values-csb/strings.xml @@ -14,7 +14,7 @@ Ùdałi logòwanié! Logòwanié nie darzëło sã! Felënk lopka. Proszã spróbòwac znowa. - Fela ùdowierzaniô! + Fela ùdowierzaniô! Wladënk zrëszony! %1$s wladowóné! Tkni, abë òbôczëc ladowóny lopk @@ -41,8 +41,6 @@ Titel Òpisënk Ni mòże sã wlogòwac - fela sécë - Ni mòże sã wlogòwac - sprôwdzë miono brëkòwnika - Ni mòże sã wlogòwac - sprôwdzë parolã Za wiele nieùdałich prób wlogòwaniô. Spróbùjë znowa za czile minut. Nen brëkòwnik òstôł zablokòwóny na Commons Mùszisz wpisac swój kòd dlô dwafaktorowi aùtorizacëji. diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml index ef56bc3b0..134c2f116 100644 --- a/app/src/main/res/values-cy/strings.xml +++ b/app/src/main/res/values-cy/strings.xml @@ -5,18 +5,25 @@ * Robin Owain --> + Ymddangosiad + Cyffredinol + Adborth + Lleoliad Comin Wicimedia + Gosodiadau Enw defnyddiwr Cyfrinair + Mewngofnodwch i\'ch cyfri Comin Beta Mewngofnodi + Anghofiwyd y Cyfrinair? Cofrestru Wrthi\'n mewngofnodi Disgwyliwch… Llwyddodd y mewngofnodi! Methodd y mewngofnodi! Ni chafwyd hyd i\'r ffeil. Ceisiwch un arall. - Methodd y dilysu! + Methodd y dilysu! Mewngofnodwch eto. Dechreuodd yr uwchlwytho! Uwchlwythwyd %1$s! Tapiwch i weld eich uwchlwythiad @@ -41,10 +48,10 @@ Rhannu Agor yn y Porwr Teitl + Rhowch deitl i\'r ffeil Disgrifiad Yn methu mewngofnodi - methodd y rhwydwaith - Yn methu mewngofnodi - gwirwch eich enw defnyddiwr - Yn methu mewngofnodi - gwirwch eich cyfrinair + Methwyd mewngofnodi - gwirwch eich enw defnyddiwr a\'ch cyfrinair Cafwyd gormod o ymgeision aflwyddiannus. Oedwch ennyd cyn ceisio eto. Ymddiheurwn. Mae\'r defnyddiwr hwn wedi ei flocio ar Gomin Wikimedia Mae\'n rhaid i chi roi eich cod adnabod 2 ffactor. @@ -56,6 +63,7 @@ Archwilio\'r categorïau Cadw Ailgyrchu + Rhestr Ataliwyd GPS ar eich dyfais. Ydych chi am ei droi\'n weithredol? Gweithredu\'r GPS Heb uwchlwytho eto @@ -77,11 +85,12 @@ Categorïau Gosodiadau Cofrestru + Delweddau nodwedd Amdanom Ap Cynnwys Agored a grewyd ac a gefnogir gan wirfoddolwyr cymuned Wicimedia yw ap Comin Wicimedia. Does a wnelo Sefydliad Wicimedia ddim byd ag e (ei greu, ei gynnal na\'i ddatblygu). \n\nCrewch <a href=\"https://github.com/commons-app/apps-android-commons/issues\">ymholiad GitHub</a> os oes gennych fyg, broblem neu awgrym. - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Polisi Preifatrwydd</a> - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Clod</a> + <u>Polisi preifatrwydd</u> + Clod a bri Amdanom Danfonwch Adborth (drwy Ebost) Dim ebost client wedi\'i ganfod @@ -93,10 +102,10 @@ Caiff y ddelwedd hon ei thrwyddedu yn ôl termau\'r drwydded %1$s. Wrth gynnig y llun yma, rwy\'n datgan mai fy ngwaith i ydyw ac nad yw\'n cynnwys unrhyw beth dan hawlfrain, na hunlun, a\'i fod yn cadw at <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Bolisiau Comin Wicimedia</a>. Lawrlwytho - Trwydded + Trwydded Ddiofyn (\'default\') Defnydiwch y teitl/disgrifiad blaenorol Defnyddiwch y lleoliad cyfredol - Canfyddwch eich lleoliad, er mwyn i ni gynnig categori (os nad ydych wedi nodi\'r cyfesurynnau). + Adfer eich lleoliad presennol os nad yw\'r ddelwedd yn cynnwys cyfesurynnau. Bydd hyn yn datgelu eich lleoliad chi! Modd fin nos Defnyddiwch thema tywyll Attribution-ShareAlike 4.0 @@ -122,9 +131,19 @@ Cyhelir llawer o luniau ar Gomin Wicimedia sy\'n cael eu defnyddio ar Wicipedia. Mae eich lluniau\'n gymorth i addysgu pobl drwy\'r byd mawr crwn! Uwchlwythwch lluniau a dynnoch eich hun: - - Natur (blodau, anifeiliaid, mynyddoedd)\n- Pethau defnyddiol (beic, tren, gorsaf drenau)\n- Enwogion (beirdd, athletwyr, blogwyr) + Gwrthrychau byd natur (blodau, anifeiliaid, mynyddoedd)\n- Gwrthrychau defnyddiol (beics, trenau, gorsafoedd trenau)\n- Enwogion (beirdd, athletwyr, blogwyr) + Gwrthrychau byd natur (blodau, anifeiliaid, mynyddoedd) + Gwrthrychau defnyddiol (beics, trenau, gorsafoedd trenau) + Enwogion (beirdd, athletwyr, blogwyr) Peidiwch ag uwchlwytho: - hunanluniau ohonoch chi na\'ch ffrindiau\n- lluniau a gawsoch o\'r we\n- sgrinluniau o apiau masnachol + Hunanluniau neu luniau o\'ch ffrindiau + Lluniau a lawrlwythwyd o\'r we gennych + Sgrinluniau o aps + Enghraifft o uwchlwythiad: + Teitl:Tŷ Opera Sydney + Disgrifiad: Golygfa o Dŷ Opera Sydney o ochr arall y bae + Categoriau: Tŷ Opera Sydney o\'r gorllewin Cyfranwch luniau. Cynorthwywch Wicipedia i roi bywyd yn yr erthyglau! Mae\'r delweddau ar Wicipedia\'n dod o\nGomin Wikimedia. Mae eich delweddau\'n cynorthwyo i addysgu pobl ledled y byd. @@ -137,6 +156,32 @@ Dim disgrifiad Trwydded anhysbys Adnewyddu + Iawn + Lleoedd Cyfagos + Ni chafwyd hyd i leoedd cyfagos + Rhybudd + Ydw + Nac ydw Teitl + teitl y cyfrwng Disgrifiad + Awdur + Dyddiad yr uwchlwythiad + Trwydded + Cyfesurynnau + Dim + Eitem Wicidata + Erthygl Wicipedia + Mewngofnodwch i\'ch cyfri + Danfonwch y ffeil log + Gweld yn y porwr + Nid yw\'r lleoliad wedi newid. + Nid yw\'r lleoliad ar gael. + Parhau + Canslo + Ailgeisio + Gwnaed! + Llun y Dydd + Llun y Dydd + Mae %1$s o luniau wedi\'u hychwanegu ar Wicidata! diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index e10a93850..4a724e640 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -26,7 +26,7 @@ Du er nu logget på! Det mislykkedes at logge på! Filen blev ikke fundet. Forsøg med en anden fil. - Godkendelse mislykkedes! + Godkendelse mislykkedes! Overførsel begyndt! %1$s overført! Tryk for at få vist din upload @@ -54,8 +54,6 @@ Angiv venligt en titel for denne fil Beskrivelse Kan ikke logge på - netværksfejl - Ude af stand til at logge på - tjek venligst dit brugernavn - Ude af stand til at logge på - tjek venligst din adgangskode Alt for mange mislykkede forsøg. Prøv igen om et par minutter. Beklager, denne bruger er blevet blokeret på Commons Du skal angive din tofaktorgodkendelseskode. @@ -107,7 +105,7 @@ Standardlicens Brug forrige titel/beskrivelse Hent automatisk nuværende placering - Hent nuværende placering for at tilbyde kategoriforslag hvis billedet ikke er geografisk mærket + Hent nuværende placering for at tilbyde kategoriforslag hvis billedet ikke er geografisk mærket Nat-tilstand Brug mørkt tema Attribution-ShareAlike 4.0 diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 36e013976..2ab99221e 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -1,12 +1,15 @@ + Entdecken + Entdecken Erscheinung Allgemein Rückmeldung @@ -14,6 +17,7 @@ Commons Einstellungen + Zu Commons hochladen Benutzername Passwort Bei deinem Commons-Beta-Benutzerkonto anmelden @@ -24,13 +28,13 @@ Bitte warten … Anmeldung erfolgreich! Anmeldung fehlgeschlagen! - Datei nicht gefunden. Bitte versuche es mit einer anderen. - Authentifizierung fehlgeschlagen! + Datei nicht gefunden. Bitte versuche es mit einer anderen Datei. + Authentifizierung fehlgeschlagen. Bitte erneut anmelden. Hochladen gestartet! „%1$s“ hochgeladen! Tippe, um deinen Upload anzusehen Hochladen von „%1$s“ starten - „%1$s“ hochladen + „%1$s“ wird hochgeladen Hochladen von „%1$s“ abschließen Hochladen von „%1$s“ fehlgeschlagen Tippen zum Betrachten @@ -53,8 +57,7 @@ Bitte gib einen Titel für diese Datei an Beschreibung Anmeldung fehlgeschlagen – Netzwerkfehler - Anmeldung fehlgeschlagen – Bitte Benutzernamen überprüfen - Anmeldung fehlgeschlagen – Bitte Passwort überprüfen + Anmeldung fehlgeschlagen. Bitte Benutzernamen und Passwort überprüfen. Zu viele erfolglose Versuche. Bitte in einigen Minuten erneut versuchen. Dieser Benutzer wurde leider auf Commons gesperrt Du musst deinen Code zur Zwei-Faktor-Authentifizierung angeben. @@ -89,6 +92,7 @@ Einstellungen Registrieren Vorgestellte Bilder + Kategorie Über Die Wikimedia-Commons-App ist eine Open-Source-App, entwickelt und gewartet von Freiwilligen der Wikimedia-Gemeinschaft. Die Wikimedia Foundation ist nicht bei der Erstellung, Entwicklung oder Wartung der App beteiligt. Einen neuen <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-Eintrag</a> für Fehlerberichte und Vorschläge erstellen. @@ -102,13 +106,13 @@ Du hast noch keine Fotos hochgeladen. Erneut versuchen Abbrechen - Dieses Bild wird lizenziert unter %1$s + Dieses Bild wird unter %1$s lizenziert Durch das Hochladen dieses Bildes erkläre ich, dass dies mein eigenes Werk ist, das kein urheberrechtlich geschütztes Material oder Selfies enthält und das auch sonst die <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/de\">Wikimedia-Commons-Richtlinien</a> einhält. Herunterladen Standardlizenz Vorherige(n) Titel/Beschreibung verwenden Aktuellen Standort automatisch abrufen - Ruft den aktuellen Standort ab, um Kategorievorschläge anzubieten, falls das Bild keine Geotags hat. + Ruft den aktuellen Standort ab, falls das Bild nicht georeferenziert ist und markiert es. Warnung: Diese Aktion verrät deinen aktuellen Standort. Nachtmodus Dunkles Thema verwenden Attribution-ShareAlike 4.0 @@ -197,6 +201,8 @@ Hintergrundbild Medienbild fehlgeschlagen Kein Bild gefunden + Keine Unterkategorien gefunden + Keine übergeordneten Kategorien gefunden Bild hochladen Zaō Lamas @@ -269,5 +275,32 @@ Fortfahren Abbrechen Erneut versuchen + Verstanden! + Dies sind die Orte in deiner Nähe, die Bilder zur Illustration ihrer Wikipedia-Artikel benötigen. + Das Antippen dieser Schaltfläche zeigt eine Liste mit diesen Orten + Du kannst ein Bild für einen beliebigen Ort von deiner Galerie oder Kamera hochladen + Keine Bilder gefunden! + Beim Laden der Bilder ist ein Fehler aufgetreten. + Hochgeladen von: %1$s + Du bist für das Bearbeiten auf Commons gesperrt App teilen + Während der Bildauswahl wurden keine Koordinaten angegeben + Fehler beim Abrufen der Orte in der Nähe. + Bild des Tages + Bild des Tages + Suchen + Commons durchsuchen + Keine Bilder mit dem Suchbegriff %1$s gefunden + Suchen + Letzte Suchanfragen: + Letzte Suchanfragen + Beim Laden der Kategorien ist ein Fehler aufgetreten. + Beim Laden der Unterkategorien ist ein Fehler aufgetreten. + Bild erfolgreich nach %1$s auf Wikidata hinzugefügt! + Fehler bei der Aktualisierung des dazugehörigen Wikidata-Objekts! + Hintergrundbild festlegen + Hintergrundbild erfolgreich festgelegt! + Bist du sicher, dass du deinen Suchverlauf löschen möchtest? + Suchverlauf gelöscht + Sitzung abgelaufen. Bitte erneut anmelden. diff --git a/app/src/main/res/values-diq/strings.xml b/app/src/main/res/values-diq/strings.xml index 2b66709f0..a312fcc9d 100644 --- a/app/src/main/res/values-diq/strings.xml +++ b/app/src/main/res/values-diq/strings.xml @@ -9,18 +9,23 @@ * Mirzali --> + Asayış + Bıngeh + Lokasyon Commons + Eyari Namey karberi Parola Cı kewe + Parola, xo vira kerde? Qeyd be Cıkewtış Kerem kerên, bıpawên... Cıkewtış hewl bi. Nidekeweya de Dosya nêvineya. Dosyê da bine bıcerebnê. - Tesdiq kerdış nebı + Tesdiq kerdış nebı Barkerdış sertera! %1$s bıbar! Barkerdışê xo pıro bıde. @@ -47,8 +52,6 @@ Sername Şınasnayış Xırabiya kewten-network xeta - Ronıştışo abeno - Namey karberi ye xo kontrol kerë - Ronıştışo nêabeno - Parolay xo kontrol kerë Şıma xeylê rayi kerd ke cı kewê, a ser nêvıst. Şıma rê zehmet 2-3 deqey ra tepeya reyna bıcerrebnên. Qısur mewni rê, Karber commons dı bloqe biyo. Nidekeweya de @@ -58,6 +61,8 @@ Bar ke Kategoriyan dı cı geyr Star ke + Newe ke + Liste \@string/contributions_subtitle_zero Yew barbiyayış @@ -78,7 +83,8 @@ Qeyd be Heq te cı Qandê yew <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-cıkewtış</a>ê neweyi rê rapor û teklifan bıaferne. - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Politikay nımıtışi</a> + <u>Politikaya nımıtışi</u> + <u>İştırakkerdoği</u> Heq te cı Peyd rışten bırış (E-posta ra) E-posta eyar nêbi @@ -86,7 +92,7 @@ Anciya bıcerrebne Bıtexelne Ron - Lisans + Lisanso hesebiyaye Attribution-ShareAlike 3.0 Attribution 3.0 CC0 @@ -121,6 +127,8 @@ E Sername + Şınasnayış + Nuştekar Lisans Koordinati Korbıze @@ -129,5 +137,7 @@ Keye Bar ke Veciyayış + Bıtexelne Anciya bıcerrebne + Mı fehm kerd! diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 0dae5c65e..30f39b29d 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -11,6 +11,8 @@ * Tgkarounos --> + Εξερεύνηση + Εξερεύνηση Εμφάνιση Γενικά Ανατροφοδότηση @@ -18,6 +20,7 @@ Commons Ρυθμίσεις + Ανέβασμα στα Κοινά Όνομα χρήστη Κωδικός πρόσβασης Συνδεθείτε στο λογαριασμό σας στο Commons Beta @@ -29,7 +32,7 @@ Επιτυχής σύνδεση! Η είσοδος απέτυχε! Το αρχείο δεν βρέθηκε. Παρακαλώ δοκιμάστε ένα άλλο αρχείο. - Απέτυχε ο έλεγχος ταυτότητας! + Απέτυχε ο έλεγχος ταυτότητας, παρακαλώ συνδεθείτε ξανά Η αποστολή ξεκίνησε! %1$s επιφορτώθηκε! Πατήστε για να προβάλλετε την αποστολή @@ -57,8 +60,7 @@ Παρακαλώ παρέχετε ένα τίτλο για αυτό το αρχείο Περιγραφή Δεν είναι δυνατή η σύνδεση - αποτυχία του δικτύου - Δεν είναι δυνατή η σύνδεση - ελέγξτε το όνομα χρήστη σας - Δεν είναι δυνατή η σύνδεση - παρακαλούμε ελέγξτε τον κωδικό σας + Αποτυχία σύνδεσης - παρακαλώ ελέγξτε το όνομα χρήστη και τον κωδικό σας Πάρα πολλές ανεπιτυχείς προσπάθειες. Παρακαλώ δοκιμάστε ξανά σε λίγα λεπτά. Συγνώμη, αυτός ο χρήστης έχει αποκλειστεί από τα Commons Πρέπει να δώσετε τον κωδικό πιστοποίησης με δύο παράγοντες @@ -93,6 +95,7 @@ Ρυθμίσεις Εγγραφή Προβεβλημμένες εικόνες + Κατηγορία Σχετικά Λογισμικό ανοικτού κωδικού που κυκλοφορεί υπό την <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Άδεια Apache v2</a>. Το Wikimedia Commons και το λογότυπο είναι εμπορικά σήματα του Ιδρύματος Wikimedia και χρησιμοποιούνται με άδεια από το Ίδρυμα Wikimedia. Δεν συμμετέχουμε στην δημιουργία, ανάπτυξη ή συντήρηση του Ιδρύματος Wikimedia. Δημιουργήστε ένα νέο <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub θέμα</a> για αναφορές σφαλμάτων και προτάσεις. @@ -112,7 +115,7 @@ Προεπιλεγμένη άδεια Χρήση προηγούμενου τίτλου/περιγραφής Αυτόματη ανάκτηση τρέχουσας θέσης - Ανάκτηση τρέχουσας τοποθεσία για να σας προσφέρουμε προτάσεις κατηγοριών αν η εικόνα δεν είναι γεωσεσημασμένη. + Ανακτά την τρέχουσα τοποθεσία εάν η εικόνα δεν είναι γεωσεσημασμένη, και τις γεωσημάνσεις με αυτή. Προειδοποίηση: Αυτό θα αποκαλύψει την τρέχουσα τοποθεσία σας. Νυχτερινή λειτουργία Χρήση σκοτεινού θέματος Attribution-ShareAlike 4.0 @@ -201,6 +204,8 @@ Εικόνα Υποβάθρου Η Εικόνα των Μέσων Απέτυχε (δεν μπορεί να φορτωθεί) Δεν Βρέθηκε καμία Εικόνα + Δεν βρέθηκαν υποκατηγορίες + Δεν βρέθηκαν γονικές κατηγορίες Φορτώστε την Εικόνα Mount Zao Llamas @@ -273,5 +278,32 @@ Συνέχεια Ακύρωση Ξαναπροσπαθήστε + Κατάλαβα! + Αυτά είναι τα μέρη κοντά σας που χρειάζονται φωτογραφίες για να εικονογραφηθούν τα λήμματά τους στη Βικιπαίδεια + Πατώντας αυτό το κουμπί φέρνει μια λίστα αυτών των μερών + Μπορείτε να ανεβάσετε μια εικόνα για οποιοδήποτε μέρος από την γκαλερί ή την κάμερά σας + Δεν βρέθηκαν εικόνες! + Συνέβη σφάλμα κατά το ανέβασμα των εικόνων. + Ανέβηκε από: %1$s + Είστε αποκλεισμένοι από την επεξεργασία των Κοινών Κοινοποίηση εφαρμογής + Οι συντεταγμένες δεν ορίστηκαν κατά την διάρκεια της επιλογής εικόνας + Σφάλμα κατά την εύρεση κοντινών μερών. + Φωτογραφία της Ημέρας + Φωτογραφία της Ημέρας + Αναζήτηση + Αναζήτηση στα κοινά + Δεν βρέθηκαν εικόνες που αντιστοιχούν στο %1$s + Αναζήτηση + Πρόσφατες αναζητήσεις: + Πρόσφατα αναζητημένα ερωτήματα + Συνέβη σφάλμα κατά τη φόρτωση κατηγοριών. + Συνέβη σφάλμα κατά τη φόρτωση υποκατηγοριών. + Η εικόνα προστέθηκε επιτυχώς στο %1$s στο Wikidata! + Αποτυχία ενημέρωσης της αντιστοιχούσας οντότητας του Wikidata! + Ρύθμιση ταπετσαρίας + Η ταπετσαρία ρυθμίστηκε επιτυχώς! + Είστε σίγουρος ότι θέλετε να καθαρίσετε το ιστορικό αναζήτησης; + Το ιστορικό αναζήτησης διεγράφη + Η συνεδρία σύνδεσης έληξε, παρακαλώ συνδεθείτε ξανά. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index c1a45b309..0865c7aef 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1,6 +1,8 @@ + Explorar + Explorar Apariencia Generales Sugerencias @@ -18,6 +22,7 @@ Commons Configuración + Cargar en Commons Nombre de usuario Contraseña Accede a tu cuenta de Commons Beta @@ -29,7 +34,7 @@ Acceso correcto. Acceso fallido. No se encontró el archivo. Prueba con otro. - Falló la autenticación. + Falló la autenticación; prueba a acceder otra vez Ha comenzado la carga. Se ha cargado %1$s. Pulsa para ver tu subida @@ -57,8 +62,7 @@ Proporciona un título para este archivo Descripción No se pudo iniciar sesión: falla de red - No se pudo iniciar sesión: revisa tu nombre de usuario - No se pudo iniciar sesión: revisa tu contraseña + No se puede acceder. Revisa el nombre de usuario y la contraseña Demasiados intentos fallidos. Inténtalo de nuevo en unos minutos. Lo sentimos, este usuario ha sido bloqueado en Commons Debes proporcionar tu código de auntenticación de dos factores. @@ -93,6 +97,7 @@ Ajustes Regístrate Imágenes en destaque + Categoría Acerca de La aplicación de código abierto Wikimedia Commons fue creada por, y recibe mantenimiento de, cesionarios y voluntarios de la comunidad de Wikimedia. La Fundación Wikimedia no está involucrada en la creación, el desarrollo ni el mantenimiento de la aplicación. Notifica de problemas y sugerencias en <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub</a>. @@ -112,7 +117,7 @@ Licencia predeterminada Usar título/descripción anteriores Obtener ubicación actual automáticamente - Recuperar ubicación actual para ofrecer sugerencias de categorías si la imagen no tiene etiquetas geográficas + Recuperar ubicación actual para ofrecer sugerencias de categorías si la imagen no tiene etiquetas geográficas Modo nocturno Usar tema oscuro Atribución-CompartirIgual 4.0 @@ -199,6 +204,7 @@ Imagen de fondo Falló la imagen de multimedia No se encontró ninguna imagen + No se encontró ninguna subcategoría Subir imagen Monte Zao Llamas @@ -267,4 +273,23 @@ Selecciona el idioma en que quieres enviar traducciones Cancelar Reintentar + Entendido + Estos sitios cercanos a ti necesitan imágenes para ilustrar sus artículos de Wikipedia + Puedes cargar una imagen para cualquier sitio desde la galería o la cámara + No se encontró ninguna imagen. + Se produjo un error al cargar las imágenes. + Cargada por: %1$s + Compartir aplicación + No se especificaron las coordenadas al seleccionar la imagen + Error al recuperar los lugares cercanos. + Foto del día + Foto del día + Buscar + Buscar en Commons + Buscar + Búsquedas recientes: + Se produjo un error al cargar las categorías. + Se produjo un error al cargar las subcategorías. + ¿Confirmas que quieres vaciar el historial de búsquedas? + Historial de búsqueda eliminado diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index f44262e05..42bd77fcb 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -1,5 +1,6 @@ + Itxura + Orokorra + Feedback + Kokapena Commons + Hobespenak Erabiltzaile izena Pasahitza + Sartu zure Commons Beta kontura Saioa hasi + Pasahitza ahaztu duzu? Eman izena Saioa hasten Mesedez itxaron… Saio hasiera egina Saio hasieran akatsa! Fitxategia ez da aurkitu. Mesedez saiatu beste batekin. - Autentifikazioan akatsa! + Autentifikazioan akatsa, mesedez saiatu berriro sartzen Igoera hasi da! %1$s igotzen! Ukitu igotakoa ikusteko @@ -40,12 +48,13 @@ Partekatu Ikusi nabigatzailean Izenburua + Mesedez, eman izenburu bat fitxategi honetarako Deskribapena Ezin izan da sartu - sarean akatsa - Ezin izan da sartu - ziurtatu ezazu zure erabiltzaile izena - Ezin izan da sartu - ziurta ezazu zure pasahitza + Ezin da sartu. Mesedez, berrikusi zure erabiltzaile izena eta pasahitza Sartzeko saiakera txar gehiegi. Mesedez saiatu zaitez minutu batzuk barru. Barka, baina erabiltzaile hau blokeatuta dago Commonsen + Zure bi faktoreko autentifikazio kodea eman behar duzu. Saio hasieran akatsa Igo Izena eman bilduma honi @@ -54,22 +63,26 @@ Kategoriak bilatu Gorde Eguneratu + Zerrenda + GPSa desgaituta dago gailu honetan. Gaitu nahi duzu? GPSa gaitu Oraindik ez da ezer igo - - Oraindik igoerarik ez + + \@string/contributions_subtitle_zero igoera 1 %1$d igoera Ez da kategoriak aukritu %1$s izenarekin - Gehitu kategoriak zure argazkiak Wikimedia Commonsen aurkitzen errazagoak izan daitezen. + Gehitu kategoriak zure argazkiak Wikimedia Commonsen aurkitzen errazagoak izan daitezen.\nHasi idazten kategoriak gehitzeko. Kategoriak Hobespenak Eman izena + Nabarmendutako irudiak Honi buruz Open Source softwarea <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache v2 Lizentziaren</a> pean egina. Wikimedia Commons eta bere logoa Wikimedia Fundazioaren marka erregistratuak dira eta Wikimedia Fundazioaren baimenarekin erabiltzen dira. Ez gaude Wikimedia Fundaziora afiliatuta. GitHub-eko <a href=\"https://github.com/commons-app/apps-android-commons\">Iturria</a> eta <a href=\"https://commons-app.github.io/\">webgunea</a>. <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-eko gai</a> berria sortu erroreen berri emateko. - <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">Pribatutasun politika</a> + <u>Pribatutasun politika</u> + <u>Kredituak</u> Honi buruz Bidali zure iritzia (e-posta bidez) Posta bezerorik ez da instalatu @@ -81,7 +94,7 @@ Irudi hau %1$s lizentziapean egongo da Irudi hau bidaltzen, nire lan propioa dela aitortzen dut, copyrighta duten materiala edo selfiak ez duela, eta beste motatakoak <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Ohikoaren arauak</a> Jaitsi - Lizentzia + Berezko lizentzia Aurreko izenburu/deskribapena erabili Oraingo kokapena automatikoki lortu Gau modua @@ -106,8 +119,25 @@ CC BY-SA 4.0 CC BY 4.0 CC Zero + Wikimedia Commons-ek Wikipedian erabiltzen diren irudi gehienak barne hartzen ditu. + Zure irudiek mundu osoko jendeari irakasten laguntzen dute! + Mesedez, igo bakarrik zuk ateratako edo sortutako irudiak: + Naturako elementuak (loreak, animaliak, mendiak)\n• Objektu erabilgarriak (bizikletak, tren geltokiak)\n• Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat) + Naturako elementuak (loreak, animaliak, mendiak) + Objektu erabilgarriak (bizikletak, tren geltokiak) + Pertsona famatuak (zure alkatea, zuk ezagututako atleta olinpikoren bat...) Mesedez EZ igo: + Autorretratuak edo zure lagunen argazkiak + Internetetik jaitsitako irudiak Igoera adibidea: + - Izenburua: Sydney Opera House\n- Deskribapena: Sydney Opera House badiaren beste aldetik ikusita\n- Kategoriak: Sydney Opera House mendebaldetik, Sydney Opera House urrunetik ikusita + Izenburua: Sydney Opera House + Deskribapena: Sydney Opera House badiaren beste aldetik ikusita + Kategoriak: Sydney Opera House mendebaldetik, Sydney Opera House urrunetik ikusita + Lagundu zuren irudiekin. Lagundu Wikipediako artikuluei bizia ematen! + Wikipediako irudiak Wikimedia Commons-etik datoz. + Zure irudiek mundu osoko jendeari irakasten laguntzen dute. + Saihestu copyright-a daukaten materialak, Interneten aurkitutakoak edo posterretakoak, liburuen azalak, etab. Lortu duzula uste duzu? Bai! Kategoriak @@ -116,8 +146,12 @@ Deskribapenik ez Lizentzia ezezaguna Eguneratu + Nahitaezko baimena: kanpo-biltegiaren irakurketa. Aplikazioa ezin da zure galeriara sartu hau gabe. + Nahitaezko baimena: kanpo-biltegiaren idazketa. Aplikazioa ezin da zure kamerara sartu hau gabe. + Aukerako baimena: momentuko kokapena zehaztu kategoriak iradokitzeko Ados Gertuko lekuak + Ez da hurbileko lekurik aurkitu Oharra Fifxategia dagoeneko Commonsen existitzen da. Ziur zaude jarraitu nahi duzula? Bai @@ -125,6 +159,8 @@ Izenburua Fitxategiaren izenburua Deskribapena + Hemen doa multimediaren deskribapena. Nahiko luzea izan daiteke, eta hala izatekotan zenbait lerrotan egokitu beharko da. Hala ere, ondo ikustea espero dugu. + Egilea Igoera data Lizentzia Koordenatuak @@ -133,7 +169,11 @@ 2FA Kodea Gehienezko muga 500 baino gehiago ezin dira erakutsi + Oraingoz ez da bi faktoreko egiaztapenik onartzen. + Benetan itxi nahi duzu saioa? Commonsen logoa + Commons webgunea + Commons-en Facebook orria Atzealdeko irudia Media irudiak kale egin du Ez da irudirik aurkitu @@ -158,12 +198,58 @@ Atzeraelikadura Saioa itxi Tutoriala + Jakinarazpenak + Nabarmendua + Ezin dira hurbileko lekuak erakutsi kokapen baimenik gabe Deskripziorik ez da aurkitu Artxibo orrialde komuna Wikidata itema + Wikipediako artikulua Argazkiak hartzerakoan sortutako akatsa + Fitxategi izenburu deskribatzaile bakarra, fitxategi-izen gisa balioko duena. Hizkuntza arrunta erabil dezakezu espazioekin. Ez sartu fitxategiaren luzapena. + Mesedez, deskribatu multimedia elementua ahal duzun gehien: non hartu zen? zer erakusten du? zein da bere testuingurua? Mesedez, objektuak eta pertsonak deskribatu. Eman asmatzeko erraza ez den informazioa, adibidez, paisaia bat izatekotan, eguneko zein orudtan hartu den. Multimediak zerbait berezia erakusten badu, mesedez azaldu zerk egiten duen berezia. + Argazkia ilunegia da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu. + Argazkia lausoa da, ziur zaude kargatu nahi duzula? Wikimedia Commons-ek balio entziklopedikoa duten argazkiak bakarrik hartzen ditu. Baimena eman + Kanpo-biltegia erabili + Aplikazioaren kamerarekin ateratako argazkiak zure gailuan gorde Zure kontuan saioa hasi + Erregistro fitxategia bidali + Ez da URLa irekitzeko nabigatzailerik aurkitu + Errorea! Ez da URLa aurkitu + Ezabaketarako izendatu + Irudi hau ezabaketarako izendatua izan da. + Nabigatzailean ikusi + Kokapena ez da aldatu. + Kokapena ez dago erabilgarri. + Baimena beharrezkoa da hurbil dauden lekuen zerrende ikusteko NORABIDEAK JASO IRAKURRI ARTIKULUA + Ongi etorria Wikimedia Commons-era! Pozten gara zu hemen egoteaz. + Eskerrik asko aldaketa bat egiteagatik + NOLA HELDU + WIKIDATA + WIKIPEDIA + COMMONS + Interneta ez dago erabilgarri + Interneta erabilgarri + Ez da jakinarazpenik aurkitu + <u>Itzuli</u> + Hizkuntzak + Aukeratu itzulpenak zein hizkuntzatarako bidali nahi dituzun + Jarraitu + Utzi + Saiatu berriro + Ulertu dut! + Zugandik hurbil dauden leku hauek irudiak behar dituzte beren Wikipedia artikuluak hornitzeko + Ez da irudirik aurkitu! + Arazo bat egon da irudiak kargatzerakoan. + Aplikazioa partekatu + Ez da koordenaturik zehaztu irudia aukeratzean + Eguneko argazkia + Eguneko argazkia + Ezin izan da dagokion Wikidata entitatea eguneratu! + Horma papera ezarri + Horma papera ezarri da! + Saioa hasteko denbora amaituta, mesedez, hasi saioa berriro. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index d80b0772c..fe87d5736 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -28,7 +28,7 @@ ورود موفق! ورود ناموفق! پرونده یافت نشد لطفاً پرونده دیگری را امتحان کنید. - تأیید اعتبار انجام نشد! + تأیید اعتبار انجام نشد! بارگذاری آغاز شد! %1$s بارگذاری شد! برای دیدن بارگذاریتان بر روی صفحه انگشت بزنید @@ -56,8 +56,6 @@ لطفاً نامی را برای این پرونده انتخاب کنید توضیحات قادر به ورود نیست - شکست شبکه‌ای - ناتوانی در ورود - لطفاً نام کاربریتان را بررسی کنید - ناتوانی در ورود - لطفاً گذرواژه‌یتان را بررسی کنید تلاش ناموفق بیش از حد. لطفاً چند دقیقهٔ دیگر دوباره تلاش کنید پوزش، کاربر در ویکی‌انبار بسته شده‌است باید تأیید دومرحله‌ای را فعال کنید. @@ -106,7 +104,7 @@ مجوز پیش‌فرض از عنوان/توضیحات پیشین استفاده کنید دریافت خودکار موقعیت کنونی - درحال دریافت موقعیت برای پیشنهاد رده در صورتی که برچسب جغرافیایی وجود نداشته باشد. + درحال دریافت موقعیت برای پیشنهاد رده در صورتی که برچسب جغرافیایی وجود نداشته باشد. حالت شبانه استفاده از حالت تیره CC Attribution-ShareAlike 4.0 diff --git a/app/src/main/res/values-fi/error.xml b/app/src/main/res/values-fi/error.xml index bc47fa3fb..546752de6 100644 --- a/app/src/main/res/values-fi/error.xml +++ b/app/src/main/res/values-fi/error.xml @@ -2,10 +2,11 @@ - Commons app on kaatunut + Commons on kaatunut Pahoittelemme, virhe tapahtui. Kerro meille mitä teit äsken, sähköpostitse. Se auttaa meitä korjaamaan ongelman! Kiitos! diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index e833932a8..7d231a2cd 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -10,8 +10,11 @@ * Samoasambia * Silvonen * Stryn +* Surjection --> + Tutki + Tutki Ulkoasu Yleinen Palaute @@ -30,7 +33,7 @@ Kirjautuminen onnistui! Kirjautuminen epäonnistui! Tiedostoa ei löytynyt. Yritä toista tiedostoa. - Tunnistautuminen epäonnistui! + Tunnistautuminen epäonnistui, kirjaudu uudelleen sisään Tallentaminen aloitettiin! %1$s tallennettiin! Napauta katsoaksesi tallennusta @@ -58,8 +61,7 @@ Anna tälle tiedostolle otsikko Kuvaus Kirjautuminen epäonnistui - verkkovirhe - Kirjautuminen epäonnistui - tarkista käyttäjätunnus - Kirjautuminen epäonnistui - tarkista salasanasi + Kirjautuminen epäonnistui - tarkista käyttäjänimesi ja salasanasi Liikaa epäonnistuneita yrityksiä. Yritä uudelleen parin minuutin kuluttua. Pahoittelut, tämä käyttäjä on estetty Commonsissa Anna kaksivaiheisen tunnistuksen koodi. @@ -93,6 +95,8 @@ Luokat Asetukset Rekisteröidy + Suositellut kuvat + Luokka Tietoja Wikimedia Commons -sovellus on vapaan lähdekoodin ohjelmisto, joka on luotu ja ylläpidetty Wikimedia-yhteisön apurahoilla ja vapaaehtoisilla. Wikimedia Foundation ei ole sitoutunut sovelluksen luomiseen, kehittämiseen tai ylläpitoon. Luo uusi <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-issue</a> bugiraporteille ja ehdotuksille. @@ -112,7 +116,7 @@ Oletuslisenssi Käytä edellistä otsikkoa/kuvausta Hae tämänhetkinen sijainti automaattisesti - Nouda nykyinen sijainti asettaaksesi käyttöön luokkaehdotuksia jos kuva ei ole paikkamerkitty + Noutaa nykyisen sijainnin jos kuva ei ole paikkamerkitty, ja paikkamerkitsee kuvan. Varoitus: tämä paljastaa nykyisen sijaintisi. Yötila Käytä tummaa teemaa Nimeä-JaaSamoin 4.0 @@ -150,6 +154,8 @@ Tallennusesimerkki: - Otsikko: Sydneyn oopperatalo\n- Kuvaus: Sydneyn oopperatalo katsottuna lahden toiselta puolelta\n- Luokat: Sydneyn oopperatalo, Sydneyn oopperatalo lännestä, Sydneyn oopperatalon etänäkymät Otsikko: Sydneyn oopperatalo + Kuvaus: Sydneyn oopperatalo lahden toiselta puolelta katsottuna + Luokat: Sydneyn oopperatalo lännestä katsottuna, Sydneyn oopperatalo kaukaa katsottuna Herätä Wikipedia-artikkelit eloon kuvillasi! Tuo kuvasi Wikipediaan. Wikipedian kuvat tulevat Wikimedia Commonsista. Kuvasi auttavat useita ihmisiä ympäri maailmaa artikkeleiden ymmärtämisessä. @@ -162,8 +168,8 @@ Ei kuvausta Tuntematon lisenssi Päivitä - Vaadittu oikeus: Ulkoisen tallennustilan luku. Appi ei voi päästä galleriaasi ilman tätä oikeutta. - Vaadittava lupa: Kirjoita ulkoiseen tallennustilaan. Appi ei voi päästä kameraasi ilman tätä oikeutta. + Vaadittu oikeus: Ulkoisen tallennustilan luku. Sovellus ei voi päästä galleriaasi ilman tätä oikeutta. + Vaadittu oikeus: Kirjoita ulkoiseen tallennustilaan. Sovellus ei voi päästä kameraasi ilman tätä oikeutta. Valinnainen lupa: Saada tämänhetkinen sijainti loukkasuosituksia varten. OK Lähellä olevat paikat @@ -177,6 +183,7 @@ Kuvaus Median kuvaus menee tänne. Tämä voi mahdollisesti olla melko pitkä, ja sen täytyy kääriä poikittain useita rivejä. Toivomme, että se näyttää silti hyvältä. Tekijä + Suositellun kuvan tekijän käyttäjänimi tähän. Tallennuspäivämäärä Lisenssi Koordinaatit @@ -219,6 +226,7 @@ Kirjaudu ulos Opas Ilmoitukset + Suositeltu Lähellä olevia paikkoja ei voida näyttää ilman sijaintilupaa kuvausta ei löytynyt Commons-tiedostosivu @@ -235,6 +243,7 @@ Kirjaudu tilillesi Lähetä lokitiedosto Lähetä lokitiedosto kehittäjille sähköpostin kautta + Ei verkkoselainta jolla avata URL-osoite Virhe! URL-osoitetta ei löytynyt Ehdotettu poistettavaksi Tätä kuvaa on ehdotettu poistettavaksi. @@ -249,6 +258,7 @@ Kiitos muokkaamisestasi %1$s mainitsi sinut %2$s. Vaihda näkymä + REITTI WIKIDATA WIKIPEDIA COMMONS @@ -257,9 +267,32 @@ Ohita opetus Internet ei saatavissa Internet saatavana + Virhe ilmoitusten haussa + Ilmoituksia ei löytynyt <u>Käännä</u> Kielet + Valitse kieli, joksi haluaisit kääntää Jatka Peruuta Yritä uudelleen + Selvä! + Tässä lähellä olevia paikkoja, jotka tarvitsevat kuvitusta Wikipedia-artikkeleihinsa + Tämä nappi näyttää listan kyseisistä paikoista + Voit lähettää kuvan mistä tahansa paikasta galleriastasi tai kamerasta + Kuvia ei löytynyt! + Virhe ladattaessa kuvia. + Tallentanut: %1$s + Jaa sovellus + Koordinaatteja ei annettu kuvaa valittaessa + Virhe paikkoja haettaessa. + Päivän kuva + Päivän kuva + Hae + Hae Commonsista + Haku + Kuva onnistuneesti lisätty Wikidata-kohteeseen %1$s! + Vastaavaa Wikidata-kohdetta ei voitu päivittää! + Aseta taustakuvaksi + Taustakuva asetettu! + Istuntosi on vanhentunut. Kirjaudu sisään uudelleen. diff --git a/app/src/main/res/values-fo/strings.xml b/app/src/main/res/values-fo/strings.xml index c4649407a..369c59d5d 100644 --- a/app/src/main/res/values-fo/strings.xml +++ b/app/src/main/res/values-fo/strings.xml @@ -12,7 +12,7 @@ Vinarliga bíða… Innritan væleydnað! Innritan miseydnaðist - Góðkenning miseydnaðist! + Góðkenning miseydnaðist! Upplóting er byrjað! %1$s er lagt út! Trýst fyri at síggja tað sum tú legði út @@ -34,8 +34,6 @@ Heiti Frágreiðing Ómøguligt at rita inn - feilur í netsambandinum - Ómøguligt at rita inn - vinarliga eftirkanna títt brúkaranavn - Ómøguligt at rita inn - vinarliga kanna eftir, um títt loyniorð er rætt Ov nógv miseydnaðar royndir. Vinarliga royn aftur um fáir minuttir Haldið okkum tilgóðar, hesin brúkari er blivin sperraður á Commons Login miseydnaðist diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 442fc6bc5..7642af675 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,10 +1,12 @@ + Explorer + Explorer Apparence Général Donner son avis @@ -25,6 +29,7 @@ Commons Paramètres + Téléverser sur Commons Nom d’utilisateur Mot de passe Connexion à votre compte Commons Beta @@ -36,7 +41,7 @@ Connexion réussie ! Échec de la connexion ! Fichier non trouvé. Veuillez en essayer un autre. - Échec de l’authentification ! + Échec de l\'authentification, veuillez vous reconnecter s\'il vous plait Téléversement démarré ! %1$s téléversés ! Appuyer pour voir votre téléversement @@ -64,8 +69,7 @@ Veuillez donner un titre à ce fichier Description Impossible de se connecter — panne de réseau - Impossible de se connecter — veuillez vérifier votre nom d’utilisateur - Impossible de se connecter — veuillez vérifier votre mot de passe + Impossible de se connecter — veuillez vérifier votre nom d’utilisateur et votre mot de passe Trop de tentatives infructueuses. Veuillez réessayer dans quelques minutes. Désolé, cet utilisateur a été bloqué dans Commons Vous devez fournir votre code d’authentification à deux facteurs. @@ -100,6 +104,7 @@ Paramètres S’inscrire Images en vedette + Catégorie À propos L’application Wikimedia Commons est une application open source créée et tenue à jour par les bénéficiaires et volontaires de la communauté Wikimedia. La fondation Wikimedia n’est pas associée à la création, le développement ou l’entretien de l’application. Créer un nouveau <a href=\"https://github.com/commons-app/apps-android-commons/issues\">signalement GitHub</a> pour signaler des bogues ou des suggestions. @@ -119,7 +124,7 @@ Licence par défaut Utiliser le titre ou la description précédent Obtenir automatiquement l’emplacement actuel - Récupérer l’emplacement actuel pour proposer des suggestions de catégorie si l’image n’est pas marquée géographiquement + Récupère l’emplacement actuel si l’image n’est pas marquée géographiquement, et marque géographiquement l’image avec. Avertissement : ceci dévoilera votre emplacement actuel. Mode de nuit Utiliser un thème sombre Attribution-ShareAlike 4.0 @@ -207,6 +212,8 @@ Image de fond Échec sur l’image du média Aucune image trouvée + Aucune sous-catégorie trouvée + Aucune catégorie parent trouvée Téléverser une image Mont Zao Lamas @@ -279,5 +286,32 @@ Continuer Annuler Réessayer + C’est bon ! + Il y a des lieux autour de vous qui demandent des images pour illustrer leurs articles Wikipédia + En cliquant sur ce bouton vous afficherez une liste de ces endroits + Vous pouvez téléverser une photo de n\'importe quel endroit de votre gallerie ou de votre appareil photo + Aucune images trouvée. + Une erreur s\'est produite pendant le chargement des images. + Importé par:%1$s + Vous avez été bloqué et ne pouvez plus modifier sur Commons Partager les applications + Les coordonnées n\'ont pas été spécifiées pendant la sélection de l\'image + Erreur durant l\'exploration du voisinage. + Image du jour + Image du jour + Chercher + Chercher dans Commons + Aucune image trouvée concernant %1$s + Chercher + Dernières recherches : + Dernières requêtes de recherche + Erreur durant le chargement des catégories. + Erreur durant le chargement des sous-catégories. + Image bien ajoutée à %1$s sur Wikidata ! + Échec de la mise à jour de l\'entité Wikidata correspondante ! + Définir le papier-peint + Papier-peint configuré avec succès! + Êtes-vous sûr de vouloir effacer votre historique de recherche ? + Historique de recherche effacée + Session expirée, veuillez vous reconnecter. diff --git a/app/src/main/res/values-frr/strings.xml b/app/src/main/res/values-frr/strings.xml index f4866e705..3c00cdb3d 100644 --- a/app/src/main/res/values-frr/strings.xml +++ b/app/src/main/res/values-frr/strings.xml @@ -15,7 +15,7 @@ Uunmeldin hää loket! Bi\'t uunmeldin as wat skiaf gingen. Datei ei fünjen. Ferschük det mä en ööder datei. - Dü küdst ei gudkäänd wurd! + Dü küdst ei gudkäänd wurd! Huuchschüüren hää begand! %1$s huuchschüürd! Tipe, am din huuchschüürd bil uuntulukin @@ -42,8 +42,6 @@ Tiitel Beskriiwang Bi\'t uunmeldin as wat skiaf gingen - näätwerk-feeler - Bi\'t uunmeldin as wat skiaf gingen - luke ans efter di brükernööm - Bi\'t uunmeldin as wat skiaf gingen - luke ans efter det paaswurd Tu fölsis fersoocht saner lok. Ferschük det uun hög minüüten noch ans nei. Didiar brüker as üüb Commons speret wurden. Dü skel dan code för\'t tau-straal-gudkäänen (2FA) uundu. @@ -94,7 +92,7 @@ Lisens Ual tiitel/beskriiwang brük Aktuel plak automaatisk ufrep - Rept di aktuel plak uf, am kategoriin föörtuslauen, wan det bil nian geotags hää. + Rept di aktuel plak uf, am kategoriin föörtuslauen, wan det bil nian geotags hää. Naacht muude Jonk skak brük Attribution-ShareAlike 4.0 diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 630311db4..446735d37 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -16,6 +16,7 @@ Commons Configuracións + Cargar en Commons Nome de usuario Contrasinal Acceda á súa conta de Commons Beta @@ -27,7 +28,7 @@ Accedeu correctamente! Erro durante o inición de sesión! Ficheiro non atopado. Por favor, probe con outro. - Erro durante a autenticación! + Erro de autenticación, por favor inicia unha nova sesión A carga comezou! Cargouse \"%1$s\"! Prema para ollar a súa carga @@ -55,8 +56,7 @@ Por favor, proporcione un título para este ficheiro Descrición Erro ao acceder ao sistema: Fallou a rede - Erro ao acceder ao sistema: Comprobe o seu nome de usuario - Erro ao acceder ao sistema: Comprobe o seu contrasinal + Non se pode acceder. Revise o nome de usuario e o contrasinal Demasiados intentos incorrectos. Inténteo de novo nuns minutos. Sentímolo, este usuario está bloqueado en Commons Debe proporcionar o seu código de autenticación de dous factores. @@ -110,7 +110,7 @@ Licenza por defecto Usar o título ou a descrición anterior Obter automaticamente a localización actual - Obter a localización actual para ofrecer suxestións de categoría se a imaxe non está xeolocalizada + Obter a localización actual se a imaxe non está xeolocalizada, e xeolocaliza a imaxe con ela. Atención: Isto revelará a súa localización actual. Modo nocturno Usar tema escuro Recoñecemento-CompartirIgual 4.0 @@ -269,4 +269,22 @@ Proceder Cancelar Reintentar + Entendido! + Hai sitios preto de vostede que precisan fotos para ilustrar os seus artigos de Wikipedia + Premendo neste botón aparecerá unha lista destes lugares + Pode cargar unha imaxe de calquera lugar dende a galería ou a cámara + Non se atopou ningunha imaxeǃ + Houbo un erro ó subir as imaxes. + Subida porː %1$s + Está bloqueado e non pode editar en Commons + Compartir a aplicación + Non se indicaron as coordenadas ó seleccionar a imaxe + Erro ó procurar os lugares próximos. + Imaxe do día + Imaxe do día + A imaxe engadiuse con éxito a %1$s en Wikidata! + Fallou a actualización da entidade do Wikidata correspondente! + Definir imaxe de fondo + A imaxe de fondo configurouse correctamenteǃ + A sesión caducou, por favor inicia unha nova sesión. diff --git a/app/src/main/res/values-haw/strings.xml b/app/src/main/res/values-haw/strings.xml index a81ab120d..7693bc628 100644 --- a/app/src/main/res/values-haw/strings.xml +++ b/app/src/main/res/values-haw/strings.xml @@ -12,7 +12,7 @@ E kali ke ʻoluʻolu… ʻEʻe kūleʻa Hāʻule ka ʻeʻena - Hāʻule ka hōʻoiaʻiʻo ʻike + Hāʻule ka hōʻoiaʻiʻo ʻike Ua hoʻomaka ka hoʻouka! Ua hoʻouka ʻia ʻo %1$s Kīkē no ka ʻike ʻana o kāu hoʻouka @@ -34,8 +34,6 @@ Poʻo Inoa Hōʻike ʻAno Hiki ʻole ke ʻeʻe - hāʻule pūnaewele - Hiki ʻole ke ʻeʻe - hōʻoiaʻiʻo i kāu inoa mea hoʻohana ke ʻoluʻolu - Hiki ʻole ke ʻeʻe - hōʻoiaʻiʻo i kāu ʻōlelo hūnā ke ʻoluʻolu Hoʻāʻo ʻeʻe ʻole he nui kā. E ʻoluʻolu, e hana hou i ka wā hou aku E kala mai, ua pale ʻia kēia mea hoʻohana ma ke Kahilehulehu Hāʻule ka ʻeʻena diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 69bb67655..4858c991a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -26,7 +26,7 @@ प्रवेश में सफल हुआ! प्रवेश विफल हुआ! फ़ाइल नहीं मिली, कृपया अन्य फ़ाइल से प्रयास करें। - प्रमाणीकरण विफल! + प्रमाणीकरण विफल! अपलोड आरंभ! %1$s अपलोड हुआ! अपना अपलोड देखने के लिए टैप करें @@ -53,8 +53,6 @@ शीर्षक विवरण प्रवेश नहीं हो रहा - नेटवर्क विफल - प्रवेश नहीं हो रहा - कृपया अपना सदस्य नाम जाँचें - प्रवेश नहीं हो रहा - कृपया अपना पासवर्ड जाँचें ढेर सारे असफल प्रयास होने के कारण कुछ मिनटों के बाद प्रयास करें। क्षमा करें, यह सदस्य कॉमन्स में अवरोधित है आपको अपना दो कारक प्रमाणन कोड प्रदान करना होगा। @@ -106,7 +104,7 @@ लाइसेन्स पिछले शीर्षक/विवरण का उपयोग करें वर्तमान स्थान स्वतः ज्ञात करें - यदि छवि पर जियोटैग नहीं है तो श्रेणियों के सुझाव हेतु वर्तमान स्थान ज्ञात करें। + यदि छवि पर जियोटैग नहीं है तो श्रेणियों के सुझाव हेतु वर्तमान स्थान ज्ञात करें। रात्रि मोड डार्क थीम का प्रयोग करें एट्रीब्यूशन-शेयरअलाइक 4.0 diff --git a/app/src/main/res/values-hr/error.xml b/app/src/main/res/values-hr/error.xml new file mode 100644 index 000000000..4bc77982c --- /dev/null +++ b/app/src/main/res/values-hr/error.xml @@ -0,0 +1,10 @@ + + + + Aplikacija Zajednički poslužitelj je prestala s radom + Nešto je krenulo po krivu! + Napišite nam što radite i podijelite s nama putem elektroničke pošte. Pomoći će nam da to popravimo! + Hvala Vam! + diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml new file mode 100644 index 000000000..34be966b1 --- /dev/null +++ b/app/src/main/res/values-hr/strings.xml @@ -0,0 +1,275 @@ + + + + Izgled + Opće + Povratna informacija + Lokacija + Zajednički poslužitelj + + Postavke + Suradničko ime + Zaporka + Prijavite se na Commons beta račun + Prijavi se + Zaboravljena zaporka? + Otvori račun + Prijava + Molimo pričekajte ... + Prijava uspješna! + Prijava neuspješna! + Datoteka nije pronađena. Molimo probajte drugu. + Autentifikacija neuspješna! + Postavljanje započeto! + %1$s postavljeno! + Dodirnite da biste vidjeli datoteku + Počinje postavljanje %1$s + Postavljanje %1$s + Završeno postavljanje %1$s + Postavljanje %1$s neuspješno + Dodirnite da biste vidjeli + + Postavlja se %1$d datoteka + Postavljaju se %1$d datoteke + + Moja nedavja postavljanja + U redu čekanja + Neuspješno + %1$d%% postavljeno + Postavljanje + Iz galerije + Napravi sliku + U blizini + Moja postavljanja + Podijeli + Pogledaj u pregledniku + Naziv + Molimo imenujte ovu datoteku + Opis + Prijava nije moguća - mrežna pogrješka + Prijava nije moguća - molimo provjerite suradničko ime i zaportku + Previše neuspješnih pokušaja, molimo probajte opet za par minuta. + Ispričavamo se, ovaj je suradnik blokiran na Zajendičkom poslužitelju + Morate upisati autetifikacijski kôd od dva faktora + Prijava neuspješna + Postavljanje + Imenujte ovaj set + Promjene + Postavljanje + Pretraži kategorije + Spremi + Osvježi + Popis + GPS je onemogućen na Vašem uređaju. Želite li ga omogućiti? + Omogući GPS + Nemate još postavljenih datoteka + + \@string/contributions_subtitle_zero + %1$d postavljena datoteka + %1$d postavljene datoteke + + + Započeto %1$d postavljanje + Započeta %1$d postavljanja + + + %1$d postavljanje + %1$d postavljanja + + Nema kategorija koje odgovoraju upitu %1$s + Dodajte slikama kategorije kako bi se lakše pronašle. Da biste ih dodali, započnite s upisivanjem. + Kategorije + Postavke + Otvori račun + Izabrane slike + O + Aplikacija The Wikimedia Commons je aplikacija otvorenog kôda koju razvijaju i održavaju volonteri Wikimedijine zajednice. Zaklada Wikimedija nije uključena u stvaranje, razvoj ili održavanje ove aplikacije. + Da biste prijavili poteškoću ili dali prijedlog, stvorite <a href=\"https://github.com/commons-app/apps-android-commons/issues\">novi zahtjev na GitHubu</a>. + <u>Politika privatnosti</u> + <u>Zasluge</u> + O + Pošaljite povratnu informaciju (putem elektroničke pošte) + Klijent za elektroničku poštu nije instaliran + Nedavno rabljene kategorije + Pričekajte za prvu sinkronizaciju... + Nemate još postavljenih slika. + Pokušaj ponovo + Odustani + Ova će slika biti licencirana pod %1$s + Slanjem ove slike izjavljujem da je ona moje djelo i ne sadrži materijale zaštićene autorskim pravom ili selfije, te da je u skladu sa <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">smjernicama Zajedničkog poslužitelja</a>. + Preuzmi + Podrazumijevana licencija + Rabite prethodni naziv/opis + Automatski pribavi trenutačnu lokaciju + Ako slika nema oznaku lokacije, pribavi trenutačnu lokaciju kako bi se mogle ponuditi kategorije + Noćni način + Rabi tamnu temu + Imenovanje-Dijeli pod istim uvjetima 4.0 + Imenovanje 4.0 + Imenovanje-Dijeli pod istim uvjetima 3.0 + Imenovanje 3.0 + CC0 + CC BY-SA 3.0 + CC BY-SA 3.0 (Austrija) + CC BY-SA 3.0 (Njemačka) + CC BY-SA 3.0 (Estonija) + CC BY-SA 3.0 (Španjolska) + CC BY-SA 3.0 (Hrvatska) + CC BY-SA 3.0 (Luksemburg) + CC BY-SA 3.0 (Nizozemska) + CC BY-SA 3.0 (Norveška) + CC BY-SA 3.0 (Poljska) + CC BY-SA 3.0 (Rumunjska) + CC BY 3.0 + CC BY-SA 4.0 + CC BY 4.0 + CC Zero + Na Zajedničkom poslužitelju se nalazi većina slika rabljena na Wikipediji. + Vaše slike pomažu u edukaciji ljudi diljem svijeta! + Molimo postavite slike koje su u cijelosti Vaše djelo: + Objekti iz prirode (cvijeće, životinje, planine)\n• Korisni objekti (bicikla, željezničke postaje)\n• Poznate osobe (Vaš gradonačelnik, olimpijski sportaš kojeg ste sreli) + Objekti iz prirode (cvijeće, životinje, planine) + Korisni objekti (bicikla, željezničke postaje) + Poznate osobe (Vaš gradonačelnik, olimpijski sportaš kojeg ste sreli) + Molimo NE postavljajte: + - selfije ili slike Vaših prijatelja\n- slike koje ste preuzeli s interneta\n- snimke ekrana zaštićenih aplikacija + Selfije ili slike Vaših prijatelja + Slike koje ste preuzeli s interneta + Snimke ekrana zaštićenih aplikacija + Primjer postavljanja: + - Naziv: Sydneyska opera\n- Opis: Sydneyska opera viđena iz zaljeva\n- Kategorije: Sydney Opera House from the west, Sydney Opera House remote views + Naziv: Sydneyska opera + Opis: Sydneyska opera viđena iz zaljeva + Kategorije: Sydney Opera House from the west, Sydney Opera House remote views + Dijelite Vaše slike. Pomozite da članci na Wikipediji zažive! + Slike na wikipediji su sa Zajedničkog poslužitelja. + Vaše slike pomažu u edukaciji ljudi diljem svijeta. + Izbjegavajte materijale s autorskim pravima koje ste pronašli na internetu (slike plakata, naslovnice knjiga, i slično). + Jeste li razumjeli? + Da! + Kategorije + Učitavanje... + Ništa nije odabrano + Nema opisa + Nepoznata licencija + Osvježi + Potrebno dopuštenje čitanja vanjske pohrane. Bez toga aplikacija ne može pristupiti Vašoj galeriji. + Potrebno dopuštenje spremanja na vanjsku pohranu. Bez toga aplikacija ne može pristupiti Vašoj kameri. + Potrebno dopuštenje za određivanje trenutačne lokacije za prijedloge kategorija (nije obvezno) + U redu + Mjesta u blizini + Nisu pronađena mjesta u blizini + Upozorenje + Ova datoteka već postoji na Zajedničkom poslužitelju. Jeste li sigurni da želite nastaviti? + Da + Ne + Naslov + Naslov medija + Opis + Ovdje ide opis datoteke. Mogao bi biti poprilično dug i trebat će se prelomiti u nekoliko redova. Nadamo se da će lijepo izgledati. + Autor + Ovdje ide suradničko ime autora izabrane slike. + Datum postavljanja + Licencija + Koordinate + Ništa nije navedeno + Postani beta tester + Prijavite se na naš beta-kanal na Google Playu i dobijte raniji pristup novim mogućnostima i ispravkama pogrješaka + Kôd za provjeru u 2 koraka + Moje ograničenje nedavnih postavljanja + Najviše moguće + Nije moguće prikazati više od 500 + Postavi ograničenje nedavnih postavljanja + Kôd za provjeru u 2 koraka nije podržan. + Zaista se želite odjaviti? + Logotip Zajedničkog poslužitelja + Mrežno mjesto Zajedničkog poslužitelja + Stranica Zajedničkog poslužitelja na Facebooku + Izvorni kôd Zajedničkog poslužitelja na Githubu + Pozadinska slika + Slika nije uspjela + Slika nije pronađena + Postavi sliku + Planina Zao + Ljame + Dugin most + Tulipan + Bez selfija + Vlasnička slika + Welcome (Wikipedija) + Dobro došli (autorska prava) + Sidnejska opera + Odustani + Otvori + Zatvori + Početna stranica + Postavljanje + U blizini + O + Postavke + Povratna informacija + Odjava + Upute + Obavijesti + Izabrano + Mjesta u blizini ne mogu biti prikazana bez dopuštenja određivanja lokacije + nema opisa + Stranica datoteke na Zajedničkom poslužitelju + Stavka na Wikidati + Članak na Wikipediji + Pogrješka predmemoriranja slika + Jedinstveni naziv datoteke koji će služiti kao njeno ime. Možete koristiti uobičajeni jezik s razmacima. Ne uključuje datotečni nastavak. + Opišite medij što je više moguće: gdje je napravljen, što prikazuje,... Opišite objekte ili osobe. Napišite informacije koje ne mogu biti lako okrivene, npr. doba dana ako je u pitanju pejzaž. Ako medij prikazuje nešto neobično, molimo objasnite što je neobično. + Slika je pretamna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti. + Slika je mutna, želite ili je ipak ostaviti? Zajednički poslužitelj je namijenjen slikama od enciklopedijske vrijednosti. + Daj dopuštenje + Rabi vanjsku pohranu + Spremite slike načinjene kamerom Vašeg uređaja + Prijavite se na Vaš račun + Pošalji zapisnik + Pošalji zapisnik elektroničkom poštom razvijateljima + Preglednik nije pronađen + Pogrješka! URL nije pronađen + Predloži za brisanje + Slika je predložena za brisanje + Pogledaj u pregledniku + Lokacija nepromijenjena. + Lokacija nedostupna. + Potrebno je dopuštenje za popis mjesta u blizini + PRIBAVI UPUTE + PROČITAJ ČLANAK + %1$s, dobro došli na Zajednički poslužitelj! Drago nam je da ste tu. + %1$s Vam je ostavio poruku na Vašoj razgovornoj stranici + Hvala Vam na uređivanju + %1$s Vas je spomenuo na %2$s. + Prebaci prikaz + UPUTE + WIKIDATA + WIKIPEDIJA + ZAJEDNIČKI POSLUŽITELJ + <u>Ocijenite nas</u> + <u>ČPP</u> + Preskoči upute + Internet nije dostupan + Internet je dostupan + Pogrješka dohvaćanja obavijesti + Nema obavijesti + <u>Prevedi</u> + Jezici + Odaberite jezik na koji bi željeli prevoditi + Nastavi + Odustani + Pokušaj ponovo + U redu! + Ovo su mjesta u blizini koja trebaju slike za ilustriranje članaka o njima na Wikipediji + Dodirnite da biste dobili popis ovih mjesta + Možete postaviti sliku s bilo kojeg mjesta u Vašoj galeriji ili kameri + Slike nisu pronađene! + Pogrješka prilikom učitavanja slika. + Postavio: %1$s + Aplikacija za dijeljenje + Prilikom označavanja slike koordinate nisu navedene + Pogrješka prilikom dohvaćanja mjesta u blizini. + diff --git a/app/src/main/res/values-hrx/strings.xml b/app/src/main/res/values-hrx/strings.xml index 7905a92d7..b2b57412a 100644 --- a/app/src/main/res/values-hrx/strings.xml +++ b/app/src/main/res/values-hrx/strings.xml @@ -13,7 +13,7 @@ Bittschön woorte … Oonmeldung erfollichreich! Oonmeldung fehlgeschlooht! - Authentifizierung fehlgeschlooht! + Authentifizierung fehlgeschlooht! Hochloode oongefang! „%1$s“ hochgelood! Tippe mit dein Fingerspitze, um dein Upload (Ufflloodung) oonzusiehn @@ -35,8 +35,6 @@ Titel Beschreibung Oonmeldung fehlgeschlooht – Netzwerrekfehler - Oonmeldung fehlgeschlooht – Bittschön Benutzernoome üwerprüfe - Oonmeldung fehlgeschlooht – Bittschön Passwort üwerprüfe Zu viele erfollichlose Versuche. Bittschön in en poor Minute wieder erneit versuche. Der Benutzer woard leider uff Commons gesperrt Oonmeldung fehlgeschlooht diff --git a/app/src/main/res/values-hsb/strings.xml b/app/src/main/res/values-hsb/strings.xml index 97b0d1af5..a1a9a6578 100644 --- a/app/src/main/res/values-hsb/strings.xml +++ b/app/src/main/res/values-hsb/strings.xml @@ -12,7 +12,7 @@ Prošu čakaj… Přizjewjenje wuspěšne! Přizjewjenje je so njeporadźiło! - Awtentifikacija je so njeporadźiła! + Awtentifikacija je so njeporadźiła! Nahraće je so započało! %1$s nahraty! Dótkń so, zo by swoje nahraće widźał @@ -34,8 +34,6 @@ Titul Wopisanje Přizjewjenje je so njeporadźiło - syćowy zmylk - Přizjewjenje je njemóžno - prošu přepruwuj swoje wužiwarske mjeno - Přizjewjenje njeje móžno - prošu přepruwuj swoje hesło Přewjele njewuspěšnych pospytow. Prošu spytaj za něšto mjeńšin hišće raz. Tutoho wužiwarja su bohužel na Commons zablokowali Přizjewjenje je so njeporadźiło diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 81e655b95..30bf6a193 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -31,7 +31,7 @@ Sikeres bejelentkezés A bejelentkezés nem sikerült. A fájl nem található. Próbálkozz másik fájllal. - Sikertelen hitelesítés. + Sikertelen hitelesítés. Feltöltés elindult. %1$s feltöltve. Feltöltés megtekintése @@ -59,8 +59,7 @@ Kérlek, adj címet a fájlnak Leírás Nem lehet bejelentkezni - hálózati hiba - Nem lehet bejelentkezni - ellenőrizd a felhasználóneved - Nem lehet bejelentkezni - ellenőrizd a jelszavad + Nem sikerült bejelentkezni – kérlek, ellenőrizd a felhasználónevedet és a jelszavadat Túl sok sikertelen próbálkozás. Próbálkozz újra pár perc múlva. Sajnáljuk, ezt a felhasználót blokkolták a Commonson Meg kell adnia a kétlépcsős hitelesítő kódját. @@ -114,7 +113,7 @@ Alapértelmezett licenc Előző cím/leírás használata Automatikusan megkapja a jelenlegi helyet - Lekéri a jelenlegi helyet, hogy lehetőség legyen kategóriajavaslatokra a nem földrajzi címkézett képeknél. + Lekéri a jelenlegi helyet, hogy lehetőség legyen kategóriajavaslatokra a nem földrajzi címkézett képeknél. Éjszakai mód Sötét téma használata Nevezd meg! – Így add tovább! 4.0 @@ -162,8 +161,8 @@ Nincs leírás Ismeretlen licenc Frissítés - Szükséges engedély: Külső tárhely olvasása. Az alkalmazás nem működik enélkül. - Szükséges engedély: Külső tárhely írása. Az alkalmazás nem működik enélkül. + Szükséges engedély: Külső tárhely olvasása. Az alkalmazás nem működik enélkül. + Szükséges engedély: Külső tárhely írása. Az alkalmazás nem tudja használni a kamerát enélkül. Lehetséges engedély: Jelenlegi hely megszerzése, a kategóriajavaslatok lehetőségéért. OK Közeli helyek @@ -242,6 +241,7 @@ A hely nem változott. A hely nem érhető el. Közeli helyek listájának megtekintéséhez engedély szükséges + SZÓCIKK OLVASÁSA Üdvözlünk a Wikimedia Commonson, %1$s! Örülünk, hogy itt vagy. %1$s üzenetet hagyott a vitalapodon Köszönjük a szerkesztésedet! @@ -254,9 +254,19 @@ Internet nem elérhető Internet elérhető Nincs értesítés + <u>Fordítás</u> Nyelvek Folytatás Mégse Újra + Értettem! + Ezek a helyek vannak a közeledben, amikről van Wikipédia szócikk és nincs bennük kép. + A gombra koppintva bejön egy lista, ami ezeket a helyeket mutatja. + Bármelyik helyhez feltölthetsz képet a galériádból vagy készíthetsz újat a kamerával. + Nem található kép! + Képbetöltés közben hiba történt + Feltöltötte: %1$s Alkalmazás megosztása + A koordináták nem lettek megadva a kép kiválasztásakor. + Hiba a közeli helyek elérésekor. diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 16185f35d..70a0f556a 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -19,7 +19,7 @@ Berhasil masuk log! Masuk log gagal! Berkas tidak ditemukan, silakan coba berkas lain - Autentikasi gagal! + Autentikasi gagal! Mulai unggah! %1$s terunggah! Tekan untuk melihat unggahan Anda @@ -46,8 +46,6 @@ Judul Deskripsi Tidak dapat login - kesalahan pada jaringan - Tidak dapat masuk log - harap periksa nama pengguna Anda - Tidak dapat masuk log - harap periksa kata sandi Anda Terlalu banyak usaha yang gagal. Harap coba lagi dalam beberapa menit Maaf, pengguna ini telah diblokir di Commons Gagal masuk log diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index ba5bdfe42..6eff4d9f3 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -21,7 +21,7 @@ Innskráning tókst! Innskráning mistókst! Skráin fannst ekki. Prófaðu aðra skrá. - Auðkenning tókst ekki! + Auðkenning tókst ekki! Innsending í gangi! %1$s var sent inn! Bankaðu til að skoða sem þú ert að senda inn @@ -49,8 +49,6 @@ Gefðu þessari skrá einhvern titil Lýsing Innskráning mistókst - bilun í neti - Innskráning mistókst. Athugaðu notandanafnið þitt - Innskráning mistókst. Athugaðu lykilorðið þitt Of margar misteknar tilraunir. Reyndu aftur eftir nokkrar mínútur. Því miður, þessi notandi hefur verið bannaður á Commons Þú verður að setja inn tveggja-þrepa auðkenningarkóðann þinn. @@ -104,7 +102,7 @@ Sjálfgefið notkunarleyfi Nota fyrri titil/lýsingu Ná sjálfvirkt í núverandi staðsetningu - Lesa núverandi staðsetningu til að geta stungið upp á flokkum ef myndin er ekki með hnattstaðsetningarhnitum + Lesa núverandi staðsetningu til að geta stungið upp á flokkum ef myndin er ekki með hnattstaðsetningarhnitum Næturhamur Nota dökkt þema Attribution-ShareAlike 4.0 @@ -263,4 +261,14 @@ Halda áfram Hætta við Reyna aftur + Náði því! + Þetta eru þeir staðir í næsta nágrenni við þig sem vantar myndir til að skýra með Wikipedia-greinar + Ef ýtt er á þennan hnapp birtist listi yfir þessa staði + Þú getur sent inn mynd úr myndasafninu þínu eða myndavélinni + Engir myndir fundust! + Villa kom upp við að hlaða inn myndum. + Sent inn af: %1$s + Deila forriti + Hnit voru ekki tilgreind við val myndar + Villa við að sækja nálæga staði. diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 306a1e3a4..3a7d439a6 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -10,12 +10,15 @@ * Wim b --> + Esplora + Esplora Aspetto Generale Commenti Posizione Commons Impostazioni + Carica su Commons Nome utente Password Entra @@ -26,7 +29,7 @@ Accesso effettuato! Accesso non riuscito! File non trovato. Prova con un altro file. - Autenticazione non riuscita! + Autenticazione non riuscita, si prega di riprovare Caricamento iniziato! %1$s caricato! Premi per vedere i tuoi caricamenti @@ -53,8 +56,6 @@ Titolo Descrizione Impossibile effettuare l\'accesso - errore di rete - Impossibile effettuare l\'accesso - controlla il nome utente - Impossibile effettuare l\'accesso - controlla la password Troppi tentativi falliti. Riprova tra alcuni minuti. Spiacente, questo utente è stato bloccato su Commons Devi fornire il tuo codice di autenticazione a due fattori. @@ -89,6 +90,7 @@ Impostazioni Registrati Immagini in evidenza + Categoria Informazioni L\'app di Wikimedia Commons è un\'applicazione open source creata e mantenuta da beneficiari e volontari della comunità Wikimedia. La Fondazione Wikimedia non è coinvolta nella creazione, sviluppo o manutenzione dell\'applicazione. Crea una nuova <a href=\"https://github.com/commons-app/apps-android-commons/issues\">segnalazione GitHub</a> per riportare errori e suggerimenti. @@ -177,6 +179,7 @@ Pagina Facebook di Commons Codice sorgente Github di Commons Nessuna immagine trovata + Nessuna sottocategoria trovata Carica immagine Monte Zao Arcobaleno @@ -223,4 +226,22 @@ Lingue Annulla Riprova + Capito! + Questi sono i luoghi vicino a te che necessitano di immagini per illustrare le loro voci di Wikipedia + Puoi caricare un\'immagine per ogni luogo dalla tua galleria o fotocamera + Nessuna immagine trovata! + Si è verificato un errore durante il caricamento delle immagini. + Caricato da: %1$s + Condividi applicazione + Foto del giorno + Foto del giorno + Cerca + Cerca in Commons + Ricerca + Ricerche recenti: + Si è verificato un errore durante il caricamento delle categorie. + Si è verificato un errore durante il caricamento delle sottocategorie. + Sei sicuro di voler cancellare la tua cronologia di ricerca? + Cronologia di ricerca cancellata + Sessione di login scaduta, accedi nuovamente. diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index 762a3be31..97bbf4f9c 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -30,7 +30,7 @@ הכניסה הצליחה הכניסה נכשלה! הקובץ לא נמצא. נא לנסות קובץ אחר. - אימות הפרטים נכשל + אימות הפרטים נכשל, נא להיכנס מחדש ההעלאה התחילה! הקובץ %1$s הועלה! ללחוץ כאן כדי לצפות בהעלאה שלך @@ -58,8 +58,7 @@ נא לתת כותרת לקובץ הזה תיאור לא ניתן להיכנס – כשל בתקשורת - לא ניתן להיכנס – נא לבדוק את שם המשתמש שלך - לא ניתן להיכנס – נא לבדוק את הססמה שלך + לא ניתן להיכנס לחשבון – נא לבדוק את שם המשתמש ואת הסיסמה יותר מדי ניסיונות כושלים להיכנס. נא לנסות שוב בעוד מספר דקות. סליחה, החשבון הזה חסום בוויקישיתוף יש לספק את קוד האימות הדו־שלבי שלך. @@ -113,7 +112,7 @@ רישיון ברירת מחדל להשתמש בכותרת ובתיאור קודמים לקבל אוטומטית את המיקום הנוכחי - לאחזר את המיקום הנוכחי כדי להציע קטגוריות אם בתמונה אין תגי מיקום + אחזור המיקום הנוכחי אם אין בתמונה תגי מיקום, וכן הוספת תגי מיקום לתמונה. אזהרה: פעולה זו תחשוף את המיקום הנוכחי שלך. מצב לילה שימוש במצב לילה ייחוס–שיתוף זהה 4.0 @@ -273,5 +272,21 @@ המשך ביטול לנסות שוב + הבנתי! + אלה המקומות בסביבתך שזקוקים לתמונות כדי להמחיש את הערכים שלהם בוויקיפדיה + ניתן ללחוץ על כפתור זה כדי להציג רשימה של המקומות האלה + באפשרותך להעלות תמונה של כל מקום מהגלריה או מהמצלמה שלך + לא נמצאו תמונות! + אירעה שגיאה בטעינת התמונות. + הועלתה על־ידי: %1$s שיתוף היישום + לא צוינו קואורדינטות בעת בחירת התמונה + שגיאה באחזור המקומות בסביבתך. + תמונת היום + תמונת היום + התמונה נוספה בהצלחה ל־%1$s בוויקינתונים! + לא ניתן היה לעדכן הישות המתאימה בוויקינתונים! + הגדרת רקע + הרקע הוגדר בהצלחה! + זמן הכניסה לחשבון פקע, נא להיכנס שוב. diff --git a/app/src/main/res/values-ja/error.xml b/app/src/main/res/values-ja/error.xml index 9d95a7d2d..bbc722ee1 100644 --- a/app/src/main/res/values-ja/error.xml +++ b/app/src/main/res/values-ja/error.xml @@ -1,10 +1,11 @@ - コモンズがクラッシュしました - エラーが発生しました! + コモンズアプリがクラッシュしました + おおっと、何かおかしいようです! 何をしていたかを記入してメールでお送りください。それをもとに問題点を解決します。 ありがとうございます! diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index e34c98432..c3badb9fb 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -12,20 +12,25 @@ * Yusuke1109 --> + 表示 + 全般 フィードバック 場所 コモンズ + 設定 利用者名 パスワード + コモンズのベータ版アカウントにログイン ログイン + パスワードを忘れた場合 利用者登録 ログイン中 お待ちください… ログインしました! ログインに失敗しました! ファイルが見つかりません。別のファイルでお試しください。 - 認証に失敗しました! + 認証に失敗しました! もう一度ログインしてください アップロードを開始しました! %1$s をアップロードしました! アップロードしたものを表示するにはタップしてください @@ -35,7 +40,8 @@ %1$s のアップロードに失敗しました 閲覧するにはタップしてください - %1$d 件のファイルをアップロード中 + %1$d file uploading + %1$d件のファイルをアップロード中 自分の最近のアップロードファイル 順番待ち中 @@ -49,13 +55,13 @@ 共有 ブラウザーで表示 タイトル + ファイル名をつけてください 説明 ログインできません - ネットワークのエラーです - ログインできません - 利用者名を確認してください - ログインできません - パスワードを確認してください - 失敗した回数が多すぎます。数分でもう一度お試しください。 + ログインできません - 利用者名とパスワードを確認してください + 失敗した回数が多すぎます。数分待ってからもう一度お試しください。 申し訳ありませんが、この利用者はコモンズでブロックされています。 - 2 要素認証コードを提供する必要があります。 + 2段階認証コードを入力する必要があります。 ログイン失敗 アップロード このセットに名前をつけてください @@ -64,26 +70,30 @@ カテゴリを検索 保存 更新 + 一覧 お使いのデバイスではGPSが無効になっています。有効にしますか? GPSを有効にする まだ何もアップロードされていません。 \@string/contributions_subtitle_zero - %1$d 件のアップロード + pne=%1$d upload - %1$d 件のアップロードを開始中 + Starting %1$d upload + %1$d件のアップロードを開始中 + %1$d upload %1$d 件のアップロード %1$s に一致するカテゴリが見つかりません - あなたの画像をウィキメディア・コモンズで見つけやすくするためにカテゴリを追加してください。\n\nカテゴリ名の入力を開始してください。\nこの手順をスキップするにはこのメッセージをタップしてください(または戻るボタン)。 + あなたの画像をウィキメディア・コモンズで見つけやすくするためにカテゴリを追加してください。\nカテゴリ名の入力を開始してください。 カテゴリ 設定 利用者登録 + 秀逸な画像 このアプリについて - ウィキメディア・コモンズ・アプリはウィキメディア・コミュニティの助成金受給者とボランティアによって製作・メンテナンスされているオープンソースソフトウェアです。ウィキメディア財団はこのアプリの製作・開発・メンテナンスに関与していません。 + ウィキメディア・コモンズ・アプリはウィキメディア・コミュニティの助成金受給者とボランティアが製作・管理しているオープンソースソフトウェアです。ウィキメディア財団はこのアプリの製作・開発・メンテナンスに関与していません。 バグとアイディアは <a href=\"https://github.com/commons-app/apps-android-commons/issues\">Github</a> へ。 <u>プライバシー・ポリシー</u> <u>クレジット</u> @@ -95,12 +105,13 @@ まだ写真をアップロードしていません。 再試行 キャンセル - この画像が %1$s ライセンスでアップロードされます。 + この画像は%1$sライセンスのもとにアップロードされます。 + この画像の投稿に当たり、私はこれが自分自身の作品であり、著作権のあるコンテンツや自撮りは含まれていないと宣言します。 ダウンロード 既定のライセンス 前回のタイトルと記述を使用 現在の位置を自動的に取得 - 画像にジオタグが付いていない場合、現在の位置を取得してカテゴリを提案 + Retrieves current location if image is not geotagged, \n画像にジオタグが付いていない場合、現在の位置を取得して画像に添付。ご注意: 自分の現在地が明示されます。 夜モード 暗いテーマを使う 表示-継承 4.0 @@ -126,11 +137,20 @@ ウィキメディア・コモンズにはウィキペディアで使用する画像のほぼすべてが保管されています。 あなたの画像は世界中の人々が学習する助けになります! アップロードする画像はあなたご本人が撮影したものかあなたが単独で制作したものに限定します。 - - 自然物 (動植物、山)\n- 道具 (自転車、駅)\n- 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) + 自然 (動植物、山)\n• 道具 (自転車、駅)\n• 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) + 自然物 (動植物、山) + 道具 (自転車、駅) + 著名人 (市区村長・都道府県知事、自分が会ったオリンピック選手) アップロードが《禁止》のもの: - あなたの友人の自撮り写真や画像\n- インターネットからダウンロードした画像\n- 著作権のあるアプリのスクリーンショット + 自撮りもしくは友達を撮影した写真 + ウェブからダウンロードした画像 + 独自のアプリケーションのスクリーンショット アップロードの例: - 題名: シドニー・オペラハウス\n- 説明: 湾の向こうから見たシドニー・オペラハウス\n- カテゴリ: 西側から見たシドニー・オペラハウス、遠くから見たシドニー・オペラハウス + 題名: シドニーのオペラハウス + 説明: シドニーのオペラハウス。湾を挟んで撮影。 + カテゴリ: シドニーオペラハウスの西面、シドニーオペラ遠景 画像を投稿してください。ウィキペディアの記事に彩りを! ウィキペディアの画像はウィキメディア・コモンズに保管されています。 あなたの画像は世界中の人々が学習する助けになります @@ -143,14 +163,14 @@ 説明はありません。 不明なライセンス 更新 - 必要な権限:外部ストレージを読み込みます。これがなければアプリは機能しません。 - 必要な権限:外部ストレージを作成します。これがなければアプリは機能しません。 - オプションの権限:カテゴリ候補の現在の位置を取得する + 必要な権限:外部ストレージを読み込みます。これがなければアプリはギャラリーを開けません。 + 必要な権限:外部ストレージに入力します。これがないとアプリはカメラにアクセスできません。 + オプションの権限:カテゴリ候補のため現在の位置を取得する 承認 - 周りの場所 + 近くの場所 付近の場所が見つかりません 警告 - このファイルが既にコモンズにあります。本当にアップロードしますか? + このファイルは既にコモンズにあります。本当にアップロードしますか? はい いいえ タイトル @@ -158,30 +178,36 @@ 記述 ここにメディアの説明が入ります。かなり長文になる場合には数行にわたることがあります。それでも見栄えがよいと願っています。 作者 + 秀逸な画像の作者名を記入します。 アップロード日時 ライセンス 緯度経度 情報なし ベータ版を使ってみましょう! Google Playのベータ版チャンネルにオプトインして、新機能やバグ修正プログラムに早期にアクセス - 2FA コード + 2段階認証コード 最近のアップロードファイルに表示する最大件数 最大限 - 500 以上の項目を表示できません + 表示できるのは500件以下です 最近のアップロードファイルに表示する最大件数 2段階認証は現在サポートされていません。 ログアウトしてもよろしいですか? コモンズの商標 コモンズのウェブサイト コモンズのフェイスブックページ + コモンズのGithubソースコード 背景画像 + メディアイメージが失敗しました 画像がありません 画像をアップロード 蔵王連峰 リャマ レインボーブリッジ チューリップ + 自撮りはアップロードできません + 独自の著作権がある画像 ウィキペディアへようこそ + 著作権について シドニーオペラハウス キャンセル 開く @@ -195,28 +221,69 @@ ログアウト チュートリアル 通知 - 場所の権限がないと、近くの場所を表示できません + 秀逸 + 場所の権限がないため、近くの場所を表示できません 説明がありません + コモンズのファイルページ ウィキデータ項目 ウィキペディアの記事 画像をキャッシュする際のエラー ファイル固有の説明的な表題。ファイル名として使われます。平易な言葉を使い、空白を入れることができます。拡張子は含めないでください。 - 可能な限りメディアを説明してください:どこで撮られましたか?それは何を示していますか?文脈とは何ですか?物や人を説明してください。容易に推測できない情報、例えば風景の場合の時刻を明らかにする。メディアに珍しいことがある場合は、何が珍しいのかを説明してください。 - 権限を取得 + 可能な限りメディアを説明してください: 撮影地はどこですか? それは何を示していますか? どんな文脈がありますか? 被写体の物や人を説明してください。容易に推測できない情報、例えば風景であれば時刻を明示します。特筆すべき物事が映っている場合は、何が珍しいのかを説明してください。 + この画像は暗すぎますがアップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。 + ピントが合っていませんが、アップロードしますか? ウィキメディア・コモンズは百科事典に適した画像のみ受け付けます。 + 権限を付与 外部ストレージを使用 アプリ内のカメラで撮影した写真を端末に保存する 自分のアカウントにログイン ログファイルを送信する メールで開発者にログファイルを送信する + URLを開くブラウザーが見つかりません + エラーが発生しました。URL が見つかりません + 削除の提案 + この画像の削除が提案されています。 ブラウザーで表示 場所は変更されていません。 位置が無効です。 + 近くの場所を表示するには権限が必要です + 道順を調べる 記事を読む + ウィキメディアコモンズにようこそ、%1$さん! このサイトへ来てくれてありがとうございます。 + %1$さんからアナタのとーくぺ^字にメッセージが届いています + 編集をしてくれてありがとうございます + %1$さんが%2$であなたに言及しています。 + 表示の切り替え + 道順 + ウィキデータ + ウィキペディア + コモンズ <u>評価する</u> - <u>FAQ</u> + <u>よくある質問</u> チュートリアルをスキップする + インターネットに接続していません + インターネットに接続しました + 通知の取得に失敗しました + 通知はありません <u>翻訳</u> 言語 + どの言語に編集するか選択 + 次へ キャンセル - 再試行 + やり直す + 了解 + 近くでウィキペディアの記事に使う写真がない場所はこちら + このボタンをタップするとリストを表示します + 場所の写真をアップロードするには、ギャラリーから選ぶことも撮影することもできます + 画像がありません + 画像の読み込み中にエラーが発生しました + アップロードした人: %1$ + アプリをシェアする + 画像の選択中に位置情報を特定できませんでした + 付近の場所を取得しようとしてエラーが発生しました。 + 今日の一枚 + 今日の一枚 + ウィキデータの%1$sに画像を追加しました ! + 対応するウィキデータの更新に失敗しました! + 壁紙の設定 + 壁紙の設定ができました! diff --git a/app/src/main/res/values-ji/strings.xml b/app/src/main/res/values-ji/strings.xml index 6bcb88760..9fa4b1d68 100644 --- a/app/src/main/res/values-ji/strings.xml +++ b/app/src/main/res/values-ji/strings.xml @@ -1,5 +1,6 @@ @@ -17,7 +18,7 @@ אריינלאגירט מיט הצלחה! ארײַנלאגירן אדורכגעפאלן! טעקע נישט געראפן. פרובירט אפשר אן אנדער טעקע. - אויטענטיפֿיצירן דורכגעפֿאלן! + אויטענטיפֿיצירן דורכגעפֿאלן! ארויפלאדן אנגעהויבן! %1$s ארויפגעלאדן!! דרוקט צו זען אײַער ארויפֿלאד @@ -115,4 +116,7 @@ פֿידבעק אַרויסלאָגירן רעקאמנדירט + בילד פונעם טאָג + בילד פונעם טאָג + ס׳איז ניט געלונגען צו דערהיינטיקן דעם אַנטשפּרעכנדיקן בלאַט אין וויקידאַטן. diff --git a/app/src/main/res/values-jv/strings.xml b/app/src/main/res/values-jv/strings.xml index e051b6f97..c02290b4c 100644 --- a/app/src/main/res/values-jv/strings.xml +++ b/app/src/main/res/values-jv/strings.xml @@ -14,7 +14,7 @@ Kasil mlebu log! Wurung mlebu log! Barkas ora katemu. Jajalana barkas liyané. - Wurung otèntifikasi! + Wurung otèntifikasi! Wiwit ngunggah! %1$s kaunggah! Dudul saperlu ndeleng unggahané panjenengan @@ -40,8 +40,6 @@ Sesirah Wedharan Ora bisa mlebu log - jaringané gagal - Ora bisa mlebu log - tiliki jeneng panganggoné panjenengan - Ora bisa mlebu log - tiliki tembung wadiné panjenengan Kakèhan upaya sing gagal. Jajalana manèh mengko. Ngapunten, panganggo iki wis diblokir ing Commons Panjenengan kudu ngisi kodhe otèntifikasi rong faktoré panjenengan diff --git a/app/src/main/res/values-ka/strings.xml b/app/src/main/res/values-ka/strings.xml index 2bb1c5191..31103028e 100644 --- a/app/src/main/res/values-ka/strings.xml +++ b/app/src/main/res/values-ka/strings.xml @@ -15,7 +15,7 @@ სისტემაში შესვლა წარმატებით განხორციელდა! სისტემაში შესვლა ვერ განხორციელდა! ფაილი არ მოიძებნა. გთხოვთ, სცადოთ სხვა ფაილი. - ავთენტიფიკაცია ვერ მოხერხდა! + ავთენტიფიკაცია ვერ მოხერხდა! ატვირთვა დაიწყო! %1$ ატვირთულია! დააჭირეთ თქვენი ატვირთვის სანახავად @@ -42,8 +42,6 @@ სათაური აღწერა შესვლა ვერ ხერხდება - ქსელის შეცდომა - შესვლა ვერ ხერხდება - გთხოვთ შეამოწმოთ სახელი - შესვლა ვერ ხერხდება - გთხოვთ შეამოწმოთ პაროლი ძალიან ბევრი წარუმატებელი მცდელობა. გთხოვთ, რამდენიმე წუთში სცადეთ კვლავ. უკაცრავად, ეს მომხმარებელი დაბლოკილია ვიკისაწყობში თქვენ უნდა შეიყვანოთ ორფაქტორიანი ავტორიზაციის კოდი. diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 342c615b9..f465ab913 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -15,7 +15,7 @@ Tuqqna tedda! Tqqna ur teddi ara! Ulac afaylu. Ɛreḍ wayeḍ ma ulac aɣilif. - Asesteb yecceḍ! + Asesteb yecceḍ! Asali yebda! %1$s yuli! Senned akken ad twaliḍ asali-ik @@ -42,8 +42,6 @@ Azwel Aglam Ur izmir ara ad yeqqen - tuccḍa n uẓeṭṭa - Ur izmir ara ad yeqqen - wali isem-ik n useqdac - Ur izmir ara ad yeqqen - wali awal-ik uffir Ddeq n uɛraḍ ur yeddin ara. Ɛreḍ akka di kra n tisdatin Suref-aɣ, aseqdac-agi yewḥel di Commons Yessefk ad d-muddeḍ tangalt n n usesbteb s snat n tarrayin. @@ -95,7 +93,7 @@ Turagt Seqdec azwel neɣ aglam yezrin Awi s wudem awurman adig amiran - Awi adig amiran akken ad tsumreḍ taggayt ma yella tugna ur tettwacreḍ ara di tirakalt + Awi adig amiran akken ad tsumreḍ taggayt ma yella tugna ur tettwacreḍ ara di tirakalt Askar n yiḍ Seqdec asentel aberkan Attribution-ShareAlike 4.0 diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index c057e04c7..ba7a57655 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -4,58 +4,75 @@ * វ័ណថារិទ្ធ --> - Wikimedia Commons - ការកំណត់​នានា - ឈ្មោះអ្នកប្រើប្រាស់ - លេខ​សម្ងាត់​ - ឡុកអ៊ីន​ - កំពុងឡុកអ៊ីន​ - សូមរង់ចាំ… - កត់ឈ្មោះចូលបានសំរេច - កត់ឈ្មោះចូលបរាជ័យ - Authentication បានបរាជ័យ! - បានចាប់ផ្តើមការផ្ទុកឡើង! - %1$s បានផ្ទូកឡើងហើយ! - Tap ដើម្បីមើលការផ្ទុកឡើងរបស់អ្នក - កំពុងចាប់ផ្តើម ផ្ទុកឡើង %1$s - %1$s កំពុងផ្ទូកឡើង + ការ​រចនា + ទូទៅ​ + មតិយោបល់ + ទីកន្លែង + វិគីមេឌារួម + ការកំណត់​ + អត្តនាម + ពាក្យ​សម្ងាត់ + កត់ឈ្មោះចូលទៅក្នុងគណនីវិគីមេឌារួមបេតា + កត់ឈ្មោះចូល + ពេលភ្លេច​ពាក្យ​សម្ងាត់ + ចុះ​ឈ្មោះ + កំពុងកត់ឈ្មោះចូល + សូមរង់ចាំបន្តិច… + កត់ឈ្មោះចូលបានសម្រេច! + កត់ឈ្មោះចូលមិនបានសម្រេច! + រកមិនឃើញឯកសារទេ។ សូមសាកល្បងជាមួយឯកសារផ្សេងមួយទៀត។ + ការបញ្ជាក់ទទួលស្គាល់មិនបានសម្រេច។ សូមកត់ឈ្មោះចូលម្ដងទៀត។ + ការផ្ទុកឡើងបានចាប់ផ្តើមហើយ! + បានផ្ទុកឡើង %1$s ហើយ! + ចុចដើម្បីមើលអ្វីដែលអ្នកផ្ទុកឡើង + កំពុងចាប់ផ្តើមផ្ទុកឡើង %1$s + កំពុងផ្ទុកឡើង %1$s កំពុងបញ្ចប់ការផ្ទុកឡើង %1$s - ការផ្ទុកឡើង %1$s បានបរាជ័យ - Tap ដើម្បីមើល - ការផ្ទុកឡើងរបស់ខ្ញុំ - បានដាក់ក្នុងជួររង់ចាំ - បានបរាជ័យ - %1$d%% រួចរាល់ + ការផ្ទុកឡើង %1$s មិនបានសម្រេច + ចុចដើម្បីមើល + អ្វីដែលខ្ញុំផ្ទុកឡើងថ្មីៗ + ក្នុងជួររង់ចាំ + មិនបានសម្រេច + រួចរាល់​បាន %1$d%% កំពុង​ផ្ទុកឡើង​ ពីវិចិត្រសាល ថតរូប - ការផ្ទុកឡើងរបស់ខ្ញុំ - ចែករំលែក - មើលក្នុង browser + ជិតខាង + អ្វីដែលខ្ញុំផ្ទុកឡើង + ចែកចាយ + មើលក្នុងឧបករណ៍រាយរក ចំណងជើង - បរិយាយ - មិនអាចកត់ឈ្មោះចូល - បណ្តាញ network បរាជ័យ - មិនអាចកត់ឈ្មោះចូល - សូមពិនិត្យឈ្មោះអ្នកប្រើប្រាស់របស់អ្នក - មិនអាចកត់ឈ្មោះចូល - សូមពិនិត្យលេខសម្ងាត់របស់អ្នក - ការព្យាយាមមិនបានសម្រេចមានចំនួនច្រើនដងពេក។ សូមព្យាយាមម្តងទៀតនៅប៉ុន្មាននាទីក្រោយ។ - សូមអភ័យទោស អ្នកប្រើប្រាស់រូបនេះត្រូវបានហាមឃាត់នៅ Commons - កត់ឈ្មោះចូលបរាជ័យ + សូមដាក់ចំណងជើងអោយឯកសារនេះ + ការពិពណ៌នា + មិនអាចកត់ឈ្មោះចូលបានទេ ព្រោះបណ្ដាញកំពុងមានបញ្ហា។ + មិនអាចកត់ឈ្មោះចូលបានទេ។ សូមពិនិត្យអត្តនាមនិងពាក្យសម្ងាត់របស់អ្នកឡើងវិញ។ + ការព្យាយាមមិនបានសម្រេចច្រើនដងពេក។ សូមព្យាយាមម្តងទៀតនៅប៉ុន្មាននាទីក្រោយ។ + សូមអភ័យទោស អ្នកប្រើប្រាស់រូបនេះត្រូវបានហាមឃាត់នៅវិគីមេឌារួម + អ្នកចាំបាច់ត្រូវតែផ្ដល់លេខកូដសម្រាប់បញ្ជាក់ទទួលស្គាល់ពីរតង់។ + កត់ឈ្មោះចូលមិនបានសម្រេច ផ្ទុកឡើង ដាក់ឈ្មោះឲ្យសំនុំនេះ បម្រែបម្រួល ផ្ទុកឡើង ស្វែងរកចំណាត់ថ្នាក់ក្រុម រក្សាទុក + ផ្ទុកឡើងវិញ + បញ្ជី + ឧបករណ៍របស់អ្នកកំពុងបិទមិនប្រើGPS។ តើអ្នកចង់បើកវាប្រើទេ? + បើកប្រើGPS + គ្មានអ្វីដែលបានផ្ទុកឡើងទេ រកមិនឃើញចំណាត់ថ្នាក់ក្រុមដែលត្រូវនឹង %1$s ទេ - បន្ថែមចំណាត់ថ្នាក់ក្រុមអោយរូបភាពរបស់អ្នកដើម្បីអោយងាយស្រួលស្វែងរក្នុង Wikimedia Commons។\nចាប់ផ្ដើមវាយបញ្ចូលឈ្មោះចំណាត់ថ្នាក់ក្រុម។\nចុចលើសារនេះ (ឬចុចប៊ូតុងត្រលប់ក្រោយ)ដើម្បីរំលងជំហ៊ាននេះ។ + បន្ថែមចំណាត់ថ្នាក់ក្រុមអោយរូបភាពរបស់អ្នកដើម្បីអោយងាយស្រួលស្វែងរកក្នុងវិគីមេឌារួម។ ចាប់ផ្ដើមវាយបញ្ចូលឈ្មោះចំណាត់ថ្នាក់ក្រុម។ ចំណាត់ថ្នាក់ក្រុម ការកំណត់ + ចុះឈ្មោះ​ + រូបភាពឆ្នើម អំពី សូហ្វវែរប្រភពបើកទូលាយត្រូវបានចេញផ្សាយក្រោមអាជ្ញាបណ្ណ <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache License v2</a> ប្រភពកូដមាននៅ <a href=\"https://github.com/commons-app/apps-android-commons\">GitHub</a>. Bugs មាននៅ <a href=\" https://github.com/commons-app/apps-android-commons/issues\">Github</a>. - <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">គោលការភាពជាឯកជន</a> + <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">គោលការភាពឯកជន</a> អំពី - ផ្ញើមតិកែលម្អ (តាមអ៊ីមែល) + ផ្ញើមតិយោបល់ (តាមអ៊ីមែល) ចំណាត់ថ្នាក់ក្រុមដែលត្រូវបានប្រើថ្មីៗ កំពុងរង់ចាំ ធ្វើការ sync ជាលើកដំបូង… អ្នកមិនទាន់បានផ្ទុករូបថតណាមួយឡើងនៅឡើយទេ។ @@ -92,4 +109,25 @@ គ្មាន​ការ​ពណ៌នា Unknown license ធ្វើឱ្យស្រស់ + រូបភាពផ្ទៃខាងក្រោយ + គ្មានរូបភាព + ផ្ទុករូបភាពឡើង + ភ្នំហ្សាអូ + ស្ពានឥន្ទធនូ + បោះបង់ + បើក + បិទ + ទំព័រដើម + ផ្ទុកឡើង​ + ជិតខាង + អំពី + ការកំណត់​ + មតិយោបល់ + កត់ឈ្មោះចេញ + រៀនប្រើ + សារជូនដំណឹង + ឆ្នើម + គ្មានការពិពណ៌នា + វត្ថុក្នុងវិគីទិន្នន័យ + អត្ថបទវិគីភីឌា diff --git a/app/src/main/res/values-ko-rKP/strings.xml b/app/src/main/res/values-ko-rKP/strings.xml index 670479f75..4a12d8efb 100644 --- a/app/src/main/res/values-ko-rKP/strings.xml +++ b/app/src/main/res/values-ko-rKP/strings.xml @@ -15,7 +15,7 @@ 가입 성공! 가입 실패! 서류를 찾을수 없습니다. 다른 서류를 사용해주십시오. - 인증 실패! + 인증 실패! 올리적재를 시작했습니다! %1$s 서류를 올리적재하였습니다! 당신의 올리적재를 보려면 두드리세요 @@ -42,8 +42,6 @@ 제목 설명 가입할수 없습니다 - 망 오유입니다 - 가입할수 없습니다 - 사용자이름을 확인하세요 - 가입할수 없습니다 - 통행암호를 확인하세요 실패한 시도가 너무 많습니다. 몇분후에 다시 시도하세요. 죄송합니다, 이 사용자는 공용에서 차단되였습니다 두인자검증부호를 제공해야 합니다. @@ -91,7 +89,7 @@ 허가권 이전의 제목/설명을 사용하기 자동으로 현재 위치 얻기 - 화상에 지리정보꼬리표가 달려 있지 않다면, 현재 위치를 검색하여 분류를 제안해주십시오 + 화상에 지리정보꼬리표가 달려 있지 않다면, 현재 위치를 검색하여 분류를 제안해주십시오 야간방식 어두운 주제 쓰기 저작자표시-동일조건변경허락 4.0 diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index d0e1c21ed..fab4a40eb 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -1,6 +1,7 @@ Xuyabûn @@ -79,4 +80,6 @@ Destûr bide <u>Pirsên ku pir têne pirsîn</u> Rênîşandanê derbas bike + Wêneya rojê + Wêneya rojê diff --git a/app/src/main/res/values-kum/strings.xml b/app/src/main/res/values-kum/strings.xml new file mode 100644 index 000000000..34db9a395 --- /dev/null +++ b/app/src/main/res/values-kum/strings.xml @@ -0,0 +1,97 @@ + + + + Гёрюнюш + Ортакъ + + Кюйлевлер + Къоллавчу аты + Чечил + Гирмек + Чечилни унутдунгму? + Къайытланмакъ + %1$s юкленген! + %1$s юкленип тура + Къарамакъ учун бас + Гезикде + Юклев + Сурат алмакъ + Ювукъда + Юклевлерим + Пайлашмакъ + Ат + Тасвир + Юклев + Юклев + Сакъламакъ + Янгыртмакъ + Тизме + GPS ишлетмек + Юклевлер ёкъ гьали де + Категориялар + Кюйлевлер + Къайытланмакъ + Тасвири + <u>Баракаллалар</u> + Тасвири + Такрарламакъ + Гери алмакъ + Эндирмек + CC0 + CC BY-SA 3.0 + CC BY-SA 3.0 (Алмания) + CC BY-SA 3.0 (Эстония) + CC BY 3.0 + CC BY-SA 4.0 + CC BY 4.0 + CC Zero + Интернетден эндирген суратларынг + Юклев уьлгю: + Дюр! + Категориялар + Юклев... + Бир зат сайланмагъан + Тасвири ёкъ + Янгыртмакъ + OK + Буварыв + Дюр + Ёкъ + Ат + Тасвир + Ясавчу + Юклев тархы + 2FA код + Лале + Оьзсуратсыз + Хош гел Википедиягъа + Гери алмакъ + Ачмакъ + Япмакъ + Баш + Юклемек + Ювукъда + Тасвир + Кюйлевлер + Чыкъмакъ + Билдиривлер + Сайламлы + тасвири табылмады + Википедия макъала + Янгылыш! Байланыв табылмады + ЯКЪЛАР + ВИКИПЕДИЯ + <u>Къыйматла бизин</u> + Интернет гиришсиз + Интернет гиришли + Билдиривлер ёкъ + <u>Таржума этмек</u> + Тиллер + Гери алыв + Такрарламакъ + Тюшюндюм! + Гьеч сурат табылмады! + Уьстевню пайлашмакъ + diff --git a/app/src/main/res/values-ky/strings.xml b/app/src/main/res/values-ky/strings.xml index 6d78e91fe..24e1fb1d7 100644 --- a/app/src/main/res/values-ky/strings.xml +++ b/app/src/main/res/values-ky/strings.xml @@ -13,7 +13,7 @@ Сураныч, күтө туруңуз… Сиз ийгиликтүү кирдиңиз Системага кирүүдө катачылык бар! - Таану катачылыгы! + Таану катачылыгы! Жүктөө башталды! %1$s жүктөлүүдө Жүктөлгөн файлды көрүү үчүн басыңыз @@ -39,8 +39,6 @@ Аталышы Баяндамасы Кирүүгө болбой жатат - тармакта үзгүлтүк бар - Кирүүгө мүмкүн эмес - сураныч, колдонуучу ысымыңызды текшериңиз - Кирүүгө мүмкүн эмес - сураныч, сыр сөзүңүздү текшериңиз Өтө көп натыйжасыз иш аракет. Суранабыз, бир нече мүнөттөн кийин кайталаңыз Кечириңиз, бул кодонуучу Уикиказынада блокко алынган. Системага кирүүдө катачылык бар! diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 5abca6562..50617e501 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -12,6 +12,7 @@ Commons Astellungen + Op Commons eroplueden Benotzernumm Passwuert Loggt Iech an Äre Commons-Beta-Benotzerkont an @@ -23,7 +24,7 @@ Umeldung huet geklappt! D\'Aloggen huet net funktionéiert Fichier net fonnt. probéiert w.e.g. en anere Fichier. - Authentifizéierung huet net funktionéiert! + Authentifizéierung huet net funktionéiert, loggt Iech w.e.g. nach eng Kéier an. D\'Eroplueden huet ugefaang! %1$s eropgelueden! Dréckt fir de Fichier ze gesinn deen Dir eropgelueden hutt @@ -50,8 +51,6 @@ Titel Beschreiwung Aloggen huet net funktionéiert - Feeler mam Reseau - Login net méiglech - kuckt w.e.g. Äre Benotzernumm no - Login net méiglech - kuckt w.e.g. Äert Passwuert no Ze dacks ouni Succès probéiert. Probéiert w.e.g. an e puer Minutten nach eng Kéier. Pardon, dëse Benotzer ass op Commons gespaart Aloggen huet net funktionéiert @@ -103,7 +102,7 @@ Standardlizenz Viregen Titel/Beschreiwung benotzen Automatesch déi aktuell Plaz kréien - Aktuell Plaz ofruffe fir Propose fir Kategorien ze erméigleche wann d\'Bild keng Geotaggen huet + Aktuell Plaz ofruffe fir Propose fir Kategorien ze erméigleche wann d\'Bild keng Geotaggen huet Nuetsmodus Donkele Layout benotzen Attribution-ShareAlike 4.0 @@ -178,6 +177,8 @@ Et kënnen der net méi wéi 500 gewise ginn Wëllt dir Iech wierklech ausloggen? Commons-Logo + Internetsite vu Commons + Commons-Facebook-Säit Hannergrondbild Kee Bild fonnt Bild eroplueden @@ -240,4 +241,14 @@ Virufueren Ofbriechen Nach eng Kéier probéieren + Verstanen! + Keng Biller fonnt! + Feeler beim Eropluede vu Biller. + Eropgeluede vum: %1$s + Dir sidd gespaart a kënnt keng Ännerungen op Commons maachen + Bild vum Dag + Bild vum Dag + Déi entspriechend Wikidata-Entitéit konnt net aktualiséiert ginn! + Hannergrondbild festleeën + Hannergrondbild festgeluecht diff --git a/app/src/main/res/values-li/strings.xml b/app/src/main/res/values-li/strings.xml index a14add497..48733bd97 100644 --- a/app/src/main/res/values-li/strings.xml +++ b/app/src/main/res/values-li/strings.xml @@ -22,7 +22,7 @@ Aanmelje gelök! Aanmelje mislök! Bestandj neet gevónje. Perbeer \'n anger bestandj. - Verificatie mislök! + Verificatie mislök! Upload begós! %1$s upgeloadj! Wies aan veur dienen upload te betrachte @@ -50,8 +50,6 @@ Gaef estebleef \'ne naam veur dit bestandj Besjrieving Kan zich neet aanmelde - netwirkfout - Kan zich neet aanmelde - controleer de gebroekersnaam - Kan zich neet aanmelde - controleer die wachwaord Te väöl mislökde kieëre geperbeerd. Perbeer estebleef oppernuuj euver e paar menuut. Deze gebroeker is geblokkeerd op Commons Doe mós diene twieëfaktorische bevestigingscode opgaeve. @@ -105,7 +103,7 @@ Standerdlicentie Gebroek veurige naam/besjrieving Haol autematis de hujige locatie op - Haol de hujige locatie op veur categorieveurstèlle te make wen \'t bild gein geotags haet + Haol de hujige locatie op veur categorieveurstèlle te make wen \'t bild gein geotags haet Nachmodus Gebroeker duuster thema Naamsvermeljing-GeliekDeile 4.0 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index c5c0813ab..08397cb0b 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -7,18 +7,25 @@ * Zygimantus --> + Išvaizda + Bendra + Atsiliepimai + Vietovė Vikiteka + Nustatymai Naudotojo vardas Slaptažodis + Prisijunkite prie savo Commons Beta paskyros Prisijungti + Pamiršote Slaptažodį? Užsiregistruoti Jungiamasi Prašome palaukti… Sėkmingai prisijungėte! Prisijungti nepavyko! Failas nerastas. Prašome pabandyti kitą failą. - Autentifikavimas nepavyko! + Autentifikavimas nepavyko, prašome prisijungti dar kartą Įkėlimas prasidėjo! %1$s įkelta! Bakstelėkite norėdami peržiūrėti jūsų įkėlimą @@ -43,12 +50,13 @@ Dalintis Atidaryti naršyklėje Pavadinimas + Prašome pateikti šiam failui pavadinimą Aprašymas Negalima prisijungti - tinklo klaida - Negalima prisijungti - prašome patikrinti savo vartotojo vardą - Negalima prisijungti - prašome patikrinti savo slaptažodį + Nepavyko prisijungti - prašome patikrinti savo naudotojo vardą ir slaptažodį Per daug nesėkmingų bandymų. Pabandykite dar kartą po keleto minučių. Atsiprašome, šis vartotojas buvo užblokuotas Commons + Turite pateikti savo dviejų žingsnių patvirtinimo kodą. Prisijungti nepavyko Įkelti Pavadinkite šį rinkinį @@ -57,6 +65,7 @@ Ieškoti kategorijas Išsaugoti Atnaujinti + Sąrašas GPS išjungta jūsų įrenginyje. Ar norite įjungti? Išjungti GPS Nėra įkėlimų kol kas @@ -78,8 +87,10 @@ Kategorijos Nustatymai Užsiregistruoti + Rinktiniai Paveikslėliai Apie - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Privatumo politika</a> + <u>Privatumo politika</u> + <u>Kūrėjai</u> Apie Siųsti Atsiliepimą (El. paštu) Nėra įdiegtos el. pašto tvarkyklės @@ -90,10 +101,10 @@ Atšaukti Šio paveikslėlio licencija bus %1$s Parsisiųsti - Licencija + Numatytoji Licencija Naudoti ankstesnį pavadinimą/aprašymą Automatiškai gauti dabartinę vietą - Gauti dabartinę vietove, kad būtų pasiūlytos kategorijos, jei paveikslėlis neturi geografinės žymės + Gauti dabartinę vietove, kad būtų pasiūlytos kategorijos, jei paveikslėlis neturi geografinės žymės Naktinis režimas Naudoti tamsią temą CC BY-SA 4.0 @@ -101,8 +112,15 @@ Vikimedija Commons talpina daugumą paveikslėlių, kurie yra naudojami Vikipedijoje. Jūsų paveikslėliai padeda šviesti žmones visame pasaulyje! Prašome kelti nuotraukos, kurios yra padarytos ar sukurtos tik jūsų: - - Gamtos objektai (gėlės, gyvūnai, kalnai)\n- Naudingi objektai (dviračiai, traukinių stotys)\n- Įžymūs žmonės (merai, Olimpiniai atletai) + Gamtos objektai (gėlės, gyvūnai, kalnai)\n• Naudingi objektai (dviračiai, traukinių stotys)\n• Įžymūs žmonės (jūsų meras, Olimpiniai atletai, kurios sutikote) + Gamtos objektai (gėlės, gyvūnai, kalnai) + Naudingi objektai (dviračiai, traukinių stotys) + Įžymūs žmonės (jūsų meras, Olimpiniai atletai, kurios sutikote) Prašome NEkelti: + - Asmenukės ar jūsų draugų nuotraukos\n- Nuotraukos, kurias atsisiuntėte iš interneto\n- Patentuotų programėlių nuotraukos + Asmenukės ar jūsų draugų nuotraukos + Nuotraukos, kurias atsisiuntėte iš interneto + Patentuotų programėlių nuotraukos Pavyzdinis įkėlimas: Įkelkite savo paveikslėlius. Padėkite Vikipedijos straipsniams būti spalvingesniems! Paveikslėliai Vikipedijoje yra iš Vikimedija Commons. diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 1eba570d2..5f1d28ce2 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -16,7 +16,7 @@ Lūdzu, uzgaidiet… Ieiešana veiksmīga Pieteikšanās neizdevās. - Autentifikācija neizdevās! + Autentifikācija neizdevās! Augšupielāde sākās! %1$s augšupielādēti! Uzsāk %1$s augšupielādi @@ -109,4 +109,5 @@ Valodas Turpināt Atcelt + Sapratu! diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 4e44dad60..b5c7a03ad 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -3,6 +3,8 @@ * Bjankuloski06 --> + Истражи + Истражи Изглед Општи Мислења @@ -10,6 +12,7 @@ Ризница Поставки + Подигни на Ризницата Корисничко име Лозинка Најавете се на бета-сметката @@ -21,7 +24,7 @@ Најавата е успешна! Најавата не успеа! Не ја пронајдов податотеката. Пробајте со друга. - Заверката не успеа! + Заверката не успеа. Најавете се повторно. Подигањето започна Податотеката „%1$s“ е подигната! Допрете за да го погледате подигањето @@ -49,8 +52,7 @@ Ставете ѝ наслов на податотеката Опис Не можам да Ве најавам — мрежата не работи - Не можам да Ве најавам — проверете си го корисничкото име - Не можам да Ве најавам — проверете си ја лозинката + Не можев да ве најавам. Проверете ги корисничкото име и лозинката. Направени се премногу неуспешни обиди. Обидете се пак за некоја минута. Нажалост, корисникот е блокиран на Ризницата Мора да го укажете вашиот код за двочинителска заверка. @@ -85,6 +87,7 @@ Нагодувања Регистрација Избрани слики + Категорија За извршникот Прилогот на Ризницата има отворен код. Негови творци и одржувачи се примателите на наменските средства од Викимедиината заедница како и членовите на заедницата. Фондацијата Викимедија нема учество во нејзиното создавање, разработка и одржување. Создајте нов <a href=\"https://github.com/commons-app/apps-android-commons/issues\">проблем на GitHub</a> за пријавување на грешки и давање предлози. @@ -104,7 +107,7 @@ Стандардна лиценца Користи претходен наслов/опис Автоматски давај тековна местоположба - Добивање на тековната местоположба за да се дадат предлози за категории, доколку сликата нема геоознаки + Става геоознака од тековната местоположба во слика (ако ја нема). Предупредување: ова ви го разоткрива наоѓалиштето. Ноќен режим Користи темен изглед Наведи извор-Сподели под исти услови 4.0 @@ -192,6 +195,8 @@ Заднинска слика Сликата не успеа Не најдов слики + Не пронајдов поткатегории + Не пронајдов матични категории Подигни слика Зао Лами @@ -263,5 +268,32 @@ Продолжи Откажи Пробај пак + Јасно! + Ова се места во ваша близинана кои им требаат слики за илустрирање на нивните статии на Википедија + Ако допрете на копчево ќе добиете список на тие места + Можете да подигнете слика за било кое од местата од вашата галерија или камера + Не пронајдов ниедна слика! + Се појави грешка при вчитувањето на сликите. + Подигач: %1$s + Спречени сте да ја уредувате Ризницата Сподели прилог + Не беа укажани координати при изборот на сликата + Грешка при добивањето на околните места. + Слика на денот + Слика на денот + Пребарај + Пребарај по Ризницата + Нема слики што одговараат на %1$s + Пребарај + Скорешни пребарувања: + Скорешни пребарувања + Се јави грешка при вчитувањето на категориите. + Се јави грешка при вчитувањето на поткатегориите. + Сликата е успешно додадена кон %1$s на Википодатоците! + Не успеав да ја изменам соодветната единица на Википодатоците! + Задај позадина + Позадината е успешно зададена! + Дали сте сигурни дека сакате да ја избришете историјата на пребарувања? + Историјата на пребарување е избришана + Најавната седница истече. Најавете се повторно. diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml index 632cc0265..f5ca39042 100644 --- a/app/src/main/res/values-ml/strings.xml +++ b/app/src/main/res/values-ml/strings.xml @@ -22,7 +22,7 @@ പ്രവേശനം വിജയകരം! പ്രവേശനം പരാജയപ്പെട്ടു! പ്രമാണം കണ്ടെത്താനായില്ല. ദയവായി മറ്റൊരു പ്രമാണം നോക്കുക. - സാധുതാനിർണ്ണയം പരാജയപ്പെട്ടു! + സാധുതാനിർണ്ണയം പരാജയപ്പെട്ടു! അപ്‌ലോഡ് തുടങ്ങി! %1$s അപ്‌ലോഡ് ചെയ്തിരിക്കുന്നു! താങ്കളുടെ അപ്‌ലോഡ് കാണാനായി ടാപ് ചെയ്യുക @@ -50,8 +50,6 @@ ഈ പ്രമാണത്തിന് ഒരു തലക്കെട്ട് നൽകുക. വിവരണം പ്രവേശിക്കാനായില്ല - നെറ്റ്‌വർക്ക് പരാജയപ്പെട്ടു - പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ ഉപയോക്തൃനാമം പരിശോധിക്കുക - പ്രവേശിക്കാനായില്ല - ദയവായി താങ്കളുടെ രഹസ്യവാക്ക് പരിശോധിക്കുക നിരവധി വിജയകരമല്ലാത്ത ശ്രമങ്ങൾ നടന്നിരിക്കുന്നു. വീണ്ടും ശ്രമിക്കുന്നതിനു മുമ്പ് ഏതാനം മിനിറ്റുകൾ വിശ്രമിക്കുക. ക്ഷമിക്കുക, ഈ ഉപയോക്താവ് കോമൺസിൽ നിന്ന് തടയപ്പെട്ടിരിക്കുകയാണ് താങ്കളുടെ ദ്വി-ഘടക സാധൂകരണ കോഡ് നൽകുക. diff --git a/app/src/main/res/values-mr/strings.xml b/app/src/main/res/values-mr/strings.xml index b88e021e7..b5edf2370 100644 --- a/app/src/main/res/values-mr/strings.xml +++ b/app/src/main/res/values-mr/strings.xml @@ -26,7 +26,7 @@ सनोंद प्रवेश यशस्वी! सनोंद प्रवेश अयशस्वी! संचिका सापडली नाही. कृपया दुसऱ्या संचिकेसाठी प्रयत्न करा. - अधिप्रमाणन अयशस्वी! + अधिप्रमाणन अयशस्वी! अपभारण सुरू झाले! %1$s अपभारीत! आपले अपभारण बघण्यास अलगद टपली मारा @@ -54,8 +54,6 @@ कृपया या फाईलसाठी शीर्षक प्रदान करा वर्णन सनोंद प्रवेश अशक्य - नेटवर्क नाही - सनोंद प्रवेश अशक्य - कृपया आपले सदस्यनाव तपासा - सनोंद प्रवेश अशक्य - कृपया आपला परवलीचा शब्द तपासा अनेक अयशस्वी प्रयत्न.काही मिनीटांनंतर पुन्हा प्रयत्न करा. माफ करा, कॉमन्सवर हा सदस्य प्रतिबंधित आहे आपण आपल्या दोन कारक प्रमाणिकरण कोड प्रदान करणे आवश्यक आहे. @@ -107,7 +105,7 @@ डिफॉल्ट परवाना मागील शीर्षक/वर्णन वापरा आपोआप सध्याचे स्थान मिळवा - जर छायाचित्राला जिओटॅग नसल्यास तुम्ही तुमच्या स्थानाची निश्चिती करा जेणे करुन संबंधीत वर्ग सुचवले जातील + जर छायाचित्राला जिओटॅग नसल्यास तुम्ही तुमच्या स्थानाची निश्चिती करा जेणे करुन संबंधीत वर्ग सुचवले जातील रात्रीच्या वेळेच्या व्यवस्था गडद त्वचा वापरा Attribution-ShareAlike 3.0 diff --git a/app/src/main/res/values-ms/strings.xml b/app/src/main/res/values-ms/strings.xml index c5c0f18ab..e58c34daa 100644 --- a/app/src/main/res/values-ms/strings.xml +++ b/app/src/main/res/values-ms/strings.xml @@ -14,7 +14,7 @@ Sila tunggu… Berjaya log masuk! Gagal log masuk! - Penentusahan gagal! + Penentusahan gagal! Pemuatnaikan telah bermula! %1$s telah dimuat naik! Ketik untuk melihat muatan naik anda @@ -37,8 +37,6 @@ Tajuk Keterangan Tidak boleh log masuk - kegagalan rangkaian - Tidak dapat log masuk - Sila semak nama pengguna anda - Tidak dapat log masuk - Sila semak kata laluan anda Terlalu banyak cubaan yang tidak berjaya. Sila cuba lagi dalam beberapa minit Maaf, pengguna ini telah disekat di Commons Log masuk gagal diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 9774035ee..7e044f0b8 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -1,5 +1,6 @@ Utseende + Generelt Tilbakemelding + Beliggenhet Commons Innstillinger Brukernavn Passord + Logg inn på Commons Beta-kontoen din Logg inn + Glemt passordet? Registrer deg Logger inn Vennligst vent … Innloggingen var vellykket! Innloggingen feilet! Filen ble ikke funnet. Forsøk med en annen fil. - Autentiseringen feilet! + Autentiseringen feilet, vennligst logg inn igjen Opplastingen startet %1$s lastet opp! Trykk for å vise din opplasting @@ -47,10 +52,10 @@ Del Vis i nettleser Tittel + Angi en tittel for denne fila Beskrivelse Innlogging feilet - nettverksproblem - Innlogging feilet - sjekk brukernavnet ditt - Innlogging feilet - sjekk passordet ditt + Kunne ikke logge inn – sjekk brukernavnet og passordet ditt For mange misslykkede forsøk. Vennligst prøv igjen om noen få minutter. Beklager, denne brukeren har blitt blokkert på Commons Du må oppgi tofaktorautentiseringskoden din. @@ -62,6 +67,7 @@ Søk kategorier Lagre Oppdater + Liste GPS er slått av på denne enheten. Ønsker du å slå den på? Slå på GPS Ingen opplastinger ennå @@ -83,11 +89,12 @@ Kategorier Innstillinger Registrer deg + Utvalgte bilder Om Wikimedia Commons-appen er åpen kildekode og er skapt og vedlikeholdt av stipendiater og frivillige fra Wikimedia-fellesskapet. Wikimedia Foundation er ikke involvert i utviklingen eller vedlikeholdet av appen. Opprett en ny <a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub-sak</a> for feilrapporter og forslag. - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">Personvernpolicy</a> - <a href=\"https://github.com/commons-app/apps-android-comons/blob/master/CREDITS\">Bidragsytere</a> + <u>Personvernpolicy</u> + <u>Bidragsytere</u> Om Send tilbakemelding (via Epost) Ingen epostklient installert @@ -102,7 +109,7 @@ Standardlisens Bruk forrige tittel/beskrivelse Hent automatisk nåværende plassering - Hent nåværende plassering for å tilby kategoriforslag om bildet ikke er geotagget + Hent nåværende plassering om bildet ikke er geotagget, og tagger bildet med plasseringen. Advarsel: Dette vil avsløre hvor du befinner deg for øyeblikket. Nattmodus Bruk mørk drakt Navngivelse-Del på samme vilkår 4.0 @@ -130,15 +137,18 @@ Last opp bilder som er tatt eller skapt av deg: – Naturlige objekter (blomster, dyr, fjell)\n– Nyttige objekter (sykler, jernbanestasjoner)\n– Berømte personer (ordføreren din, OL-deltakere du har møtt) Naturlige objekter (blomster, dyr, fjell) + Nyttige objekter (sykler, togstasjoner) Berømte personer (din ordfører, OL-deltakere du møtte) Vennligst IKKE last opp: - Selfies eller bilder av vennene dine\n- Bilder som du har lastet ned fra internet\n- Skjermbilder tatt fra proprietære apper Selvportrett eller bilder av dine venner Bilder du lastet ned fra Internett + Skjermbilder av proprietære programmer Opplastingseksempel: - Tittel: Sydneys operahus\n- Beskrivelse: Operahuset i Sydney sett fra andre siden av bukten\n- Kategorier: Operahuset i Sydney ifra vest, Operahuset i Sydney utenfra Tittel: Operahuset i Sydney Beskrivelse: Operahuset i Sydney som det sees fra motsatt side av bukten + Kategorier: Sydneys operahus fra vest, Sydneys operahus fra utsiden Bidra med dine bilder. Hjelp til med å blåse liv i Wikipedias artikler! Bilder på Wikipedia kommer fra Wikimedia Commons. Bildene dine kan være til hjelp for mennesker over hele verden som søker kunnskap og dannelse. @@ -151,8 +161,8 @@ Ingen beskrivelse Ukjent lisens Gjenoppfrisk - Nødvendig tillatelse: Lese ekstern lagring. Appen virker ikke uten dette. - Påkrevd tillatelse: Skriv til ekstern lagring. Appen fungerer ikke uten dette. + Nødvendig tillatelse: Lese ekstern lagring. Appen kan ikke få tilgang til galleriet ditt uten dette. + Nødvendig tillatelse: Skriv til ekstern lagring. Appen får ikke tilgang til kameraet ditt uten dette. Valgfri tillatelse: Hent nåværende posisjon for kategoriforslag OK Plasser i nærheten @@ -165,6 +175,8 @@ Medietittel Beskrivelse Beskrivelse av mediet skal være her. Denne kan potensielt være ganske lang, og vil trenge å strekke seg over flere linjer. Vi håper det ser bra nok ut. + Opphavsperson + Brukernavnet til opphavspersonen til utvalgte bilder kommer her. Opplastingsdato Lisens Koordinater @@ -207,10 +219,12 @@ Logg ut Veiviser Varsler + Utvalgt Steder i nærheten kan ikke vises uten tillatelse for stedsbestemmelse ingen beskrivelse funnet Commons-filside Wikidata-element + Wikipedia-artikkel Feil under mellomlagring av bilder En unik beskrivende tittel for fila, som vil fungere som filnavn. Du kan bruke vanlig språk med mellomrom. Ikke ta med filendelsen Beskriv bidraget så mye som mulig: Hvor ble det tatt? Hva viser det? Hva er konteksten? Beskriv objektene eller personene. Gi informasjon som ikke kan gjettes lett, for eksempel når på dagen bildet ble tatt om det er et landskapsbilde. Om bildet viser noe uvanlig, forklar hva som gjør det uvanlig. @@ -223,6 +237,10 @@ Send loggfil Send loggfil til utviklerne via epost Ingen internettleser funnet som kan åpne lenken + Feil! URL ikke funnet + Nominer for sletting + Dette bildet har blitt nominert for sletting. + Vis i nettleseren Stedet har ikke blitt endret. Sted ikke tilgjengelig. Tillatelse kreves for å vise listen over steder i nærheten @@ -233,8 +251,38 @@ Takk for at du har gjort en redigering %1$s nevnte deg på %2$s. Skift visning - Ofte stilte spørsmål + VEIBESKRIVELSE + WIKIDATA + WIKIPEDIA + COMMONS + <u>Vurder oss</u> + <u>Ofte stilte spørsmål</u> + Hopp over innledning Internett er utilgjengelig + Internett tilgjengelig + Feil under henting av varsler + Ingen varsler funnet <u>Oversett</u> + Språk + Velg språket du ønsker å sende inn oversettelser for + Fortsett + Avbryt Prøv igjen + Skjønner! + Det er steder i nærheten av deg som trenger bilder for å illustrere Wikipedia-artiklene sine + Trykk på denne knappen for å få en liste over disse stedene + Du kan laste opp et bilde for alle steder fra galleriet eller kameraet ditt + Ingen bilder funnet! + Feil oppsto under lasting av bilder. + Lastet opp av: %1$s + Del appen + Koordinater ble ikke spesifisert under bildevalget + Feil under henting av steder i nærheten. + Dagens bilde + Dagens bilde + Bildet ble lagt til på %1$s på Wikidata! + Kunne ikke oppdatere tilsvarende Wikidata-element! + Angi som bakgrunnsbilde + Bakgrunnsbildet ble endret! + Innloggingsøkten har utløpt, logg inn på nytt. diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index be0d2f69f..57a1891f0 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -16,7 +16,7 @@ प्रवेश सफल! प्रवेश सफल हुन सकेन! फाइल भेटिएन। कृपया अर्को फाइल प्रयास गर्नुहोस्। - प्रमाणिकरण असफल भयो! + प्रमाणिकरण असफल भयो! अपलोड शुरू भयो! %1$s अपलोड गरियो ! तपाईंको अपलोड हेर्नको लागि ट्याप गर्नुहोस् @@ -39,8 +39,6 @@ शीर्षक वर्णन प्रवेश गर्न असमर्थ - जडान खराबी - प्रवेश गर्न असमर्थ - कृपया तपाईंको प्रयोगकर्ता नाम जाँच गर्नुहोस् - प्रवेश गर्न असमर्थ - कृपया आफ्नो पासवर्ड जाँच गर्नुहोस धेरै असफल प्रयासहरू भए । कृपया केही मिनेट पछि पुन: प्रयास गर्नुहोस माफ गर्नुहोस, यो प्रयोगकर्तालाई कमोन्समा ब्लक गरिएको छ प्रवेश सफल हुन सकेन @@ -55,7 +53,7 @@ श्रेणीहरू सेटिङ्गहरू बारेमा - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">अपाचे लाइसेन्स संस्करण२</a> को अाधारमा खुला स्रोत सफ्टवेयर जारी + विकिमिडिया कमन्स याप एक स्वतन्त्र स्रोत याप हो। यो याप विकिमिडिया समुदायका अनुदानप्राप्तकर्ताहरू र स्वयंसेवकहरू द्वारा निर्मित एवं प्रबन्धित छ। विकिमीडिया फाउण्डेसन यस यापको निर्माण, विकास र प्रबन्धनमा कुनै पनि प्रकारले संलग्न छैन। <a href=\"https://github.com/commons-app/apps-android-commons\">गिटहब</a> मा स्रोत। <a href=\" https://github.com/commons-app/apps-android-commons/issues\">बगजिल्ला</a> मा बग छ। <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">गोपनीयता नीति</a> बारेमा @@ -97,4 +95,6 @@ अज्ञान अनुमतिपत्र ताजागर्ने टगल दृश्य + भित्तेपत्र चयन गर्नुहोस् + भित्तेपत्र सफलतापूर्वक चयन भयो! diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index f2688cffa..2e84ab342 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,6 +1,7 @@ + Explorar + Explorar Commons Paramètres Nom d\'utilizaire @@ -16,7 +18,7 @@ Identificacion capitada ! Error de connexion ! Fichièr pas trobat. Ensajatz-ne un autre. - Fracàs de l’autentificacion ! + Fracàs de l’autentificacion ! Telecargament aviat ! %1$s telecargats ! Quichar per veire vòstre telecargament @@ -43,8 +45,6 @@ Títol Descripcion Impossible de se connectar — pana de ret - Impossible de se connectar — verificatz vòstre nom d’utilizaire - Impossible de se connectar — verificatz vòstre senhal Tròp de temptativas infructuosas. Ensajatz tornarmai dins qualques minutas. O planhèm, aqueste utilizaire es estat blocat dins Commons Error de connexion @@ -72,6 +72,7 @@ Categorias Paramètres S’inscriure + Categoria A prepaus <a href=\"https://github.com/commons-app/apps-android-commons\">Fonts</a> e <a href=\"https://commons-app.github.io/\">site web</a> sus GitHub. Crear un novèl <a href=\"https://github.com/commons-app/apps-android-commons/issues\">senhalament GitHub</a> per senhalar de bugs o de suggestions. <a href=\"https://wikimediafoundation.org/wiki/Privacy_policy\">Politica de confidencialitat</a> @@ -129,6 +130,8 @@ Títol del mèdia Descripcion Venir un bèta-testaire + Cap de jos-categoria trobada + Cap de categoria parenta trobada Aqueste imatge es estat designat per supression <u>Nos notar</u> <u>FAQ</u> @@ -139,4 +142,14 @@ <u>Revirar</u> Seleccionatz la lenga dins la quala volriatz sometre de traduccions Tornar ensajar + Cercar + Cercar dins Commons + Cap d\'imatges correspondent a %$s trobat + Cercar + Recèrcas recentas : + Darrièras requèstas de recèrca + I aguèt una error pendent lo cargament de las categorias. + I aguèt una error pendent lo cargament de las jos-categorias. + Sètz segur de voler vuejar vòstre istoric de recèrca ? + Istoric de recèrca suprimit diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml index c1bc2b73f..e11e0bf03 100644 --- a/app/src/main/res/values-or/strings.xml +++ b/app/src/main/res/values-or/strings.xml @@ -13,7 +13,7 @@ ଟିକେ ଅପେକ୍ଷା କରନ୍ତୁ… ଠିକଭାବେ ଲଗଇନ ହେଲା! ଲଗଇନ ହେଲାନାହିଁ! - ନିଶ୍ଚିତ କରିବାରେ ବିଫଳ! + ନିଶ୍ଚିତ କରିବାରେ ବିଫଳ! ଅପଲୋଡ଼ ଆରମ୍ଭ ହେଲା! %1$s ଅପଲୋଡ଼ ହୋଇଗଲା! ନିଜର ଅପଲୋଡ଼ ଦେଖିବା ନିମନ୍ତେ ଟ୍ୟାପ କରନ୍ତୁ @@ -35,8 +35,6 @@ ଶିରୋନାମ ବିବରଣୀ ଲଗ ଇନ କରିବାରେ ବିଫଳ - ନେଟୱାର୍କରେ ଅସୁବିଧା - ଲଗ ଇନ କରିବାରେ ବିଫଳ - ଦୟାକରି ନିଜର ସଭ୍ୟ ନାମ ପରଖିନିଅନ୍ତୁ - ଲଗ ଇନ କରିବାରେ ବିଫଳ - ଦୟାକରି ନିଜର ପାସୱାର୍ଡ଼ ପରଖିନିଅନ୍ତୁ ଖୁବ ଅଧିକ ଅସଫଳ ଚେଷ୍ଟା । ଦୟାକରି କେଇ ମିନିଟ ଛାଡ଼ି ଚେଷ୍ଟା କରନ୍ତୁ କ୍ଷମା ଘେନିବେ, ଏହି ସଭ୍ୟଙ୍କୁ କମନ୍ସରେ ଅଟକାଯାଇଛି ଲଗଇନ ହେଲାନାହିଁ diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 6937ab75d..0f70b4a42 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -16,7 +16,7 @@ ਦਾਖ਼ਲਾ ਸਫ਼ਲ ਹੋਇਆ! ਦਾਖ਼ਲਾ ਫੇਲ੍ਹ ਹੋਇਆ! ਫ਼ਾਇਲ ਦੀ ਖੋਜ ਨਹੀਂ ਹੋ ਸਕੀ। ਕਿਰਪਾ ਕਰਕੇ ਹੋਰ ਫ਼ਾਇਲ ਖੋਜੋ। - ਪ੍ਰਮਾਣਤਾ ਫੇਲ੍ਹ ਹੋਈ! + ਪ੍ਰਮਾਣਤਾ ਫੇਲ੍ਹ ਹੋਈ! ਅੱਪਲੋਡ ਸ਼ੁਰੂ ਹੋਇਆ! %1$s ਅੱਪਲੋਡ ਹੋ ਗਏ! ਆਪਣਾ ਅੱਪਲੋਡ ਵੇਖਣ ਲਈ ਥਪੇੜੋ @@ -43,8 +43,6 @@ ਸਿਰਲੇਖ ਵੇਰਵਾ ਦਾਖ਼ਲਾ ਨਹੀਂ ਹੋ ਰਿਹਾ - ਨੈੱਟਵਰਕ ਫੇਲ੍ਹ ਹੋਇਆ ਹੈ - ਦਾਖ਼ਲਾ ਨਹੀਂ ਹੋ ਰਿਹਾ - ਆਪਣਾ ਵਰਤੋਂਕਾਰ ਨਾਂ ਚੈੱਕ ਕਰੋ - ਦਾਖ਼ਲਾ ਨਹੀਂ ਹੋ ਰਿਹਾ - ਆਪਣਾ ਪਾਸਵਰਡ ਚੈੱਕ ਕਰੋ ਜੀ ਬਹੁਤ ਸਾਰੀਆਂ ਅਸਫ਼ਲ ਕੋਸ਼ਿਸ਼ਾਂ। ਥੋੜ੍ਹੀ ਦੇਰ ਬਾਅਦ ਕੋਸ਼ਿਸ਼ ਕਰੋ ਜੀ। ਅਫ਼ਸੋਸ, ਇਹ ਵਰਤੋਂਕਾਰ ਕਾਮਨਜ਼ ਉੱਤੇ ਬਲਾਕ ਕਰ ਦਿੱਤਾ ਗਿਆ ਹੈ ਦਾਖ਼ਲਾ ਫੇਲ੍ਹ ਹੋਇਆ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 77cdb547d..ac0d1808f 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -33,7 +33,7 @@ Zalogowano pomyślnie! Logowanie nie powiodło się! Nie znaleziono pliku. Spróbuj znaleźć inny. - Błąd uwierzytelniania! + Uwierzytelnianie nie powiodło się, zaloguj się ponownie Wysyłanie rozpoczęte! Przesłano %1$s! Dotknij, aby zobaczyć przesłany plik @@ -60,8 +60,7 @@ Tytuł Opis Nie można zalogować - błąd sieci - Nie można zalogować - sprawdź nazwę użytkownika - Nie można zalogować - sprawdź hasło + Nie można się zalogować - sprawdź swoją nazwę użytkownika i hasło Zbyt wiele nieudanych prób zalogowania. Spróbuj ponownie za kilka minut. Przepraszamy, ten użytkownik został zablokowany na Commons Wprowadź swój kod dla dwuetapowej autoryzacji. @@ -95,6 +94,8 @@ Kategorie Ustawienia Zarejestruj się + Wyróżniony obrazek + Kategoria O aplikacji Aplikacja Wikimedia Commons jest oprogramowaniem typu open-source tworzonym i rozwijanym przez stypendystów i wolontariuszy ze społeczności Wikimedii. Fundacja Wikimedia nie bierze udziału w tworzeniu, rozwijaniu ani utrzymywaniu aplikacji. <a href=\"https://github.com/commons-app/apps-android-commons\">Kod źródłowy</a> oraz <a href=\"https://commons-app.github.io/\">strona internetowa</a> na GitHub. Aby zgłosić błąd lub sugestię, utwórz nowe <a href=\"https://github.com/commons-app/apps-android-commons/issues\">zgłoszenie na GitHub</a>. @@ -113,7 +114,7 @@ Domyślna licencja Użyj poprzedniego tytułu/opisu Automatycznie uzyskaj bieżącą lokalizację - Pobierz aktualną lokalizację, aby uzyskać propozycje kategorii, jeśli obraz nie zawiera znaczników geograficznych + Pobierz aktualną lokalizację, aby uzyskać propozycje kategorii, jeśli obraz nie zawiera znaczników geograficznych Tryb nocny Użyj ciemnej skórki Uznanie autorstwa-Na tych samych warunkach 4.0 @@ -160,6 +161,7 @@ Nieznana licencja Odśwież Wymagane uprawnienia: odczyt z dysku zewnętrznego. Aplikacja nie będzie w stanie funkcjonować bez tego. + Wymagane uprawnienia: odczyt z dysku zewnętrznego. Aplikacja nie będzie w stanie funkcjonować bez tego. Opcjonalne zezwolenie: uzyskiwanie bieżącej lokalizacji dla wygenerowania propozycji kategorii OK Pobliskie miejsca @@ -176,6 +178,7 @@ Data przesłania Licencja Współrzędne + Nie dostarczone Zostań beta-testerem Dołącz do kanału bety w Google Play i dostań wczesny dostęp do nowych funkcji i łatek Kod 2FA @@ -185,6 +188,8 @@ Czy na pewno wylogować? Logo Commons Obraz w tle + Nie znaleziono podkategorii + Nie znaleziono kategorii nadrzędnych Załaduj zdjęcie Zaō Lamy @@ -206,12 +211,15 @@ Wyloguj Samouczek Powiadomienia + Wyróżnione nie znaleziono opisu Element Wikidanych Artykuł na Wikipedii Podaj krótką, opisową i unikalną nazwę, która będzie służyła jako nazwa pliku. Możesz używać prostego języka i spacji. Nie dodawaj rozszerzenia pliku. + Uzyskaj uprawnienie Zaloguj się na swoje konto Błąd! Nie znaleziono adresu URL + Zgłoszone do usunięcia Ta grafika została zgłoszona do usunięcia. Otwórz w przeglądarce Witamy w Wikimedia Commons, %1$s! Cieszymy się, że tu jesteś. @@ -226,5 +234,20 @@ Pomiń samouczek Nie znaleziono powiadomień Języki + Dalej Anuluj + Ponów próbę + Wszystko jasne! + Nie znaleziono grafik! + Wystąpił błąd podczas ładowania grafik. + Przesłano przez $1 + Szukaj + Ostatnie wyszukiwania: + Ostatnio wyszukiwane zapytania + Wystąpił błąd podczas ładowania kategorii. + Wystąpił błąd podczas ładowania podkategorii. + Ustaw tapetę + Tapeta ustawiona pomyślnie! + Czy na pewno chcesz usunąć swoją historię wyszukiwania? + Historia wyszukiwania została usunięta diff --git a/app/src/main/res/values-pms/strings.xml b/app/src/main/res/values-pms/strings.xml index 30115f1ad..128c8bf52 100644 --- a/app/src/main/res/values-pms/strings.xml +++ b/app/src/main/res/values-pms/strings.xml @@ -3,6 +3,8 @@ * Borichèt --> + Esploré + Esploré Aparensa General Sugeriment @@ -10,6 +12,7 @@ Comun Paràmeter + Carié su Commons Stranòm Ciav Ch\'a intra an sò cont Comun beta @@ -21,7 +24,7 @@ A l\'é intrà ant ël sistema! Falì a rintré ant ël sistema! Archivi nen trovà. Për piasì, ch\'a preuva con n\'àutr. - Autentificassion falìa! + Autentificassion falìa, për piasì ch\'as colega torna Cariament ancaminà! %1$s carià! Sgnaché për vëdde sò cariament @@ -49,8 +52,7 @@ Për piasì, ch\'a-j buta \'n tìtol a s\'archivi Descrission Impossìbil rintré ant ël sistema - la rej a marcia nen - Impossìbil rintré ant ël sistema - për piasì, ch\'a verìfica sò stranòm - Impossìbil rintré ant ël sistema - për piasì, ch\'a contròla soa ciav + Impossìbil rintré ant ël sistema - për piasì ch\'a contròla sò stranòm e soa ciav Tròpi tentativ falì. Për piasì, ch\'a preuva torna da-sì chèiche minute. An dëspias, s\'utent-sì a l\'é stàit blocà ansima a Commons A dev fornì sò còdes d\'autentificassion a doi fator. @@ -85,6 +87,7 @@ Paràmeter Marchesse Plance an evidensa + Categorìa A propòsit L\'aplicassion Wikimedia Commons a l\'é n\'aplicassion a sorgiss duverta creà e mantnùa da \'d përson-e pagà e da \'d volontari ëd la comunità Wikimedia. La Fondassion Wikimedia a l\'é nen amplicà ant la creassion, ël dësvlup, o la manutension dl\'aplicassion. Creé na neuva <a href=\"https://github.com/commons-app/apps-android-commons/issues\">signalassion GitHub</a> për signalé dij givo e dij sugeriment. @@ -104,7 +107,7 @@ Licensa dë stàndard Dovré ël tìtol o la descrission precedent Oten-e an automàtich la posission atual - Arcuperé la posission atual për propon-e dij sugeriment ëd categorìa si la plancia a l\'é nen marcà da na mira geogràfica + A arcupera la posission atual si la plancia a l\'é nen marcà da na mira geogràfica, e a marca da na mira geogràfica la plancia. Atension: Sòn a dësveilërà soa posission atual. Meud nuité Dovré un tema sombr Atribussion-Partagi ugual 4.0 @@ -192,6 +195,8 @@ Plancia dë sfond Faliment ëd la plancia dël mojen Gnun-a plancia trovà + Gnun-e sot-categorìe trovà + Gnun-é categorìe ce trovà Carié na plancia Mont Zao Lama @@ -263,5 +268,32 @@ Andé anans Anulé Prové torna + Fàit! + A-i é dij pòst davzin a chiel ch\'a l\'han da manca ëd plance për ilustré ij sò artìcoj su Wikipedia + Sgnacand su \'s boton a comparirà na lista ëd si pòst + A peul carié na fòto da \'n pòst qualsëssìa ëd soa galarìa o màchina fòto + Gnun-e plance trovà! + A-i é staje n\'eror durant ël cariament ëd le plance. + Carià da: %1$s + Chiel a l\'é stàit blocà e a peul pa modifiché su Commons Partagé j\'aplicassion + Le coordinà a son nen ëstàite spessificà durant la selession ëd la plancia + Eror durant l\'esplorassion dj\'anviron. + Plancia dël di + Plancia dël di + Arserché + Arserché su Commons + Gnun-e plance rëspondente a %1$s trovà + Arserché + Arserche recente: + Domande arsercà ëd recent + A l\'é ancapitaje n\'eror antramentre ch\'as cariavo le categorìe. + A l\'é ancapitaje n\'eror antrametre ch\'as cariavo le sot-categorìe. + Plancia giontà për da bin a %1$s su Wikidata! + Falì a agiorné l\'entità ëd Wikidata corëspondenta! + Definì la tapissarìa + La tapissarìa a l\'é stàita definìa për da bin! + É-lo sigur ëd vorèj dëscancelé lë stòrich ëd soe arserche? + Lë stòrich ëd j\'arserche a l\'é stàit dëscancelà + Session ëscadùa, për piasì ch\'a rintra torna ant ël sistema. diff --git a/app/src/main/res/values-ps/strings.xml b/app/src/main/res/values-ps/strings.xml index 3e2b499e6..2010b9bd5 100644 --- a/app/src/main/res/values-ps/strings.xml +++ b/app/src/main/res/values-ps/strings.xml @@ -37,8 +37,6 @@ مهرباني وکړئ د دې دوتنې لپاره سرلیک چمتو کړئ څرگندونه د ننوتلو توان نلري - د شبکې ناکامي - د ننوتلو توان نلري - لطفاً خپل کارن نوم وګورئ - د ننوتلو توان نلري - لطفاً خپل پټنوم وګورئ ډیری ناکامه هڅې. لطفا څو دقیقې وروسته بیا هڅه وکړئ. بخښنه غواړو، په دي کارن د کامنز لخوا بنديز ولګول شو غونډال کې ننوتنه نابريالې شوه diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6722ae4bb..bbc08f96d 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -15,6 +15,8 @@ * Tuliouel --> + Explorar + Explorar Aparência Geral Comentário @@ -22,6 +24,7 @@ Commons Configurações + Carregar no Commons Nome de usuário Senha Entre com a sua conta do Commons Beta @@ -33,7 +36,7 @@ Login bem sucedido Falha na identificação Arquivo não encontrado. Tente outro arquivo. - Falha de autenticação! + A autenticação falhou, inicie uma nova sessão, por favor Upload iniciado! %1$s enviado! Toque para ver os seu upload @@ -61,8 +64,7 @@ Forneça um título para este arquivo Descrição Erro ao efetuar o login - falha na rede - Erro ao efetuar o login - confira seu nome de usuário - Erro ao efetuar o login - confira sua senha + Não é possível fazer o login - verifique seu nome de usuário e senha Muitas tentativas malsucedidas. Tente de novo daqui alguns minutos. Desculpe, esse usuário foi banido do Commons Você precisa fornecer o seu código de ativação de dois fatores. @@ -97,6 +99,7 @@ Configurações Criar conta Imagens destacadas + Categoria Sobre O Wikimedia Commons é um aplicativo de código aberto criado e mantido por beneficiários e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção do aplicativo. Criar uma nova <a href=\"https://github.com/commons-app/apps-android-commons/issues\">publicação no GitHub</a> para informar erros e sugestões. @@ -116,7 +119,7 @@ Licença padrão Usar o título/descrição anterior Obter automaticamente a localização atual - Recuperar o local atual para oferecer sugestões de categoria se a imagem não tiver geo-tagged + Se a imagem não estiver georreferenciada, obtém a localização atual e georreferencia a imagem com ela. Aviso: isto revela a sua localização corrente. Modo noturno Usar tema escuro Atribuição-CompartilhaIgual 4.0 @@ -204,6 +207,8 @@ Imagem de fundo Falha na imagem de mídia Nenhuma imagem encontrada + Nenhuma subcategoria encontrada + Nenhuma categoria pai encontrada Enviar Imagem Mount Zao Llamas @@ -275,5 +280,32 @@ Avançar Cancelar Tentar novamente + Entendido! + Estes são os lugares perto de você que precisam de fotografias para ilustrar os respetivos artigos na Wikipédia + Tocar neste botão fará surgir uma lista destes lugares + Pode carregar uma fotografia para qualquer dos lugares, da sua galeria ou câmara + Não foi encontrada nenhuma imagem! + Ocorreu um erro durante o carregamento das imagens. + Carregada por: %1$s + Está impedido de editar no Commons Compartilhar o aplicativo + Não foram especificadas coordenadas durante a seleção da imagem + Erro ao buscar lugares próximos. + Imagem do Dia + Imagem do Dia + Pesquisar + Pesquisar Commons + Nenhuma imagem correspondente %1$s encontrada + Pesquisar + Pesquisas recentes: + Consultas recentemente pesquisadas + Ocorreu um erro ao carregar categorias. + Ocorreu um erro ao carregar subcategorias. + Imagem adicionada a %1$s na wiki Wikidata! + Falha ao atualizar a entidade Wikidata correspondente! + Definir imagem de fundo + Imagem de fundo definida! + Tem certeza de que deseja apagar o histórico de pesquisa? + Histórico de pesquisa excluído + A sessão expirou. Inicie uma nova sessão, por favor. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index b0717f3dc..1ace955ef 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -5,6 +5,7 @@ * Giro720 * Hamilton Abreu * Imperadeiro98 +* Ldacosta * Lijealso * Luckas * Malafaya @@ -12,16 +13,19 @@ * Vitorvicentevalente --> + Explorar + Explorar Aparência Geral Comentários Localização - Wikimedia Commons + Commons Configurações - Nome de utilizador(a) + Carregar na wiki Commons + Nome de utilizador Palavra-passe - Entre com a sua conta do Commons Beta + Entre com a sua conta da wiki Commons Beta Iniciar sessão Esqueceu-se da palavra-passe? Registar-se @@ -30,7 +34,7 @@ Inicio de sessão bem sucedido O início de sessão falhou! Ficheiro não encontrado. Por favor, tente outro ficheiro. - Falha de autenticação! + A autenticação falhou, inicie uma nova sessão, por favor Iniciado o carregamento! %1$s enviado! Toque para ver seu carregamento @@ -58,8 +62,7 @@ Forneça um título para este ficheiro, por favor Descrição Não foi possível iniciar sessão - falha de rede - Não foi possível iniciar sessão - verifique o seu nome de utilizador(a) - Não foi possível iniciar sessão - verifique a sua palavra-passe + Não foi possível iniciar sessão - verifique o seu nome de utilizador e a palavra-passe Demasiadas tentativas malsucedidas. Por favor, tente de novo dentro de alguns minutos. Desculpe, este utilizador foi bloqueado no Commons Precisa fornecer o seu código de ativação de dois fatores. @@ -89,13 +92,14 @@ %1$d carregamentos Nenhuma categoria correspondente %1$s encontrada - Adicione categorias para tornar as suas imagens mais fáceis de encontrar no Wikimedia Commons.\nComece a digitar para adicionar categorias. + Adicione categorias para tornar as suas imagens mais fáceis de encontrar na wiki Wikimedia Commons.\nComece a escrever para adicionar categorias. Categorias Configurações Registar-se Imagens destacadas + Categoria Sobre - A aplicação do Wikimedia Commons é uma aplicação de código aberto criada e mantida por bolseiros e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção da aplicação. + A aplicação da wiki Wikimedia Commons é uma aplicação de código aberto criada e mantida por bolseiros e voluntários da comunidade Wikimedia. A Wikimedia Foundation não está envolvida na criação, desenvolvimento ou manutenção da aplicação. Criar uma nova <a href=\"https://github.com/commons-app/apps-android-commons/issues\">incidência no GitHub</a> para reportar erros e sugestões. <u>Política de privacidade</u> <u>Créditos</u> @@ -108,17 +112,17 @@ Tente novamente Cancelar Essa imagem será licenciada sob %1$s - Ao carregar esta imagem, declaro que esta é a minha própria obra, que não contém material protegido ou selfies, e que adere às <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/pt\">políticas do Wikimedia Commons</a>. + Ao carregar esta imagem, declaro que esta é uma obra própria, que não contém material protegido nem selfies, e que adere às <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines/pt\">normas da wiki Wikimedia Commons</a>. Descarregar Licença padrão Usar título/descrição anteriores Obter automaticamente a localização atual - Recuperar localização atual para oferecer sugestões da categoria se a imagem não é georreferenciada + Se a imagem não estiver georreferenciada, obtém a localização atual e georreferencia a imagem com ela. Aviso: isto revela a sua localização corrente. Modo noturno Utilizar tema escuro Atribuição-CompartilhaIgual 4.0 Atribuição 4.0 - Atribuição – Compartilhamento pela mesma Licença + Atribuição–CompartilhaIgual 3.0 Atribuição 3.0 CC0 CC BY-SA 3.0 @@ -136,7 +140,7 @@ CC-BY-SA 4.0 CC BY 4.0 CC Zero - Wikimedia Commons armazena a maioria das imagens que são usadas na Wikipédia. + A wiki Wikimedia Commons aloja a maioria das imagens que são usadas na Wikipédia. As suas imagens ajudam a educar pessoas em todo o mundo! Por favor, carregue apenas imagens tiradas ou criadas exclusivamente por si: Objetos naturais (flores, animais, montanhas)\n• Objetos úteis (bicicletas, estações de comboio)\n• Pessoas famosas (o seu presidente da câmara, atletas olímpicos que conheça) @@ -173,7 +177,7 @@ Locais Próximos Não foram encontrados locais próximos. Aviso - Este ficheiro já existe no Commons. Tem a certeza de que deseja continuar? + Este ficheiro já existe na wiki Commons. Tem a certeza de que deseja continuar? Sim Não Título @@ -195,13 +199,15 @@ Definir o limite de carregamentos recentes Atualmente, a autenticação de dois fatores não é suportada. Deseja realmente sair? - Logótipo do Commons - Sítio do Commons - Página do Commons no Facebook - Código-fonte do Commons no Github + Logótipo da wiki Commons + Sítio da wiki Commons + Página da wiki Commons no Facebook + Código-fonte da wiki Commons no Github Imagem de fundo Falha na imagem de média Nenhuma imagem encontrada + Não foi encontrada nenhuma subcategoria. + Não foi encontrada nenhuma categoria mãe Carregar imagem Monte Zao Lamas @@ -222,12 +228,12 @@ Configurações Comentários Sair - Tutorial + Explicação Notificações Destacadas Os sítios aqui perto não podem ser apresentados sem permissões de localização não foi encontrada nenhuma descrição - Página do ficheiro no Commons + Página do ficheiro na wiki Commons Item do Wikidata Artigo na Wikipédia Erro ao colocar imagens na cache @@ -274,4 +280,32 @@ Avançar Cancelar Tentar novamente + Entendido! + Estes são os lugares perto de si que precisam de fotografias para ilustrar os respetivos artigos na Wikipédia + Tocar neste botão fará surgir uma lista destes lugares + Pode carregar uma fotografia para qualquer dos lugares, da sua galeria ou câmara + Não foi encontrada nenhuma imagem! + Ocorreu um erro durante o carregamento das imagens. + Carregada por: %1$s + Está impedido de editar a wiki Commons + Partilhar aplicação + Não foram especificadas coordenadas durante a seleção da imagem + Erro ao localizar locais próximos. + Imagem do Dia + Imagem do Dia + Pesquisar + Pesquisar a wiki Commons + Não foi encontrada nenhuma imagem que corresponda a %1$s + Pesquisar + Pesquisas recentes: + Consultas pesquisadas recentemente + Ocorreu um erro ao carregar categorias. + Ocorreu um erro ao carregar subcategorias. + Imagem adicionada a %1$s na wiki Wikidata! + Falha ao atualizar a entidade Wikidata correspondente! + Definir imagem de fundo + Imagem de fundo definida! + Tem a certeza de que deseja limpar o historial de pesquisas? + O historial de pesquisas foi eliminado + A sessão expirou. Inicie uma nova sessão, por favor. diff --git a/app/src/main/res/values-qq/strings.xml b/app/src/main/res/values-qq/strings.xml index 37c2978bf..1ffc944ad 100644 --- a/app/src/main/res/values-qq/strings.xml +++ b/app/src/main/res/values-qq/strings.xml @@ -3,6 +3,7 @@ * Amire80 * Liuxinyu970226 * Minh Nguyen +* Nemo bis * Robby * Shirayuki * Siebrand @@ -50,8 +51,6 @@ {{Identical|Title}} {{Identical|Description}} Error message shown to user when login can not be completed due to network issues. - Error message shown to user when login can not be completed because the user name is wrong. - Error message shown to user when login can not be completed beause the password is wrong Error message shown to user when login can not be completed because the user has attempted to login too many times in a short period of time, and hence been throttled. Error message shown to user when login can not be completed because the user is blocked on Wikimedia Commons {{Identical|Login failed}} @@ -99,6 +98,7 @@ Message explaining what kind of images not to submit. Message asking user if they understand what kinds of images to upload. Button text for confirming the user understands what kinds of images to upload.\n{{Identical|Yes}} + \'\'This message is empty, and it\'s probably invalid. See bug report: https://github.com/commons-app/apps-android-commons/issues/1333 .\'\' Label for categories list in media detail panel.\n{{Identical|Category}} Placeholder for categories list in media detail panel, while loading from network.\n{{Identical|Loading}} Placeholder for categories list in media detail panel, if no categories found.\n{{Identical|None selected}} @@ -137,4 +137,5 @@ {{Identical|Language}} {{Identical|Cancel}} {{Identical|Retry}} + Message shown in a dialog (\"success toast\") after a contribution by the user.\n\nParameter:\n* %1$s - title of the target page on Wikidata diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index b87abfbbf..ed80bd81f 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -16,7 +16,7 @@ Autentificare reușită! Autentificare nereușită! Fișierul nu a fost găsit. Încercați cu un alt fișier. - Certificare nereușită! + Certificare nereușită! Încărcarea a început! %1$s s-a încărcat! Atingeți pentru a vizualiza elementul încărcat @@ -44,8 +44,6 @@ Titlu Descriere Autentificare nereușită – defecțiune de rețea - Autentificare nereușită – verificați-vă numele de utilizator - Autentificare nereușită – verificați-vă parola Prea multe încercări nereușite. Încercați din nou peste câteva minute. Ne pare rău, acest utilizator a fost blocat la Commons Trebuie să introduceți tokenul de autentificare. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 4dc90489d..f31642902 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -15,10 +15,13 @@ * Okras * Redredsonia * Rubin16 +* Vlad5250 * Wirbel78 * ОйЛ --> + Обзор + Обзор Внешний вид Общие Обратная связь @@ -26,18 +29,19 @@ Викисклад Настройки + Загрузить на Викисклад Имя пользователя Пароль Войдите в свою учётную запись Commons Beta Войти Забыли пароль? - Зарегистрироваться + Регистрация Вход в систему Пожалуйста, подождите… Вход выполнен успешно! Ошибка входа в систему! Файл не найден. Попробуйте другой файл. - Ошибка аутентификации! + Ошибка аутентификации. Пожалуйста, авторизуйтесь ещё раз. Загрузка началась! %1$s загружен! Нажмите для просмотра загруженного файла @@ -46,11 +50,7 @@ Завершение загрузки %1$s Загрузка %1$s не удалась Нажмите для просмотра - - %1$d файл загружается - %1$d файла загружается - %1$d файлов загружается - + %1$d {{PLURAL:%1$d|one=файл загружается|few=файла загружается|файлов загружается}} Мои недавние загрузки В очереди Ошибка загрузки. @@ -66,8 +66,7 @@ Пожалуйста, укажите название этого файла Описание Не удаётся войти — сбой сети - Не удалось войти — пожалуйста, проверьте своё имя пользователя - Не удалось войти — пожалуйста, проверьте свой пароль + Не удаётся войти — проверьте ваше имя пользователя и пароль Слишком много неудачных попыток. Пожалуйста, попробуйте ещё раз через несколько минут. Извините, но участник с таким именем был заблокирован на Викискладе Вы должны ввести код двухфакторной аутентификации. @@ -99,12 +98,13 @@ %1$d загрузки %1$d загрузок - Категории, соответствующие %1$s, не найдены + Не найдено категорий, соответствующих %1$s Добавьте категории, чтобы ваши изображения можно было легко найти на Викискладе.\nНачните вводить название для добавления категорий. Категории Настройки - Зарегистрироваться + Регистрация Избранные изображения + Категория О приложении Приложение «Викисклад» - это программа с открытым кодом, которую создали волонтёры и участники грантов Викимедиа. Фонд Викимедиа не участвует в создании, разработке или обслуживании этого приложения. Вы можете создать <a href=\"https://github.com/commons-app/apps-android-commons/issues\">запрос на GitHub</a>, чтобы сообщить об ошибке или внести предложение. @@ -115,7 +115,7 @@ Почтовый клиент не установлен Недавно использованные категории Ожидание первой синхронизации… - Вы ещё не загрузили ни одной фотографии. + Вы ещё не загрузили ни одного изображения. Повторить Отмена Это изображение будет лицензировано под %1$s @@ -123,8 +123,8 @@ Скачать Лицензия по умолчанию Использовать предыдущие название/описание - Автоматически получить текущее местоположение - Получить текущее местоположение, чтобы были предложены категории, если изображение не содержит геотегов + Анализ местоположения + Если изображение не содержит геотега, то будет получено и проставлено текущее местоположение. Будьте внимательны, если вы не хотите раскрывать ваше месторасположение. Ночной режим Использовать тёмную тему Attribution-ShareAlike 4.0 @@ -148,7 +148,7 @@ CC BY 4.0 CC Zero Викисклад содержит бо́льшую часть изображений, которые используются в Википедии. - Ваши изображения помогают образованию людей во всём мире! + Ваши изображения могут помочь образованию людей во всём мире! Пожалуйста, загрузите фотографии, которые были сняты или созданы исключительно вами: Природные объекты (например, цветы, животные, горы)\n• Полезные предметы (например, велосипеды, вокзалы)\n• Известные люди (например, ваш мэр, спортсмены-олимпийцы, которых вы встретили) Природные объекты (например, цветы, животные, горы) @@ -166,7 +166,7 @@ Категории: Sydney Opera House from the west, Sydney Opera House remote views Загрузите свои изображения. Помогите Википедии оживить статьи! Изображения в Википедии хранятся на Викискладе. - Ваши изображения помогают образованию людей во всём мире. + Ваши изображения могут помочь образованию людей во всём мире. Избегайте материалов, защищённых авторским правом, например, найденных в Интернете, изображений плакатов, книжных обложек и т.п. Вам это понятно? Да! @@ -207,12 +207,14 @@ Двухфакторная аутентификация в настоящее время не поддерживается. Вы действительно хотите выйти? Логотип Викисклада - Веб-сайт Commons - Facebook-страница Commons - Исходные коды Commons на гитхабе + Веб-сайт Викисклада + Facebook-страница Викисклада + Исходные коды Викисклада на гитхабе Фоновое изображение - Ошибка медиаизображения + Ошибка медиафайла Изображение не найдено + Подкатегории не найдены. + Не найдено родительских категорий Загрузить изображение Гора Зао Ламы @@ -246,7 +248,7 @@ Пожалуйста, подробно опишите загружаемый файл: где он был снят? что на нём изображено? каков его контекст? Пожалуйста опишите изображённых персон или объекты. Добавьте информацию, о которой нельзя легко догадаться, например, время суток, когда снимался файл. Если снято что-то необычное, постарайтесь пояснить, что именно в этом необычного. Это изображение слишком тёмное. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность. Это изображение размыто. Вы уверены, что хотите его загрузить? Викисклад подходит только для фотографий, имеющих энциклопедическую ценность. - Дать разрешение + Разрешить Использовать внешнее хранилище Сохранять изображения, сделанные с помощью встроенной камеры на устройстве Войдите в свою учётную запись @@ -282,7 +284,35 @@ <u>Перевести</u> Языки Выберите язык локализации, на который сможете перевести элементы интерфейса приложения - Выполняется + Перейти к переводу Отмена Повторить + Понятно! + Это места поблизости, статьи о которых нуждаются в иллюстрациях + Нажатие этой кнопки сгенерирует список таких мест + Вы можете загрузить изображение для любого из этих мест, сделав снимок камерой или выбрав уже существующее изображение из галереи + Изображений не найдено! + Произошла ошибка при загрузке изображений. + Загружено участником %1$s + Редактирование на Викискладе вам было заблокировано + Поделиться приложением + Во время выбора изображения не были указаны координаты + Ошибка получения мест поблизости + Изображение дня + Изображение дня + Поиск + Найти на Викискладе + Не найдено изображений, соответствующих %1$s + Найти + Последние поиски: + Последние запросы + При загрузке категорий произошла ошибка. + При загрузке подкатегорий произошла ошибка. + Изображение успешно добавлено на страницу викиданных! + Не удалось обновить соответствующую страницу викиданных! + Сделать фоновой заставкой + Фоновая заставка успешно установлена! + Вы уверены, что хотите очистить историю поисковых запросов? + История поиска очищена + Сессия авторизации истекла, пожалуйста авторизуйтесь заново. diff --git a/app/src/main/res/values-sd/strings.xml b/app/src/main/res/values-sd/strings.xml index 3ccc39dd3..14d551ee9 100644 --- a/app/src/main/res/values-sd/strings.xml +++ b/app/src/main/res/values-sd/strings.xml @@ -2,20 +2,31 @@ + تلاش ڪريو + تلاش ڪريو + حليو + عام + پذيرائي + جڳهه العام + ترتيبون + ڪامنز ۾ چاڙهيو واپرائيندڙ-نانءُ ڳجھولفظ + وڪي ڪامنز جي آزمائشي کاتي ۾ داخل ٿيو داخل ٿيو + پاسورڊ وساري ويٺا آهيو؟ کاتو کوليو داخل ٿيندي براءِ مھرباني انتظار ڪريو… داخل ٿيڻ ڪامياب! داخل ٿيڻ ناڪام! فائيل نہ لڌو. براءِ مھرباني ٻيو ڪو فائيل آزمايو. - اٿينٽيڪيشن ناڪام! + تصديق ناڪام! ٻيهر داخل ٿيو چاڙھ شروع! %1$s چڙھي چڪا! پنھنجو چاڙھ ڏسڻ لاءِ ٺونگو ھڻو @@ -40,12 +51,13 @@ ونڊيو جھانگوءَ ۾ ڏسو عنوان + هن فائيل لاءِ ڪا سُرخي ڏيو تشريح ناقابلِ داخل ٿيڻ - باھمڄار ناڪامي - ناقابلِ داخل ٿيڻ - براءِ مھرباني پنھنجو واپرائيندڙ-نانءُ چڪاسيو - ناقابل داخل ٿيڻ - براءِ مھرباني پنھنجو ڳجھولفظ چڪاسيو + داخل نه ٿيا آهيو - مهرباني ڪري پاسورڊ ۽ کاتي جو نالو چيڪ ڪيو ھيڪانديون ناڪام ڪوششون. براءِ مھرباني ڪجھ منٽن کانپوءِ ٻيھر ڪوشش ڪريو. افسوس، ھي واپرائيندڙ العام تي بندشيل آھي + اوهان هر صورت ۾ پنهنجو ٻن عنصرن وارو تصديقي ڪوڊ ڏيو. داخل ٿيڻ ناڪام چاڙھيو ھن سيٽ کي نالو ڏيو @@ -54,14 +66,16 @@ زمرا ڳوليو سانڍيو تازو ڪيو + فهرست + اوهان جي ڊوائيس ۾ جي پي ايس بند آهي. ڇا اوهان ان کي کولڻ چاهيو ٿا؟ جي پي ايس چالو ڪيو (اين ايبل جي پي ايس) اڃان تائين ڪو به ڄاڙهه (اَپلوڊ) نه ٿيو آهي - + \@string/contributions_subtitle_zero %1$d چاڙھ %1$d چاڙھَ - + چاڙھ %1$d شروع ڪندي چاڙھَ %d$1 شروع ڪندي @@ -74,8 +88,10 @@ زمرا ترتيبون کاتو کوليو + چونڊ تصويرون + زمرو بابت - <a href=\"https://github.com/commons-app/apps-android-commons/wiki/Privacy-policy\">ذاتيات پاليسي</a> + <u>ذاتيات پاليسي</u> بابت پذيرائي موڪليو (برقٽپال ذريعي) ڪوبہ برقٽپال ڪلائينٽ تنصيبيل ناھي @@ -86,10 +102,10 @@ رد ھن عڪس کي %1$s جي تحت لائسنس ٿيندو لاھيو - لائسنس + رٿيل لائسنس گذريل عنوان/تشريح استعمال ڪريو خوبخود ھاڻوڪي مڪانيت وٺو - ھاڻوڪي مڪانيت لھو زمرن جون تجويزون پيش ڪرڻ لاءِ جيڪڏھن عڪس تي جيوٽيگ ناھي لڳل + ھاڻوڪي مڪانيت ڏئي ٿو جيڪڏھن عڪس جيوٽيگ ناھي ٿيل، ۽ عڪس کي ان سان جيوٽيگ ڪري ٿو. چتاءُ: ھي توھان جي ھاڻوڪي مڪانيت ظاھر ڪندو. رات جو ڏيک گھرو نظارو استعمال ڪريو انتساب-ھجھڙي ڀاڱيداري 4.0 @@ -115,11 +131,17 @@ وڪيپيڊيا تي استعمال ٿيندڙ گھڻن عڪسن جي وڪيميڊيا العام ميزباني ڪري ٿو. توھان جا عڪس سڄي دنيا جي ماڻھن کي تعليم يافتا ڪرڻ ۾ مدد ڪن ٿا براءِ مھرباني اھي تصويرون چاڙھيو مڪمل طور تي توھان پاران ڪڍيل يا تخليقيل آھن: - u2022 قدرتي شيون (گل، جانور، جبل) \nu2022 استعمال جوڳيون شيون (سائيڪلون، ٽرين اسٽيشنون) \nu2022 مشھور شخصيتون (توھان جو ناظم، اولمپڪ رانديگر جنھن سان توھان مليئو) + قدرتي شيون (گل، جانور، جبل) \nاستعمال جوڳيون شيون (سائيڪلون، ريل اسٽيشنون) \nمشھور شخصيتون (توھان جو ناظم، اولمپڪ رانديگر جنھن سان توھان مليو) + قدرتي شيون (گل، جانور، جبل) + استعمال جوڳيون شيون (سائيڪلون، ريل اسٽيشنون) + مشھور شخصيتون (توھان جو ناظم، اولمپڪ رانديگر جنھن سان توھان مليو) براءِ مھرباني نہ چاڙھيو: u2022 سيلفيون يا پنھنجي دوستن جو تصويرون \nu2022 اھي تصويرون جيڪي توھان انٽرنيٽ تان ڊائونلوڊ ڪيون \nu2022 پروپرائيٽري ايپس جا اسڪرين شاٽ + پاڻفي يا اوهان جي دوستن جون تصويرون + انٽرنيٽ تان کنيل تصويرون مثال چاڙھ: - عنوان: سڊني اوپيرا گھر \n- تشريح: سڊني اوپيرا گھر نھر جي پاسي کان ڏيک \n- زمرا: سڊني اوپيرا گھر، سڊني اوپيرا گھر اولھ کان، سڊني اوپيرا گھر ڏورانھان ڏيک + سُرخي: سڊني اوپيرا گھر پنھنجي عڪسن جي ڀاڱيداري ڪريو. وڪيپيڊيا ڪي مضمونن ۾ زندگي آڻيو! وڪيپيڊيا تي عڪس وڪيميڊيا العام تان اچن ٿا. توھان جا عڪس سڄي دنيا ۾ ماڻھن کي تعليم يافتا ڪرڻ ۾ مدد ڪن ٿا. @@ -132,10 +154,11 @@ ڪا تشريح ناھي اڻڄاتل لائسنس تازو ڪريو - گھربل اجازت: خارجي اسٽوريج پڙھڻ. ايپ ھن کانسواءِ فنڪشن نٿي ڪري سگھي. + گھربل اجازت: خارجي اسٽوريج پڙهو. ايپ ھن کانسواءِ تصوير گيليري نٿو ڏسي سگھي. چونڊ اجازت: زمرن جي تجويزن لاءِ ھاڻوڪي مڪانيت وٺو ٺيڪ ويجھڙائيءَ ۾ جڳھون + آپاس وارو جڳهه نه لڌو چتاءُ فائل اڳ ئي العام ۾ موجود آھي. ڇا توھان کي پڪ آھي تہ توھان اڳتي وڌڻ ٿا چاھيو؟ ها @@ -143,8 +166,10 @@ عنوان ابلاغ جو عنوان تشريح + ليکڪ چاڙھيل تاريخ لائسنس (اجازت نامو) + مڪانيت آزمائشي آزمائيندڙ ٿيو منهنجي تازي چاڙهڻ (اَپلوڊ) جي حد (لِمٽ) وڌ ۾ وڌ حد (ميگزيمم لِمٽ) @@ -172,19 +197,58 @@ اوهان جي راءِ ٻاهر نڪرو سکيا (ٽيوٽوريل) + نوٽيفيڪيشنس + چونڊ ڪيل ويجھيو جڳھون بغير مڪانيت اجازت جي نٿيون ڏيکاري سگھجن ڪا به وضاحت نه ملي ڪامن فائيل جو ورق وڪيڊيٽا جزو (وڪيڊيٽا آئيٽم) + وڪيپيڊيا مضمون اجازت ڏيو ٻاھري سنڀار استعمال ڪريو ايپ ۾ ڪئمرا سان ڪڍيل تصويرون پنھنجي ڊوائيس تي سانڍيو پنھنجي کاتي ۾ داخل ٿيو لاگ فائيل موڪليو لاگ فائيل سرجڻھارن کي برقٽپال ذريعي موڪليو + خلل: يو آر ايل نه لڌو + هن تصوير کي ڊاهڻ لاءِ مقرر ڪيو ويو آهي. + برائوزر ۾ ڏسو مڪانيت تبديلي ناھي ٿي. مڪانيت موجود ناھي. ويجھين جڳھن جي فھرست ڏيکارڻ لاءِ اجازت گھربل آھي ھدايتون وٺو مضمون پڙھو + وڪيپيڊيا ڪامنز ۾ ڀليڪار، %1$s! اسان کي خوشي آهي ته اوهان هتي آهيو + %1$s اوهان جي بحث واري صفحي تي پيغام ڇڏيو آهي. + سنوارڻ لاءِ مهرباني + %1$s اوهان جو %2$s تي ذڪر ڪيو آهي. + طرف + وڪيڊيٽا + وڪيپيڊيا + ڪامنز + <u>پذيرائي ڏيو</u> + <u>عام سوال</u> + سبق کي ڇڏيو + انٽرنيٽ ناهي + انٽرنيٽ آهي + <u>ترجمو</u> + ٻوليون + رد + ٻيهر ڪوشش ڪريو + سمجھي ويس! + ڪوبہ عڪس نہ لڌو! + تصويرون لوڊ ڪرڻ دوران چُڪ ٿي آهي. + پاران چاڙهيل: %1$s + ايپ ونڊيو + اڄ جي تصوير + اڄ جي تصوير + ڳوليو + ڪامنز ۾ ڳولا + ڳوليو + تازيون ڳولائون: + تازه ڳوليل مسودا + وال پيپر لڳايو + وال پيپر ڪاميابيءَ سان لڳي ويو! + ڳولا سوانح ڊاٿي وئي + لاگ اِن سيشن ايڪسپائير ٿي ويو آهي، مهرباني ڪري وري لاڳ اِن ٿيو. diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 4ae0db17c..6fb06002a 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -17,7 +17,7 @@ පිවිසුම සාර්ථකයි! පිවිසීම අසාර්ථකයි! ගොනුව හමු නොවිණි. තවත් ගොනුවක් උත්සාහ කරන්න. - සහතික කිරීම අසාර්ථකයි + සහතික කිරීම අසාර්ථකයි උඩුගතකිරීම ආරම්හවූවා! %1$s උඩුගතකලා! ඔබගේ උඩුගතකිරීම පෙන්වන්න තට්ටු කරන්න @@ -44,8 +44,6 @@ මාතෘකාව විස්තරය පිවිසීමට නොහැකිය-ජාලය ඇනහිටීමක් - පිවිසීමට නොහැකිය-කරුණාකර ඔබගේ පිවිසුම්-නාමය පරික්ෂා කරන්න - පිවිසීමට නොහැකිය-කරුණාකර ඔබගේ මුරපදය පරික්ෂා කරන්න. බොහෝ අසාර්ථක උත්සාහයන් කර ඇත. කරුණාකර මිනිත්තු කිහිපයකට පසුව උත්සාහ කරන්න කණගාටුයි,මෙම පරිශීලක වාරණයට ලක්කර ඇත. ඔබ ඔබගේ ද්විත්ව සහතික කිරීමේ කේතය ඇතුලත් කළ යුතුය. diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 90fac7f16..ae91a8d25 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -23,7 +23,7 @@ Prihlásenie úspešné Prihlásenie zlyhalo! Súbor nebol nájdený. Skúste, prosím, iný súbor. - Overenie zlyhalo! + Overenie zlyhalo! Nahrávanie začalo! %1$s je nahraný! Kliknutím zobrazíte váš upload @@ -51,8 +51,6 @@ Prosím, dajte tomuto súboru názov Opis prihlásenie zlyhalo - zlyhanie siete - Prihlásenie zlyhalo - skontrolujte vaše používateľské meno - Prihlásenie zlyhalo - skontrolujte vaše heslo Príliš veľa neúspešných pokusov. Skúste to znova o niekoľko minút. Ospravedlňujeme sa, tento užívateľ bol na Commons zablokovaný Prihlásenie zlyhalo diff --git a/app/src/main/res/values-skr/strings.xml b/app/src/main/res/values-skr/strings.xml index 796ae8555..7d446473a 100644 --- a/app/src/main/res/values-skr/strings.xml +++ b/app/src/main/res/values-skr/strings.xml @@ -20,7 +20,7 @@ لاگ ان کامیاب! لاگ ان ناکام! فائل کائنی لبھی،ٻئی فائل کیتے کوشش کرو۔ - تصدیق ناکام! + تصدیق ناکام، ولدا لاڳ ان تھیوو اپ لوڈ شروع! %1$s اپ لوڈ تھی ڳیا! آپݨی اپلوڈ ݙیکھݨ کیتے ٹیپ کرو @@ -46,18 +46,18 @@ ایں سیٹ دا ناں ݙسو تبدیلیاں اپلوڈ - قسماں دی ڳول + ونکیاں دی ڳول بچاؤ سجرا، تازہ کرو فہرست جی پی ایس چلاؤ اڄݨ ککھ وی اپ لوڈ نی تھیا - قسماں، زمرے + ونکیاں ترتیباں سائن اپ تعارف تعارف - حالیہ ورتیاں ڳیاں قسماں + حالیہ ورتیاں ڳیاں ونکیاں ولدا کوشش کرو منسوخ ڈاؤن لوڈ ، لہاوݨ @@ -74,7 +74,7 @@ وکی پیڈیا تے فوٹو وکی میڈیا کامنز کنوں امدن۔ تہاݙے فوٹو پوری دنیا دے لوکاں کوں تعلیم ݙیوݨ کیتے مدد ݙیندن جیا! - قسماں، زمرے + ونکیاں لوڈ تھیدا پئے۔۔۔ کجھ نی چݨیا کوئی تفصیل کائنی @@ -129,4 +129,5 @@ اڳوں تے تھیوو منسوخ ولدا کوشش کرو + گھن گھندا diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 925b8a5ce..29a025bd8 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -29,7 +29,7 @@ Успешно сте пријављени. Пријављивање није успело. Датотека није пронађена. Покушајте са другом датотеком. - Провера идентитета није успела. + Провера идентитета није успела. Отпремање је започето. Датотека „%1$s“ је отпремљена. Додирните да бисте видели отпремање @@ -57,8 +57,7 @@ Унесите наслов за ову датотеку Опис Неуспешно пријављивање – грешка на мрежи - Неуспешно пријављивање – проверите Ваше корисничко име - Неуспешно пријављивање – проверите Вашу лозинку + Не могу да извршим пријаву — проверите своје корисничко име и лозинку Превише неуспешних покушаја. Пробајте поново за неколико минута. Нажалост, овај корисник је блокиран на Остави Морате унети Ваш двофакторски код за аутентификацију. @@ -70,6 +69,7 @@ Претражи категорије Сачувај Освежи + Списак GPS је онемогућен на Вашем уређају. Желите ли га омогућити? Омогући GPS Још увек нема отпремања @@ -91,6 +91,7 @@ Категорије Подешавања Отвори налог + Изабране слике О апликацији Софтвер отвореног кода доступан под лиценцом <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Apache вер. 2</a> Викимедијина Остава и њен лого су заштитни знаци Викимедијине Фондације и користе се са дозволом Викимедијине Фондацине. Ми не одобравамо или подржавмо Викимедијину Фондацију.\n\nАпликација за Викимедијину оставу је апликација отвореног кода која је направљена и која се одржава помоћу грантова и волонтера Викимедијине заједнице. Задужбина Викимедија није укључена у стварање, развој или одржавање апликације. Направите нови <a href=\"https://github.com/commons-app/apps-android-commons/issues\">захтев на GitHub-у</a> да бисте пријавили грешке или дали предлоге. @@ -110,7 +111,7 @@ Подразумевана лиценца Користи претходан наслов/опис Аутоматски детектуј тренутну локацију - Прими тренутну локацију да би предложили категорију ако слика није географски означена + Прима тренутну локацију ако слика није геотагована и геотагује исту њоме. Упозорење: Овиме ће да се открије Ваша тренутна локација. Ноћни режим Користити тамну тему Ауторство-Делити под истим условима 4.0 @@ -156,14 +157,15 @@ Избегавајте материјале које сте нашли на интернету, као и слике плаката, корица књига итд. Јесте ли разумели? Јесам! + Категорије Учитавање… Ништа није изабрано Нема описа Непозната лиценца Освежи - Потребна дозвола: читање спољашње меморије. \nАпликација не може да функционише без овога. - Потребна дозвола: писање у спољашњој меморији. \nАпликација не може да функционише без овога. + Потребна дозвола: Читање спољашње меморије. Апликација не може да приступи Ваших галерији без овога. + Потребна дозвола: Писање спољашње меморије. Апликација не може да приступи Вашој камери без овога. Необавезна дозвола: преузми тренутну локацију за предлоге категорија У реду Места у близини @@ -176,6 +178,8 @@ Наслов медија Опис Опис датотеке иде овде. Може да буде поприлично дуг и приказиваће се у више редова. Надамо се да ће изгледати лепо. + Аутор + Корисничко име аутора изабране слике иде овде. Датум отпремања Лиценца Координате @@ -218,10 +222,12 @@ Одјави ме Туторијал Обавештења + Изабрана Оближња места не могу да се приказују без дозволе за локацију опис није пронађен Страница датотеке на Остави Ставка на Википодацима + Википедијски чланак Грешка при кеширању слика Јединствен описни наслов за датотеку, који ће бити име датотеке. Можете да користите обични језик са размацима. Не треба уносити екстензију датотеке Молимо да опишете датотеку колико је то могуће: Где је направљена? Шта приказује? Шта је контекст? Опишите објекте и/или особе. Откријте информације које се не могу лако погодити, на пример доба дана ако је у питању пејзаж. Ако датотека приказује нешто необично, молимо да објасните шта је то чини необичном. @@ -235,6 +241,10 @@ Пошаљи дневничку датотеку девелоперима преко имејла Није пронађен веб-претраживач за отварање URL-а Грешка! URL није пронађен + Номиновање за брисање + Ова слика је била номинована за брисање + + Види у претраживачу Локација није промењена. Локација није доступна. Потребна је дозвола за приказивање листе локација у близини @@ -245,7 +255,35 @@ Хвала Вам за прављење измене %1$s Вас је поменуо на страници %2$s. Пребаци приказ + УПУТСТВА + ВИКИПОДАЦИ + ВИКИПЕДИЈА + ОСТАВА <u>Оцените нас</u> - Често постављана питања + <u>ЧПП</u> Прескочи туторијал + Интернет недоступан + Интернет доступан + Грешка при фечовању нотификација + Нису пронађене нотификације + <u>Превођење</u> + Језици + Одаберите језик за који бисте желели да правите преводе + Настави + Откажи + Покушај поново + Разумем! + Ово су места у Вашој близини којима је потребна слика да илуструје њихове википедијске чланке + Клик на ово дугме даће Вам списак ових места + Можете да отпремите слику за било које место из своје галерије или камере + Нису пронађене слике! + Десила се грешка при учитавању слика. + Отпремио/ла: %1$s + Подели апликацију + Координате нису биле одређене током селекције слике + Грешка при фечовању оближњих места. + Слика успешно додата у %1$s на Википодацима! + Неуспешно ажурирање одговарајућег ентитета на Википодацима! + Постави позадину + Позадина успешно постављена! diff --git a/app/src/main/res/values-su/strings.xml b/app/src/main/res/values-su/strings.xml index b2604eefd..cdb954d38 100644 --- a/app/src/main/res/values-su/strings.xml +++ b/app/src/main/res/values-su/strings.xml @@ -5,27 +5,34 @@ * Uchup19 --> + Pidangan + Umum + Eupan balik + Lokasi Commons + Séting Sandiasma Kecap sandi + Asup log kana akun Commons Beta anjeun Asup log + Poho Kecap Sandi? Daptar Asup log - Tungguan heula… - Asup log suksés! - Asup log Gagal! + Tungguan… + Laksana login! + Gagal login! Berkas teu kapanggih. Coba berkas séjén. - Oténtikasi gagal! - Ngamimitian ngunjal! + Oténtikasi gagal! mangga asup log deui + Mitembeyan ngunjal! %1$s diunjal! Toél pikeun némpo unjalan anjeun - Ngamimitian ngunjal %1$s + Ngamimitian %1$s ngunjal Ngunjal %1$s Méréskeun unjalan %1$s Ngunjal %1$s gagal Toél pikeun nempo - + ngunjal %1$d berkas ngunjal %1$d berkas @@ -43,8 +50,6 @@ Judul Pedaran Teu bisa login - gangguan jaringan - Teu bisa login - pariksa sandiasma - Teu bisa login - pariksa kecap sandi Loba teuing nu gagalna. Mangga cobian sababaraha menit deui mah Punten, ieu kontributor geus diblokir di Commons Anjeun kudu nyayagakeun kodeu oténtikasi dua faktor. @@ -77,6 +82,7 @@ Kategori Séting Daptar + Gambar petingan Ngeunaan Aplikasi Wikimédia Commons mangrupa aplikasi sumber nembrak nu dijieun jeung dikokolakeun ku panampa hibah sarta rélawan komunitas Wikimédia. Wikimedia Foundation teu pépérodeun dina nyieun, ngamekarkeun, atawa mulasara ieu aplikasi. <a href=\"https://github.com/commons-app/apps-android-commons\">Sumber</a> sarta <a href=\"https://commons-app.github.io/\">situ wéb</a> dina GitHub. Jieun anyar <a href=\"https://github.com/commons-app/apps-android-commons/issues\">perkara GitHub</a> pikeun saran jeung laporan kutu. @@ -93,10 +99,10 @@ Ieu gambar bakal ditangtayungan ku lisénsi %1$s Ku ngirimkeun ieu gambar, kuring ngedalkeun yén ieu mangrupa karya kuring sorangan, yén ieu karya teu ngandung matéri atawa hak pribadi nu ditangtayungan ku hak cipta, sarta mun teu nurut kana <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Kawijakan Wikimedia Commons</a>. Undeur - Lisénsi + Lisénsi buhun Paké baé judul/pedaran saméméhna Comot lokasi sacara otomatis - Catet lokasi ayeuna pikeun nawarkeun usulan kategori lamun gambar tanpa géotag + Catet lokasi ayeuna pikeun nawarkeun usulan kategori lamun gambar tanpa géotag Modus peuting Gunakeun téma cakueum Atribusi-BabagiSarupa 4.0 @@ -127,6 +133,7 @@ - Sélpi atawa poto sobat anjeun\n- Poto anu diundeur ti Internét\n- Poto layar aplikasi Conto unjalan: - Judul: Gedung Opera Sydney\n- Pedaran: Gedung Opera Sydney (Sydney Opera House) ditempo ti basisir peuntas\n- Kategori: Sydney Opera House, Sydney Opera House from the west, Sydney Opera House remote views + Judul: Rumah Opera Sydney Sumbangkeun gambar Anjeun. Bantuan ngahirupkeun artikel Wikipédia! Gambar di Wikipédia asalna ti Wikimedia Commons. Gambar-gambar anjeun ngabantu ngatik jalma di sakuliah dunya. @@ -153,6 +160,7 @@ Judul berkas Déskripsi Pedaran berkas nyangkaruk didieu. Bisa jadi matak ngalikakeun, sarta ngempladkeun kana sawatara baris. Sanajan kitu, muga bisa katémbong écés. + Pangarang Tanggal ngunjal Lisénsi Koordinat @@ -167,6 +175,9 @@ Oténtikasi dua faktor kiwari teu dirojong. Yakin anjeun rék kaluar? Logo Commons + Situs wéb Commons + Kaca Facebook Commons + Kode Sumber Github Commons Gambar Kasang Tukang Gambar Média Gagal Gambar Teu Kapanggih @@ -191,10 +202,13 @@ Eupan balik Kaluar Pituduh + Iber + Petingan Tempat sabudeureun teu bisa ditémbongkeun tanpa idin lokasi teu manggihan pedaran Kaca berkas commons item Wikidata + Artikel Wikipédia Kasalahan nalika muat gambar Judul déskriptif anu unik pikeun berkas, anu bakal miboga fungsi minangka ngaran berkas. Anjeun bisa maké basa basajan kalawan spasi. Ulah ngawuwuhkeun éksténsi berkas Pék émbarkeun wincikan média saloba-lobabana: Dimana éta dicokot? Naon nu titojokeunna? Naon kontéksna? Pék jéntrékeun obyék atawa jalmana. Ébré informasi anu teu gampang kajudi, kawas wayah mun éta mangrupa pamandangan. Ari média nu némbongkeun perkara nu teu guyub, pék jéntrékeun naon nu ngabalukarkeun éta téh teu guyub. diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 24ef60b61..b216afffe 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -27,7 +27,7 @@ Inloggningen lyckades! Det gick inte att logga in! Filen hittades inte. Försök med en annan fil. - Autentisering misslyckades! + Autentisering misslyckades, var god logga in igen! Överföring påbörjad! %1$s överförd! Tryck för att visa din uppladdning @@ -55,8 +55,7 @@ Var god ange en titel för denna fil Beskrivning Det gick inte att logga in - nätverksfel - Det gick inte att logga in - var god kontrollera ditt användarnamn - Det gick inte att logga in - var god kontrollera ditt lösenord + Kunde inte logga in - kontrollera ditt användarnamn och lösenord För många misslyckade försök. Var god försök igen om några minuter. Tyvärr, denna användare har blockerats på Commons Du måste ange din tvåstegsverifieringskod. @@ -90,9 +89,9 @@ Kategorier Inställningar Registrera - Utvalda bild + Utvalda bilder Om - Wikimedia Commons är en app med öppen källkod som skapas och underhålls av frivilliga från Wikimedias gemenskap. Wikimedia Foundation är inte involverad i skapandet, utvecklingen eller underhållet av appen. + Wikimedia Commons-appen är en app med öppen källkod som skapas och underhålls av frivilliga från Wikimedias gemenskap. Wikimedia Foundation är inte involverad i skapandet, utvecklingen eller underhållet av appen. Skapa ett nytt <a href=\"https://github.com/commons-app/apps-android-commons/issues\">ärende på GitHub</a> för att rapportera buggar och förslag. <u>Integritetspolicy</u> <u>Erkännande</u> @@ -105,12 +104,12 @@ Försök igen Avbryt Denna bild kommer att licensieras under %1$s - Genom att skicka in denna bild intygar jag att detta är mitt eget verk, som inte innehåller upphovsrättsskyddat material eller selfies samt annars följer <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Commons-policys</a>. + Genom att skicka in denna bild intygar jag att detta är mitt eget verk, som inte innehåller upphovsrättsskyddat material eller selfies samt följer <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">Wikimedia Commons-policys</a>. Ladda ned Standardlicens Använd föregående titel/beskrivning Hämta aktuell plats automatiskt - Hämta aktuell plats för att erbjuda kategoriförslag om bilden inte är geotaggad + Hämtar aktuell plats om bilden inte är geotaggad och geotaggar bilden med den. Varning: Detta kommer att avslöja din nuvarande position. Nattläge Använd mörkt tema Erkännande-DelaLika 4.0 @@ -135,7 +134,7 @@ CC Zero Wikimedia Commons lagrar de flesta bilderna som används på Wikipedia. Dina bilder hjälper till att utbilda\nmänniskor runt hela världen! - Ladda upp bilder som endast du har tagit eller skapat: + Ladda endast upp bilder som du har tagit eller skapat själv: Naturliga föremål (blommor, djur, berg)\n• Användbara föremål (cyklar, tågstationer)\n• Berömda personer (din borgmästare, olympiska atleter du har träffat) Naturliga föremål (blommor, djur, berg) Användbar föremål (cyklar, tågstationer) @@ -163,8 +162,8 @@ Ingen beskrivning Okänd licens Uppdatera - Nödvändig behörighet: Läsa extern lagring. Appen kan inte komma åt ditt galleri utan detta. - Nödvändig behörighet: Skriva till extern lagring. Appen kan inte komma åt din kamera utan detta. + Nödvändig behörighet: Läs extern lagring. Appen kan inte komma åt ditt galleri utan detta. + Nödvändig behörighet: Skriv till extern lagring. Appen kan inte komma åt din kamera utan detta. Valfri behörighet: Hämta aktuell plats för kategoriförslag OK Platser i närheten @@ -271,5 +270,21 @@ Fortsätt Avbryt Försök igen + Uppfattat! + Detta är platserna nära dig som behöver bilder för att illustrera deras Wikipedia-artiklar + Klicka på den här knappen för att få upp en lista med dessa platser + Du kan ladda upp en bild från vilken plats som helst från ditt galleri eller kamera + Inga bilder hittades! + Ett fel uppstod vid inläsning av bilder. + Uppladdad av: %1$s Dela app + Koordinater specificerades inte vid bildvalet + Fel uppstod när platser i närheten hämtades. + Dagens bild + Dagens bild + Bilden lades till i %1$s på Wikidata! + Misslyckades att uppdatera motsvarande Wikidataentitet! + Ange som bakgrundsbild + Bakgrundsbilden ändrades! + Inloggningssessionen löptes ut, var god logga in igen. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 1b611137d..c3dd661c7 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -1,6 +1,6 @@ - விக்கிமீடியா காமன்சு + காமன்சு அமைப்புகள் பயனர் பெயர் கடவுச்சொல் diff --git a/app/src/main/res/values-tcy/strings.xml b/app/src/main/res/values-tcy/strings.xml index c6469511e..3b540c75d 100644 --- a/app/src/main/res/values-tcy/strings.xml +++ b/app/src/main/res/values-tcy/strings.xml @@ -15,7 +15,7 @@ ಲಾಗಿನ್ ಅಂಡ್! ಲಾಗಿನ್ ಅಯಿಜಾತ್ತ! ಈ ಕಡತ ತಿಕ್ಕಿಜಿ. ದಯಮಲ್ತ್ ಕುಡೊಂಜಿ ಕಡತೊನು ಪ್ರಯತ್ನ ಮಲ್ಪುಲೆ. - ದೃಢೀಕರಣ ಸರಿ ಆಯಿಜಿ! + ದೃಢೀಕರಣ ಸರಿ ಆಯಿಜಿ! ದಿಂಜಪುನಾ ಸುರು ಅಂಡ್! %1$s ಅಪ್ಲೋಡ್ ಆಂಡ್! ಇರೆನ ಅಪ್ಲೋಡ್ ತೂಯೆರೆ ಒತ್ತುಲೆ diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml index b2ad478b0..c8d05f346 100644 --- a/app/src/main/res/values-te/strings.xml +++ b/app/src/main/res/values-te/strings.xml @@ -5,17 +5,23 @@ * Veeven --> + రూపురేఖలు + సాధారణం + ప్రతిస్పందన + ప్రాంతం కామన్స్ + అమరికలు వాడుకరిపేరు సంకేతపదం లాగినవండి + సంకేతపదం మర్చిపోయారా? నమోదవ్వండి లాగినవుతున్నారు వేచివుండండి… లాగిన్ విజయవంతమైంది! లాగిన్ విఫలమైంది! - ఆథెంటికేషను విఫలమైంది! + ఆథెంటికేషను విఫలమైంది! ఎక్కింపు మొదలైంది! %1$s ను ఎక్కించాం! మీ ఎక్కింపును చూసేందుకు నొక్కండి @@ -24,7 +30,7 @@ %1$s ఎక్కింపు పూర్తికావస్తోంది %1$s ఎక్కింపు విఫలమైంది చూసేందుకు నొక్కండి - నా ఎక్కింపులు + ఇటీవలి నా ఎక్కింపులు క్యూలో ఉంది విఫలమైంది %1$d%% పూర్తయింది @@ -38,8 +44,6 @@ శీర్షిక వివరణ లాగిన్ చెయ్యలేకపోయాం - నెట్‍వర్కు విఫలం - లాగిన్ చెయ్యలేకపోయాం - మీ వాడుకరిపేరును సరిచూసుకోండి - లాగిన్ చెయ్యలేకపోయాం - మీ సంకేతపదాన్ని సరిచూసుకోండి మరీ ఎక్కువ విఫల యత్నాలు చేసారు. కొద్ది నిముషాలాగి ప్రయత్నించండి ఈ వాడుకరి కామన్స్ లో నిరోధించబడ్డారు, సారీ. లాగిన్ విఫలమైంది @@ -49,6 +53,7 @@ ఎక్కించు వర్గాల్లో వెతకండి భద్రపరచు + జాబితా %1$s తో సరిపోలే వర్గాలేమీ లేవు వికీమీడియా కామన్స్ లో వెతికేటపుడు మీ బొమ్మలు మరింత సులువుగా కనబడేందుకు వాటికి వర్గాలను చేర్చండి.\n\nవర్గాలను చేర్చేందుకు టైపండి.\nఈ అంగను దాటేసి ముందుకు పోయేందుకు, ఈ సందేశాన్ని నొక్కండి (లేదా ’బ్యాక్’ నొక్కండి) వర్గాలు diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 29b29d804..4b00eb215 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -23,7 +23,7 @@ การเข้าสู่ระบบสำเร็จแล้ว! การเข้าสู่ระบบล้มเหลว! ไม่พบไฟล์ กรุณาลองใช้ไฟล์อื่น - การตรวจสอบความถูกต้องล้มเหลว! + การตรวจสอบความถูกต้องล้มเหลว! เริ่มการอัปโหลดแล้ว! อัปโหลด %1$s แล้ว! แตะเพื่อดูการอัปโหลดของคุณ @@ -48,8 +48,6 @@ กรุณาระบุชืิ่อเรื่องของไฟล์นี้ คำอธิบาย ไม่สามารถเข้าสู่ระบบได้ - ความล้มเหลวของเครือข่าย - ไม่สามารถเข้าสู่ระบบได้ - กรุณาตรวจสอบชื่อผู้ใช้ของคุณ - ไม่สามารถเข้าสู่ระบบได้ - กรุณาตรวจสอบรหัสผ่านของคุณ จำนวนครั้งที่พยายามไม่สำเร็จมากเกินไป กรุณาลองอีกครั้งในอีกสักครู่ ขออภัย ผู้ใช้นี้ถูกบล็อกบนคอมมอนส์อยู่ คุณต้องระบุโค้ดการตรวจสอบความถูกต้องสองปัจจัยของคุณ @@ -98,7 +96,7 @@ สัญญาอนุญาตปริยาย ใช้ชื่อเรื่อง/คำอธิบายก่อนหน้านี้ รับข้อมูลตำแหน่งที่ตั้งปัจจุบันโดยอัตโนมัติ - ดึงข้อมูลตำแหน่งที่ตั้งปัจจุบันเพื่อรับข้อเสนอแนะเกี่ยวกับหมวดหมู่ถ้ารูปภาพไม่ได้ติดแท็กตำแหน่งที่ตั้งเอาไว้ + ดึงข้อมูลตำแหน่งที่ตั้งปัจจุบันเพื่อรับข้อเสนอแนะเกี่ยวกับหมวดหมู่ถ้ารูปภาพไม่ได้ติดแท็กตำแหน่งที่ตั้งเอาไว้ โหมดกลางคืน ใช้ธีมสีเข้ม Attribution-ShareAlike 4.0 diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 95548b662..15ecf2afe 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -4,8 +4,10 @@ * Emperyan * Hedda * Incelemeelemani +* Joseph * McAang * Neslihan Turan +* Rapsar * Sayginer * Trockya * VikipediBilgini @@ -29,7 +31,7 @@ Oturum açma başarılı! Oturum açma başarısız oldu! Dosya bulunamadı. Lütfen başka bir dosya deneyin. - Kimlik doğrulama başarısız oldu! + Kimlik doğrulama başarısız oldu, lütfen tekrar giriş yapın Yükleme başladı! %1$s yüklendi! Yüklemelerinizi görüntülemek için dokunun @@ -57,8 +59,7 @@ Lütfen bu dosya için bir başlık ekleyin Açıklama Oturum açılamıyor - ağ hatası - Oturum açılamıyor - lütfen kullanıcı adınızı kontrol edin - Oturum açılamıyor - lütfen parolanızı kontrol edin + Giriş yapılamıyor - lütfen kullanıcı adınızı ve şifrenizi kontrol edin Çok sayıda başarısız girişimde bulundunuz. Birkaç dakika sonra tekrar deneyin. Üzgünüz, bu kullanıcı Commons\'ta engellendi İki faktörlü kimlik doğrulama kodunu sağlamalısınız. @@ -112,7 +113,7 @@ Varsayılan lisans Önceki başlığı/açıklamayı kullan Otomatik olarak mevcut konumu al - Resim koordinat olarak etiketlendirilmemişse kategori önerileri için mevcut konum bulun + Resim coğrafi etiketli değilse ve coğrafi etiketler resimle görüntüleniyorsa geçerli konumu alır. Uyarı: Mevcut konumunuzu gösterir. Gece modu Koyu temayı kullanın Attribution-ShareAlike 4.0 @@ -158,6 +159,7 @@ Telif hakkı olan ve internette bulunan film afişi, kitap kapağı gibi malzemelerin kullanımından kaçının. Bunu anladınız mı? Evet! + * Kategoriler Yükleniyor... Hiçbir şey seçilmedi @@ -241,7 +243,7 @@ Kayıt dosyasını, e-posta aracılığıyla geliştiricilere gönderin URL\'yi açabilecek bir tarayıcı bulunamadı Hata! URL bulunamadı - Silinmesi için aday göster + Silinmeye aday göster Bu görsel silinmesi için aday gösterildi. Tarayıcıda görüntüle Konum değiştirilmedi @@ -271,4 +273,21 @@ İlerle Vazgeç Tekrar Deneyin + Anladım! + Vikipedi maddelerine eklemek için fotoğrafa ihtiyaç duyan size yakın yerler + Bu tuşa dokunmak bu yerlerin bir listesini getirir + Galerinizden veya kameranızla herhangi bir yer için resim yükleyebilirsiniz. + Resim bulunamadı! + Resimler yüklenirken hata oluştu. + Yükleyen: %1$s + Uygulamayı Paylaş + Koordinatlar görüntü seçimi sırasında belirlenmedi + Yakındaki yerler alınırken hata oluştu. + Günün Resmi + Günün Resmi + Resim, Vikiveri\'de %1$s içine başarıyla eklendi. + Karşılık gelen Vikiveri varlığı güncellenemedi! + Duvar kağıdı ayarla + Duvar kağıdı başarıyla ayarlandı! + Oturumun süresi doldu, lütfen tekrar giriş yapın. diff --git a/app/src/main/res/values-ug/strings.xml b/app/src/main/res/values-ug/strings.xml index 3c180fbf2..40342d1d1 100644 --- a/app/src/main/res/values-ug/strings.xml +++ b/app/src/main/res/values-ug/strings.xml @@ -16,7 +16,7 @@ تىزىمغا كىرىش مۇۋەپپەقىيەتلىك! تىزىمغا كىرەلمىدى! ھۆججەت تېپىلمىدى . سىناپ بېقىڭ ، باشقا ھۆججەتلەر . - سالاھىيەتنى ئىسپاتىنى تەكشۈرۈش مەغلۇپ بولدى ! + سالاھىيەتنى ئىسپاتىنى تەكشۈرۈش مەغلۇپ بولدى ! يۈكلەش باشلاندى! %1$s يۈكلەندى! چېكىپ كۆرۈپ بېقىڭ ، سىزنىڭ يۇقىرىغا يوللاش @@ -39,8 +39,6 @@ ماۋزۇ چۈشەندۈرۈش تىزىملىتىش - تور كاشىلىسى - تىزىملاشقا ئامالسىز - سىزنىڭ ئابونت نامىڭىزنى تەكشۈرۈپ بېقىڭ - تىزىملاشقا ئامالسىز مەخپىي نومۇرىڭىزنى تەكشۈرۈپ بېقىڭ مەغلۇپ بولغان قېتىم سانى بەك كۆپ . نەچچە مىنۇتتىن كېيىن قايتا سىناڭ . كەچۈرۈڭ، بۇ ئابونت ئاللىقاچان ئورتاق بەھرىمەن بولىدىغان بايلىق مەنبەسى دائىرىلىك سىز چوقۇم سىزنىڭ قوش ئامىل تەكشۈرۈش كودىنى تاپشۇرىسىز . diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a4ed20320..c4e9bbbbf 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -17,6 +17,7 @@ Вікісховище Налаштування + Завантажити до Вікісховища Ім\'я користувача Пароль Увійдіть до вашого облікового запису Commons Beta @@ -28,7 +29,7 @@ Ви успішно увійшли! Не вдалося увійти Файл не знайдено. Будь ласка, спробуйте інший файл. - Помилка автентифікації + Помилка автентифікації. Будь ласка, увійдіть у свій обліковий запис знову Завантаження розпочато! Завантажено %1$s! Торкніться, щоб переглянути Ваше завантаження @@ -58,8 +59,7 @@ Будь ласка, вкажіть назву цього файлу Опис Неможливо увійти — збій у мережі - Неможливо увійти — будь ласка, перевірте своє ім\'я користувача - Неможливо увійти — будь ласка, перевірте свій пароль + Неможливо увійти — будь ласка перевірте ім\'я користувача та пароль Надто багато невдалих спроб. Будь ласка, спробуйте знову через кілька хвилин. Вибачте, цього користувача було заблоковано на Вікісховищі Ви повинні надати код двофакторної автентифікації. @@ -100,6 +100,7 @@ Налаштування Зареєструватися Вибрані зображення + Категорія Про програму Додаток «Вікісховище» — це програма з відкритим кодом, яку створили отримувачі грантів та волонтери спільноти Вікімедіа. Фонд Вікімедіа не брав участі у створенні, розробці чи обслуговуванні цього додатка. Ви можете створити новий <a href=\"https://github.com/commons-app/apps-android-commons/issues\">запит на GitHub</a>, щоб повідомити про помилки, або висловити пропозиції. @@ -119,7 +120,7 @@ Стандартна ліцензія Використати попередню назву/опис Автоматично отримати поточне розташування - Отримати поточне розташування, щоб з\'явилися підказки категорій, якщо зображення не має геотегів + Якщо зображення не містить координат, то буде отримано і поставлено ваше поточне розташування. Будьте уважні, якщо ви не хочете розкривати ваше розташування. Нічний режим Використати темну тему Attribution-ShareAlike 4.0 @@ -150,7 +151,7 @@ Корисні об\'єкти (велосипеди, залізничні станції) Відомі люди (ваш мер, спортсмен-олімпієць, якого ви зустріли) Будь ласка, НЕ завантажуйте: - u2022 Селфі або фото своїх друзів \nu2022 Зображення, які Ви завантажили з інтернету \nu2022 Скріншоти патентованих програм + - Селфі або фото своїх друзів \n- Зображення, які Ви завантажили з інтернету \n- Знімки екрану пропрієтарних програм Селфі чи фото ваших друзів Зображення, які ви завантажили з інтернету Знімки екрану пропрієтарних програм @@ -162,9 +163,10 @@ Надсилайте Ваші зображення. Допоможіть оживити статті Вікіпедії! Зображення у Вікіпедії надходять з Вікісховища. Ваші зображення допомагають освіті людей у всьому світі. - Уникайте захищених авторським правом матеріалів, знайдених в Інтернеті, а також зображень плакатів, обкладинок книг і т. п. + Уникайте захищених авторським правом матеріалів, знайдених в Інтернеті, а також зображень плакатів, обкладинок книг, тощо. Ви це зрозуміли? Так! + Категорії Завантаження… Нічого не обрано @@ -186,7 +188,7 @@ Опис Сюди потрапляє опис медіафайлу. Він потенційно може бути досить довгим і розтягнутися на декілька рядків. Однак ми сподіваємось, що він виглядатиме гарно. Автор - Тут вказується ім\'я автора вибраного зображеня + Тут вказується ім\'я автора вибраного зображення Дата завантаження Ліцензія Координати @@ -202,11 +204,12 @@ Ви справді хочете вийти із системи? Логотип Вікісховища Веб-сайт Commons - Фейсбук сторінка Commons + Facebook-сторінка Commons Програмний код Commons на GitHub Фонове зображення Помилка медіазображення Не знайдено зображення + Підкатегорій не знайдено Завантажити зображення Гора Зао Лами @@ -237,15 +240,15 @@ Стаття Вікіпедії Помилка кешування зображень Унікальна описова назва файлу. Ви можете використовувати простий текст з пробілами. Не вказуйте розширення файлу - Будь ласка, докладно опишіть файл: де його було зроблено? що на ньому зображено? який контекст? Будь ласка, опишіть об\'єкти чи осіб. Додайте інформацію, яку не можна легко здогадатися, наприклад, пору доби для фотографії пейзажу. Якщо зображено щось незвичайне, постарайтеся пояснити, що робить його незвичайним. + Будь ласка, докладно опишіть файл: де його було зроблено? що на ньому зображено? який контекст? Будь ласка, опишіть об\'єкти чи осіб. Додайте інформацію, яку не можна легко здогадатися, наприклад, пору доби для фотографії пейзажу. Якщо зображено щось незвичайне, спробуйте пояснити, що робить його незвичайним. Це зображення надто темне. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність. Це зображення розмите. Ви упевнені, що хочете його завантажити? Вікісховище призначене лише для зображень, що мають енциклопедичну цінність. Надати дозвіл Використовувати зовнішнє сховище Зберігати зображення, виконані вбудованою камерою Вашого пристрою Увійдіть у свій обліковий запис - Надіслати лог-файл - Надіслати лог-файл розробникам електронною поштою + Надіслати файл журналу + Надіслати файл журналу розробникам електронною поштою Не знайдено браузера, щоб відкрити посилання Помилка! Посилання не знайдено Номінувати на вилучення @@ -257,9 +260,9 @@ Потрібний дозвіл для показу списку місць поблизу Показати на мапі у зовнішній програмі ЧИТАТИ СТАТТЮ - Вітаємо у Wikimedia Commons, %1$s! Раді вас бачити. + Вітаємо у Вікісховищі, %1$s! Раді вас бачити. %1$s залишив повідомлення на вашій сторінці обговорення - Дякуємо за правку + Дякуємо за редагування %1$s згадав вас на %2$s. Перемкнути режим перегляду НАПРЯМКИ @@ -279,4 +282,23 @@ Виконується Скасувати Повторити + Зрозуміло + Це місця поблизу, про які є статті Вікіпедії, але які потребують ілюстрацій + Натискання цієї кнопки згенерує список таких місць + Ви можете завантажити зображення для любого з цих місць, зробивши знімок камерою або вибравши зображення з галереї + Зображень не знайдено! + Сталася помилка при завантаженні зображень. + Завантажено: %1$s + Редагування у до Вікісховищі вам було заблоковано + Поділитися програмою + Під час вибору зображення не були вказані координати + Помилка отримання місць поблизу. + Зображення дня + Зображення дня + Недавні пошуки: + Зображення успішно додано до сторінки %1$s у Вікіданих! + Не вдалось оновити відповідну сторінку Вікіданих! + Поставити шпалерами екрану + Шпалери екрану виставлено успішно! + Сесія авторизації спливла. Будь ласка, авторизуйтесь знову. diff --git a/app/src/main/res/values-ur/strings.xml b/app/src/main/res/values-ur/strings.xml index 4fdd67514..b477245c8 100644 --- a/app/src/main/res/values-ur/strings.xml +++ b/app/src/main/res/values-ur/strings.xml @@ -18,7 +18,7 @@ لاگ ان کامیاب۔ داخل ہونے میں ناکامی ہوئی! فائل نہیں ملی، براہ کرم دوسری فائل آزمائیں۔ - تصدیق ناکام! + تصدیق ناکام! اپلوڈ شروع! %1$s اپلوڈ شد! اپنی اپلوڈ دیکھنے کے لیے ٹیپ کریں۔ @@ -39,14 +39,12 @@ از نگار خانہ تصویر لیں قریبی - میری اپلوڈ + میرے اپلوڈ کردہ شیئر براؤزر میں کھولیں عنوان وضاحت لاگ ان ہونے میں ناکام - نیٹ ورک ناکامی - لاگ ان ہونے میں ناکام - براہ مہربانی اپنا صارف نام کی جانچ کریں - لاگ ان ہونے میں ناکام - براہ مہربانی - اپنے پاس ورڈ کی جانچ کریں بے شمار ناکام کوششیں کچھ منٹوں میں دوبارہ کوشش کریں۔ معذرت، یہ صارف کومنز پر بلاک کردیا گیا ہے آپ کو اپنے دو عامل کے تصدیق کوڈ فراہم کرنا چاہیے۔ @@ -71,9 +69,9 @@ شروع %1$d اپلوڈ شروع %d$1 اپلوڈ - + %1$d اپلوڈ - %1$d اپلوڈ + %1$d اپلوڈز %1$s سے کوئی زمرہ جات میل نہیں کھاتے ویکیمیڈیا کامنز پر اپنی تصاویر کو قابل دریافت بنانے کے لیے زمرے شامل کریں۔\n\nزمرے شامل کرنے کے لیے لکھنا شروع کریں۔\n\nاس مرحلے کو نظر انداز کرنے کے لیے اس پیغام یا (یا پیچھے) پر ٹیپ کریں۔ @@ -99,7 +97,7 @@ اجازت نامہ گزشتہ عنوان/وضاحت استعمال کریں خودکارانہ طریقے سے حالیہ جگہ حاصل کریں - اگر تصویر جغرافیائی نہیں ہے تو قسم کے تجاویز پیش کرنے کیلئے موجودہ مقام کو دوبارہ حاصل کریں + اگر تصویر جغرافیائی نہیں ہے تو قسم کے تجاویز پیش کرنے کیلئے موجودہ مقام کو دوبارہ حاصل کریں نائٹ موڈ کالا تھیم استعمال کریں انتباہ-شراکت 4.0 @@ -132,6 +130,7 @@ آپ کی رائے لاگ آوٹ معلمی + %1$s، ویکیمیڈیا کامنز میں خوش آمدید! آپ کو یہاں پا کر ہمیں خوشی ہوئی۔ ہدایات عام انٹرنیٹ دستیاب نہیں diff --git a/app/src/main/res/values-v14/dimens.xml b/app/src/main/res/values-v14/dimens.xml new file mode 100644 index 000000000..4db8c5906 --- /dev/null +++ b/app/src/main/res/values-v14/dimens.xml @@ -0,0 +1,10 @@ + + + + + 0dp + + \ No newline at end of file diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 4cff15064..a45a6d7c1 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,23 +1,31 @@ + Giao diện + Tổng quát + Phản hồi + Vị trí Commons + Thiết lập Tên người dùng Mật khẩu + Đăng nhập vào tài khoản Commons Beta của bạn Đăng nhập + Quên mật khẩu? Mở tài khoản Đang đăng nhập Vui lòng chờ… Đã đăng nhập thành công! Đăng nhập thất bại! Không tìm thấy tập tin. Xin vui lòng thử tập tin khác. - Xác thực thất bại! + Xác thực thất bại! Đã bắt đầu tải lên! Đã tải lên %1$s! Chạm để xem những tập tin tải lên của bạn @@ -26,7 +34,10 @@ Đang hoàn thành việc tải lên tập tin %1$s Tải lên tập tin %1$s thất bại Chạm để xem - %d tập tin đang được tải lên + + %1$d tập tin đang được tải lên + %1$d tập tin đang được tải lên + Tập tin Tôi đã Tải lên Gần đây Đang chờ Thất bại @@ -39,10 +50,10 @@ Chia sẻ Mở trong Trình duyệt Tên + Xin hãy đặt tiêu đề cho tập tin này Miêu tả Không thể đăng nhập – có lỗi mạng - Không thể đăng nhập – xin vui lòng kiểm tra tên người dùng - Không thể đăng nhập – xin vui lòng kiểm tra mật khẩu + Không thể đăng nhập – vui lòng kiểm tra tên người dùng và mật khẩu của bạn Đã đăng nhập thất bại quá nhiều lần. Xin vui lòng thử lại trong vòng vài phút. Rất tiếc, người dùng này đã bị cấm tại Commons Bạn phải cung cấp mã xác thực dùng hai nhân tố. @@ -54,25 +65,31 @@ Tìm thể loại Lưu Làm mới + Danh sách Chức năng GPS đang tắt trên thiết bị của bạn. Bạn có muốn bật nó lên? Bật GPS Chưa có tập tin tải lên - + \@string/contributions_subtitle_zero - %1$d tập tin tải lên + %1$d tập tin đã tải lên + %1$d tập tin đã tải lên Đang bắt đầu tải lên %1$d tập tin - %1$d tập tin tải lên + + %1$d tập tin đã tải lên + %1$d tập tin đã tải lên + Không tìm thấy thể loại khớp với %1$s - Xếp các hình ảnh vào thể loại để cho chúng dễ tìm kiếm hơn trên Wikimedia Commons.\n\nHãy bắt đầu nhập tên thể loại để tìm kiếm.\nChạm vào thông điệp này (hoặc bấm Quay lại) để bỏ qua bước này. + Xếp các hình ảnh vào thể loại để giúp chúng dễ tìm kiếm hơn trên Wikimedia Commons.\nHãy bắt đầu nhập để thêm thể loại. Thể loại Cài đặt Mở tài khoản + Hình ảnh chọn lọc Giới thiệu - Phần mềm mã nguồn mở được phát hành theo <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/COPYING\">Giấy phép Apache v2</a>. %1$s và biểu trưng của nó là nhãn hiệu của Quỹ Wikimedia và được sử dụng do Quỹ Wikimedia cho phép. Chúng tôi không được Quỹ Wikimedia ủng hộ hoặc nhận làm chi nhánh. - <a href=\"https://github.com/commons-app/apps-android-commons\">Mã nguồn</a> và <a href=\"https://commons-app.github.io/\">trang chủ</a> tại GitHub. <a href=\"https://github.com/commons-app/apps-android-commons/issues\">Tạo vấn đề GitHub mới</a> để báo cáo lỗi hoặc gợi ý thay đổi. - - <a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\">Công trạng</a> + Ứng dụng Wikimedia Commons là ứng dụng mã nguồn mở được sáng tạo và quản lý bởi các tình nguyện viên và những người được tin tưởng của cộng đồng Wikipedia. Wikimedia Foundation không tham gia vào sự tạo lập, phát triển cũng như quản lý của ứng dụng. + Tạo một <a href=\"https://github.com/commons-app/apps-android-commons/issues\">thảo luận (issue) mới trên GitHub</a> để báo cáo lỗi cũng như đưa ra các ý tưởng. + <u>Chính sách riêng tư</u> + <u>Công trạng</u> Giới thiệu Gửi Phản hồi (qua Thư điện tử) Không có chương trình thư điện tử nào được cài đặt @@ -84,10 +101,10 @@ Hình này sẽ được phát hành theo giấy phép %1$s Với việc đăng hình này, tôi tuyên bố rằng nó là tác phẩm của chính mình, rằng nó không chứa nội dung có bản quyền hoặc ảnh tự chụp, và về mặt khác thì hoàn toàn tuân theo <a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines?uselang=vi\">các quy định Wikimedia Commons</a>. Tải về - Giấy phép + Giấy phép mặc định Sử dụng tiêu đề/miêu tả trước Lấy vị trí hiện tại tự động - Định vị hiện tại để nhận gợi ý thể loại trong trường hợp hình ảnh chưa được gắn thẻ địa lý + Định vị hiện tại để nhận gợi ý thể loại trong trường hợp hình ảnh chưa được gắn thẻ địa lý Chế độ ban đêm Dùng chủ đề tối Ghi công–Chia sẻ tương tự 4.0 @@ -113,11 +130,20 @@ Wikimedia Commons là nơi lưu giữ phần nhiều hình ảnh xuất hiện trong Wikipedia. Các hình ảnh của bạn giúp giáo dục người dân trên khắp thế giới! Xin hãy tải lên các hình ảnh do chính bạn chụp hoặc vẽ: - - Thiên nhiên (bông hoa, thú vật, cảnh núi)\n- Vật nhân tạo (xe đạp, nhà ga, món ăn)\n- Nhân vật nổi tiếng (thị trưởng của bạn, cầu thủ đội tuyển mà bạn gặp) + Thiên nhiên (bông hoa, thú vật, cảnh núi)\n* Vật nhân tạo (xe đạp, nhà ga, món ăn)\n* Nhân vật nổi tiếng (thị trưởng của bạn, cầu thủ đội tuyển mà bạn gặp) + Thiên nhiên (bông hoa, thú vật, cảnh núi) + Vật nhân tạo (xe đạp, nhà ga, món ăn) + Nhân vật nổi tiếng (thị trưởng của bạn, cầu thủ đội tuyển mà bạn gặp) Xin DỪNG tải lên: - Hình tự sướng hoặc hình bạn bè\n- Hình ảnh tải về từ Internet\n- Ảnh chụp màn hình của ứng dụng thương mại + Ảnh tự chụp hoặc hình bạn bè + Hình ảnh trên Internet tải về + Ảnh chụp màn hình của ứng dụng có bản quyền Tập tin tải lên ví dụ: - - Tiêu đề: Nhà hát Opera Sydney\n- Miêu tả: Nhà hát Opera Sydney nhìn qua cảng\n- Thể loại: Sydney Opera House, Sydney Opera House from the west, Sydney Opera House remote views + - Tiêu đề: Nhà hát Opera Sydney\n- Miêu tả: Nhà hát Opera Sydney nhìn qua cảng\n- Thể loại: Nhà hát Opera Sydney nhìn từ phía tây, Nhà hát Opera Sydney nhìn từ xa + Tiêu đề: Nhà hát Opera Sydney + Miêu tả: Nhà hát Opera Sydney nhìn qua cảng + Thể loại: Nhà hát Opera Sydney nhìn từ phía tây, Nhà hát Opera Sydney nhìn từ xa Đóng góp hình ảnh của bạn. Làm sinh động các bài viết Wikipedia! Các hình ảnh trên Wikipedia được cung cấp bởi Wikimedia Commons. Các hình ảnh của bạn giúp giáo dục người dân trên khắp thế giới. @@ -130,8 +156,8 @@ Không miêu tả Giấy phép không rõ Làm tươi - Yêu cầu cấp phép: Đọc thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép đọc thiết bị lưu trữ bên ngoài để hoạt động. - Yêu cầu cấp phép: Ghi vào thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép ghi vào thiết bị lưu trữ bên ngoài để hoạt động. + Yêu cầu cấp phép: Đọc thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép đọc thiết bị lưu trữ bên ngoài để truy cập kho ảnh của bạn. + Yêu cầu cấp phép: Ghi vào thiết bị lưu trữ bên ngoài. Ứng dụng cần được phép ghi vào thiết bị lưu trữ bên ngoài để truy cập máy chụp hình của bạn. Tùy chọn cấp phép: Định vị hiện tại để nhận gợi ý thể loại OK Nơi Lân cận @@ -144,6 +170,7 @@ Tiêu đề phương tiện Mô tả Mô tả phương tiện xuất hiện tại đây. Nó có thể khá dài và sẽ cần phải ngắt thành nhiều dòng. Nhưng chúng tôi hy vọng sẽ trông ưa nhìn. + Tác giả Ngày tải lên Giấy phép Tọa độ @@ -158,6 +185,9 @@ Hiện chưa hỗ trợ xác thực dùng hai nhân tố. Bạn có chắc chắn muốn đăng xuất? Biểu trưng Commons + Trang Web Commons + Trang Commons tại Facebook + Mã nguồn Commons tại GitHub Hình nền Hình ảnh bị Thất bại Không tìm thấy Hình ảnh @@ -182,15 +212,58 @@ Phản hồi Đăng xuất Hướng dẫn + Thông báo + Chọn lọc Cần có quyền truy cập vị trí của bạn để hiển thị các địa điểm lân cận không tìm thấy miêu tả Trang tập tin Commons Khoản mục Wikidata + Bài Wikipedia Xuất hiện lỗi khi đưa hình ảnh vào vùng nhớ đệm Tên ngắn và duy nhất cho tập tin sẽ được dùng làm tên tập tin. Có thể dùng thuật ngữ bình thường với khoảng cách. Đừng bao gồm phần mở rộng tập tin. Xin vui lòng miêu tả phương tiện càng đầy đủ càng tốt: Chụp ở đâu? Trong hình có gì? Bối cảnh làm sao? Xin vui lòng miêu tả các đối tượng và người trong hình. Cho biết những thông tin khó đoán ra, chẳng hạn giờ trong ngày nếu là phong cảnh. Nếu phương tiện có gì kỳ lạ, xin vui lòng giải thích tại sao nó kỳ lạ. + Cho phép Sử dụng thiết bị lưu trữ bên ngoài Lưu các hình ảnh được chụp bằng máy chụp hình trong ứng dụng vào thiết bị của bạn + Đăng nhập vào tài khoản của bạn + Gửi tập tin nhật trình + Gửi tập tin nhật trình cho nhà phát triển qua thư điện tử + Không tìm thấy trình duyệt để mở URL + Lỗi! Không tìm thấy URL Bầu chọn để xóa + Có đề nghị xóa hình này. Hiện lên ở trang xem browse + Vị trí không thay đổi. + Vị trí không có sẵn. + Bạn cần cho phép để hiển thị danh sách nơi lân cận + HƯỚNG DẪN + ĐỌC BÀI + Hoan nghênh %1$s đã đến Wikimedia Commons! Chúng tôi rất mừng bạn đến đây. + %1$s đã nhắn tin tại trang thảo luận của bạn + Cảm ơn vì bạn đã thực hiện sửa đổi + %1$s đã nhắc đến bạn tại %2$s. + HƯỚNG DẪN + WIKIDATA + WIKIPEDIA + COMMONS + <u>Đánh giá chúng tôi</u> + <u>Câu thường hỏi</u> + Bỏ qua Hướng dẫn + Internet không có sẵn + Internet có sẵn + Lỗi khi lấy thông báo + Không tìm thấy thông báo + <u>Biên dịch</u> + Ngôn ngữ + Chọn ngôn ngữ để gửi bản dịch + Tiến hành + Hủy bỏ + Thử lại + Được rồi! + Không tìm thấy hình ảnh! + Đã xuất hiện lỗi khi tải hình ảnh. + Tải lên bởi: %1$s + Chia sẻ Ứng dụng + Tọa độ không được chỉ định khi chọn hình ảnh + Lỗi khi lấy các nơi lân cận. diff --git a/app/src/main/res/values-xmf/strings.xml b/app/src/main/res/values-xmf/strings.xml index b9689f9e2..3ab830fb9 100644 --- a/app/src/main/res/values-xmf/strings.xml +++ b/app/src/main/res/values-xmf/strings.xml @@ -14,7 +14,7 @@ სისტემაშა მიშულაქ წჷმოძინელო გეთუ! სისტემაშა მიშულაქ ვემიხუჯინუ! ფაილქ ვეგორუ. ქორთხინთ, ქოცადით შხვა ფაილი. - აუთენტიფიკაციაქ ვემიხუჯინუ! + აუთენტიფიკაციაქ ვემიხუჯინუ! ეხარგუაქ ქჷდიჭყუ! %1$ ეხარგილი რე! ქეგუწკანტეთ თქვანი ეხარგუაშ ოძირაფალო @@ -41,8 +41,6 @@ დუდჯოხო ეჭარუა მიშულაქ ვემიხუჯინუ - რშვილიშ ჩილათა - მიშულაქ ვემიხუჯინუ - ქორთხინთ გეგნაჯინით ჯოხოს - მიშულაქ ვემიხუჯინუ - ქორთხინთ გეგნაჯინით პაროლს ძალამ მიარე უმწუძინუ ცადება. ქორთხინ, მუხირენ წუთშა ხოლო ქოცადით. მორდება, თე მახვარებუ ბლოკირი რე ვიკიოწკარუეს თქვა გემშიონათ ოკო ჟირფაქტორიანი ავტორიზაციაშ კოდი. diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index a2878cddf..662d2c96c 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -4,6 +4,7 @@ * Kly * LNDDYL * Liuxinyu970226 +* S099001 * Simon Shek * StephDC * Wwycheuk @@ -11,6 +12,8 @@ * 烈羽 --> + 探索 + 探索 外觀 一般 意見回饋 @@ -18,6 +21,7 @@ 共享資源 設定 + 上傳至共享資源 使用者名稱 密碼 登入您的維基共享資源測試版帳號 @@ -28,11 +32,11 @@ 請稍候… 登入成功! 登入失敗! - 找不到檔案。請嘗試其它檔案看看。 - 未能核對身分! + 找不到檔案。請試試看其它檔案。 + 身份驗證失敗,請重新登入 開始上傳! 已上傳%1$s! - 點選檢視您上傳的項目 + 輕觸來檢視您上傳的項目 開始上傳%1$s 正在上傳%1$s 即將完成上傳 %1$s @@ -42,7 +46,7 @@ 正在上載 %1$d 個檔案 正在上載 %1$d 個檔案 - 我的最近上傳 + 我最近的上傳 已佇列 失敗 %1$d%%完成 @@ -57,8 +61,7 @@ 請提供此檔案的標題 說明 無法登入-網路故障 - 無法登入-請檢查您的使用者名稱 - 無法登入-請檢查您的密碼 + 無法登入 - 請檢查您的使用者名稱與密碼 失敗次數過多。請於幾分鐘後重試。 很抱歉,該使用者已被維基共享資源封禁 必須提供您的雙重因素身分核對代碼。 @@ -93,6 +96,7 @@ 設定 註冊 特色圖片 + 分類 關於 維基共享資源應用程式是透過維基媒體社群上的受讓人,與志願者們所建立及維護的開放原始碼應用程式。維基媒體基金會並不涉及此應用程式的建立、開發,與維護方面。 建立新的<a href=\"https://github.com/commons-app/apps-android-commons/issues\"> GitHub 問題</a>來回報程式錯誤和提出建議。 @@ -110,9 +114,9 @@ 透過提交此圖片,我宣佈這是我個人創作的成品,且不包含受版權保護或自拍內容,並除此之外遵守<a href=\"https://commons.wikimedia.org/wiki/Commons:Policies_and_guidelines\">維基媒體共享資源方針</a>。 下載 預設授權條款 - 使用先前標題/說明 + 使用先前標題、說明 自動獲取目前位置 - 若圖片未有地理標記,就以目前位置來作為分類建議。 + 如果圖片沒有地理標記,就索取目前位置的地理資訊來標記在圖片上。注意:這會透露出您目前所在的位置。 夜間模式 使用暗黑佈景主題 姓名標示-相同方式分享4.0 @@ -155,9 +159,10 @@ 貢獻您的圖片,使維基百科的文章更加生動! 維基百科的圖片,來自維基共享資源。 您的圖片可以幫助教育世界各地的人。 - 避免使用受版權保護的材料,例如從互聯網找來的圖片、海報、書籍封面等 - 明白了嗎? - 是! + 避免使用受版權保護的材料,例如從網際網路找來的圖片、海報、書籍封面等 + 以上您明白了嗎? + 是的! + 此提示為空,可能無效。請見錯誤報告: https://github.com/commons-app/apps-android-commons/issues/1333 。 分類 載入中… 未選擇 @@ -200,6 +205,8 @@ 背景圖片 媒體圖片失敗 找不到圖片 + 找不到子分類 + 找不到父分類 上傳圖片 藏王連峰 大羊駝 @@ -243,6 +250,7 @@ 錯誤!查無 URL 提名刪除 此圖片已被提名刪除。 + 此提示為空,可能無效。請見錯誤報告: https://github.com/commons-app/apps-android-commons/issues/1333 。 於瀏覽器檢視 位置無法更改。 位置無效。 @@ -271,5 +279,32 @@ 已進行 取消 重試 + 了解! + 這些是在您的附近,並且需要圖片來圖解有關它們的維基百科條目之地點 + 輕觸此按鈕來帶出這些地點的清單 + 您可從您的圖庫或相機,來上傳任何地點的圖片 + 找不到圖片! + 當載入圖片時發生錯誤。 + 由:%1$s 上傳 + 您被禁止編輯共享資源 分享應用程式 + 當選擇圖片時未指定座標 + 索取附近地點時出錯。 + 每日圖片 + 每日圖片 + 搜尋 + 搜尋共享資源 + 找不到符合%1$s的圖片 + 搜尋 + 近期搜尋: + 近期搜尋查詢 + 當載入分類時發生錯誤。 + 當載入子分類時發生錯誤。 + 圖片已成功添加到維基數據上的 %1$s! + 更新所對應的維基數據項目失敗! + 設定桌布 + 桌布設定成功! + 確定要清除搜尋歷史嗎? + 搜尋歷史已刪除 + 登入用 session 逾期,請重新登入。 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index d3d049ea2..ec287e7c3 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -1,14 +1,18 @@ + 探索 + 探索 外观 一般 反馈 @@ -27,7 +31,7 @@ 登录成功! 登录失败! 找不到文件。请尝试其他文件。 - 身份验证失败! + 身份验证失败,请重新登录 上传开始! %1$s已上传! 点击查看您的上传 @@ -55,8 +59,7 @@ 请提供此文件的标题 说明 无法登录 - 网络故障 - 无法登录 - 请检查您的用户名 - 无法登录 - 请检查您的密码 + 无法登录——请检查您的用户名和密码 失败次数过多。请在几分钟后重试。 对不起,该用户已经被共享资源封禁 您必须提供您的双因素验证代码。 @@ -91,6 +94,7 @@ 设置 注册 特色图片 + 类别 关于 维基共享资源应用程序是由维基媒体社区的受助者和志愿者创建和维护的开源应用程序。维基媒体基金会不参与该应用程序的创立,开发或维护。 创建新的<a href=\"https://github.com/commons-app/apps-android-commons/issues\">GitHub问题</a>以发送错误报告和建议。 @@ -110,7 +114,7 @@ 默认许可协议 使用之前的标题/描述 自动获取当前位置 - 如果图片没有地理标记的话,就取得当前位置以提供分类建议 + 如果图片没有地理标记,以及地理标签图片的话,就取得当前位置。警告:这将暴露您的当前位置。 夜间模式 使用黑暗主题 署名-相同方式共享4.0 @@ -198,6 +202,8 @@ 背景图片 媒体图片失败 找不到图片 + 找不到子类别 + 找不到父分类 上传图片 藏王连峰 大羊驼 @@ -269,5 +275,26 @@ 已处理 取消 重试 + 明白了! + 这些是您附近需要图片以阐明维基百科条目的地方 + 点按此按钮会出现这些地点的列表 + 您可以从您的图库或照相机中上传任意地点的图片 + 找不到图片! + 加载图片时出错。 + 由%1$s上传 分享应用 + 图片选择时,坐标并未指定 + 检索附近地点时出错。 + 每日图片 + 每日图片 + 搜索 + 找不到匹配%1$s的图片 + 搜索 + 加载子类别时发生错误。 + 图片已成功添加到维基数据上的%1$s! + 更新对应维基数据实体失败! + 设置墙纸 + 墙纸已成功设置! + 您确认要清除您的搜索历史? + 登录会话已过期,请重新登录。 diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 8d125aa61..a4c9ef716 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -23,4 +23,10 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index ef5000d60..bad958661 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -50,6 +50,9 @@ #E0E0E0 #424242 + #D6DCE0 #757575 + #FFFFFF + #000000 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7eaf97880..bac816cd4 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -4,6 +4,7 @@ 16dp 16dp + 8dp 48dp @@ -25,4 +26,10 @@ 14sp 15dp 25dp + + + 8dp diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5c375d14d..cafab6ed8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,7 @@ + Explore + Explore Appearance General Feedback @@ -7,6 +9,7 @@ Commons Settings + Upload to Commons Username Password Log in to your Commons Beta account @@ -18,7 +21,7 @@ Login success! Login failed! File not found. Please try another file. - Authentication failed! + Authentication failed, please login again Upload started! %1$s uploaded! Tap to view your upload @@ -46,8 +49,7 @@ Please provide a title for this file Description Unable to login - network failure - Unable to login - please check your username - Unable to login - please check your password + Unable to login - please check your username and password Too many unsuccessful attempts. Please try again in a few minutes. Sorry, this user has been blocked on Commons You must provide your two factor authentication code. @@ -74,8 +76,8 @@ Starting %1$d uploads - %1$d upload - %1$d uploads + %1$d upload + %1$d uploads No categories matching %1$s found Add categories to make your images more discoverable on Wikimedia Commons.\nStart typing to add categories. @@ -83,6 +85,7 @@ Settings Sign Up Featured Images + Category About The Wikimedia Commons app is an open-source app created and maintained by grantees and volunteers of the Wikimedia community. The Wikimedia Foundation is not involved in the creation, development, or maintenance of the app. Wikimedia Commons @@ -103,7 +106,7 @@ Default License Use previous title/description Automatically get current location - Retrieve current location to offer category suggestions if image is not geotagged + Retrieves current location if image is not geotagged, and geotags image with it. Warning: This will reveal your current location. Night mode Use dark theme Attribution-ShareAlike 4.0 @@ -188,7 +191,7 @@ Unable to display more than 500 Set Recent Upload Limit Two factor authentication is currently not supported. - Do you really want to logout? + Do you really want to logout? Commons Logo Commons Website Commons Facebook Page @@ -196,6 +199,8 @@ Background Image Media Image Failed No Image Found + No subcategories found + No parent categories found Upload Image Mount Zao Llamas @@ -241,46 +246,85 @@ This image has been nominated for deletion. See webpage for details View in Browser - Location has not changed. - Location not available. - Permission required to display a list of nearby places - GET DIRECTIONS - READ ARTICLE - Welcome to Wikimedia Commons, %1$s! We\'re glad you\'re here. - %1$s left a message on your talk page - Thank you for making an edit - %1$s mentioned you on %2$s. - Toggle view - DIRECTIONS - WIKIDATA - WIKIPEDIA - COMMONS - Rate us]]> - FAQ]]> - Skip Tutorial - Internet unavailable - Internet available - Error fetching notifications - No notifications found - Translate]]> - Languages - Select the language that you would like to submit translations for - Proceed - Cancel - Retry Skip - Login - Do you really want to skip login ? - You might not be able to access some features of the app. - Got it! - These are the places near you that need pictures to illustrate their Wikipedia articles - Tapping this button brings up a list of these places - You can upload a picture for any place from your gallery or camera - No images found! - Error occurred while loading images. - Uploaded by: %1$s - Share App - Coordinates were not specified during image selection - This feature requires user to be logged in !! + Log in + Do you really want to skip login? + You will not be able to upload pictures. + This feature requires user to be logged in! + Location has not changed. + Location not available. + Permission required to display a list of nearby places + GET DIRECTIONS + READ ARTICLE + + Welcome to Wikimedia Commons, %1$s! We\'re glad you\'re here. + %1$s left a message on your talk page + Thank you for making an edit + %1$s mentioned you on %2$s. + Toggle view + DIRECTIONS + WIKIDATA + WIKIPEDIA + COMMONS + Rate us]]> + FAQ]]> + Skip Tutorial + Internet unavailable + Internet available + Error fetching notifications + No notifications found + Translate]]> + Languages + Select the language that you would like to submit translations for + Proceed + Cancel + Retry + + Got it! + These are the places near you that need pictures to illustrate their Wikipedia articles + Tapping this button brings up a list of these places + You can upload a picture for any place from your gallery or camera + + No images found! + Error occurred while loading images. + Uploaded by: %1$s + + You are blocked from editing Commons + Share App + Coordinates were not specified during image selection + Error fetching nearby places. + Pic of the Day + Pic of the Day + Search + Search Commons + No Images matching %1$s found + Search + Recent searches: + Recently searched queries + Error occurred while loading categories. + Error occurred while loading subcategories. + + Image successfully added to %1$s on Wikidata! + Failed to update corresponding Wikidata entity! + Set wallpaper + Wallpaper set successfully! + Are you sure you want to clear your search history? + Search history deleted + + Achievements + STATISTICS + Thanks Received + Featured Images + LEVEL + Images Uploaded + Images Not Reverted + Images Used + Share your achievements with your friends! + Your level increases as you meet these requirements. Items in the "statistics" section do not count towards your level. + minimum required: + The number of images you have uploaded to Commons, via any upload software + The percentage of images you have uploaded to Commons that were not deleted + The number of images you have uploaded to Commons that were used in Wikimedia articles + Login session expired, please log in again. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index a8eb1d849..26d9c357b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -57,4 +57,34 @@ @android:color/transparent + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/pic_of_day_app_widget_info.xml b/app/src/main/res/xml/pic_of_day_app_widget_info.xml new file mode 100644 index 000000000..5b370e9a1 --- /dev/null +++ b/app/src/main/res/xml/pic_of_day_app_widget_info.xml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 49720b247..37b6a8b01 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -2,66 +2,58 @@ - - - + - - - - - + - + - - - - - - - + - - + \ No newline at end of file diff --git a/app/src/main/res/xml/provider_paths.xml b/app/src/main/res/xml/provider_paths.xml index 696723b68..c0669d474 100644 --- a/app/src/main/res/xml/provider_paths.xml +++ b/app/src/main/res/xml/provider_paths.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/resources/queries/nearby_query.rq b/app/src/main/resources/queries/nearby_query.rq index 0453005e8..b11f0985a 100644 --- a/app/src/main/resources/queries/nearby_query.rq +++ b/app/src/main/resources/queries/nearby_query.rq @@ -39,6 +39,11 @@ SELECT # Get emoji OPTIONAL { ?classId wdt:P487 ?emoji0. } OPTIONAL { ?classId wdt:P279*/wdt:P487 ?emoji1. } + + OPTIONAL { + ?wikipediaArticle schema:about ?item ; + schema:isPartOf . + } OPTIONAL { ?wikipediaArticle schema:about ?item ; schema:isPartOf . diff --git a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt index b1de29143..edb9fc374 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/TestCommonsApplication.kt @@ -1,5 +1,6 @@ package fr.free.nrw.commons +import android.content.ContentProviderClient import android.content.Context import android.content.SharedPreferences import android.support.v4.util.LruCache @@ -8,7 +9,6 @@ import com.nhaarman.mockito_kotlin.mock import com.squareup.leakcanary.RefWatcher import fr.free.nrw.commons.auth.AccountUtil import fr.free.nrw.commons.auth.SessionManager -import fr.free.nrw.commons.caching.CacheController import fr.free.nrw.commons.data.DBOpenHelper import fr.free.nrw.commons.di.CommonsApplicationComponent import fr.free.nrw.commons.di.CommonsApplicationModule @@ -33,21 +33,31 @@ class TestCommonsApplication : CommonsApplication() { override fun setupLeakCanary(): RefWatcher = RefWatcher.DISABLED } +@Suppress("MemberVisibilityCanBePrivate") class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModule(appContext) { val accountUtil: AccountUtil = mock() val appSharedPreferences: SharedPreferences = mock() val defaultSharedPreferences: SharedPreferences = mock() - val categorySharedPreferences: SharedPreferences = mock() val otherSharedPreferences: SharedPreferences = mock() val uploadController: UploadController = mock() val mockSessionManager: SessionManager = mock() - val mediaWikiApi: MediaWikiApi = mock() val locationServiceManager: LocationServiceManager = mock() - val cacheController: CacheController = mock() val mockDbOpenHelper: DBOpenHelper = mock() val nearbyPlaces: NearbyPlaces = mock() val lruCache: LruCache = mock() val gson: Gson = Gson() + val categoryClient: ContentProviderClient = mock() + val contributionClient: ContentProviderClient = mock() + val modificationClient: ContentProviderClient = mock() + val uploadPrefs: SharedPreferences = mock() + + override fun provideCategoryContentProviderClient(context: Context?): ContentProviderClient = categoryClient + + override fun provideContributionContentProviderClient(context: Context?): ContentProviderClient = contributionClient + + override fun provideModificationContentProviderClient(context: Context?): ContentProviderClient = modificationClient + + override fun providesDirectNearbyUploadPreferences(context: Context?): SharedPreferences = uploadPrefs override fun providesAccountUtil(context: Context): AccountUtil = accountUtil @@ -61,12 +71,8 @@ class MockCommonsApplicationModule(appContext: Context) : CommonsApplicationModu override fun providesSessionManager(context: Context, mediaWikiApi: MediaWikiApi, sharedPreferences: SharedPreferences): SessionManager = mockSessionManager - override fun provideMediaWikiApi(context: Context, sharedPreferences: SharedPreferences, categorySharedPreferences: SharedPreferences, gson: Gson): MediaWikiApi = mediaWikiApi - override fun provideLocationServiceManager(context: Context): LocationServiceManager = locationServiceManager - override fun provideCacheController(): CacheController = cacheController - override fun provideDBOpenHelper(context: Context): DBOpenHelper = mockDbOpenHelper override fun provideNearbyPlaces(): NearbyPlaces = nearbyPlaces diff --git a/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt index 7efb48c24..84e3ae173 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/UtilsTest.kt @@ -28,4 +28,8 @@ class UtilsTest { @Test fun `capitalize - pass Japanase characters`() { Assert.assertThat(Utils.capitalize("こんにちは"), _is("こんにちは")) } + + @Test fun `capitalize does not fail on empty string`() { + Assert.assertThat(Utils.capitalize(""), _is("")) + } } diff --git a/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt new file mode 100644 index 000000000..d764c8da7 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/explore/recentsearches/RecentSearchesDaoTest.kt @@ -0,0 +1,307 @@ +package fr.free.nrw.commons.explore.recentsearches + +import android.content.ContentProviderClient +import android.content.ContentValues +import android.database.Cursor +import android.database.MatrixCursor +import android.database.sqlite.SQLiteDatabase +import android.os.RemoteException +import com.nhaarman.mockito_kotlin.* +import fr.free.nrw.commons.BuildConfig +import fr.free.nrw.commons.TestCommonsApplication +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.BASE_URI +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider.uriForId +import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao.Table.* +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import java.util.* + +@RunWith(RobolectricTestRunner::class) +@Config(constants = BuildConfig::class, sdk = [21], application = TestCommonsApplication::class) +class RecentSearchesDaoTest { + + private val columns = arrayOf(COLUMN_ID, COLUMN_NAME, COLUMN_LAST_USED) + private val client: ContentProviderClient = mock() + private val database: SQLiteDatabase = mock() + private val captor = argumentCaptor() + private val queryCaptor = argumentCaptor>() + + private lateinit var testObject: RecentSearchesDao + + @Before + fun setUp() { + testObject = RecentSearchesDao { client } + } + + /** + * Unit Test for creating a table for recent Searches + */ + @Test + fun createTable() { + onCreate(database) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + /** + * Unit Test for deleting table for recent Searches + */ + @Test + fun deleteTable() { + onDelete(database) + inOrder(database) { + verify(database).execSQL(DROP_TABLE_STATEMENT) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + } + + /** + * Unit Test for migrating from database version 1 to 2 for recent Searches Table + */ + @Test + fun migrateTableVersionFrom_v1_to_v2() { + onUpdate(database, 1, 2) + // Table didnt exist before v7 + verifyZeroInteractions(database) + } + + /** + * Unit Test for migrating from database version 2 to 3 for recent Searches Table + */ + @Test + fun migrateTableVersionFrom_v2_to_v3() { + onUpdate(database, 2, 3) + // Table didnt exist before v7 + verifyZeroInteractions(database) + } + + /** + * Unit Test for migrating from database version 3 to 4 for recent Searches Table + */ + @Test + fun migrateTableVersionFrom_v3_to_v4() { + onUpdate(database, 3, 4) + // Table didnt exist before v7 + verifyZeroInteractions(database) + } + + /** + * Unit Test for migrating from database version 4 to 5 for recent Searches Table + */ + @Test + fun migrateTableVersionFrom_v4_to_v5() { + onUpdate(database, 4, 5) + // Table didnt exist before v7 + verifyZeroInteractions(database) + } + + /** + * Unit Test for migrating from database version 5 to 6 for recent Searches Table + */ + @Test + fun migrateTableVersionFrom_v5_to_v6() { + onUpdate(database, 5, 6) + // Table didnt exist before v7 + verifyZeroInteractions(database) + } + + /** + * Unit Test for migrating from database version 6 to 7 for recent Searches Table + */ + @Test + fun migrateTableVersionFrom_v6_to_v7() { + onUpdate(database, 6, 7) + verify(database).execSQL(CREATE_TABLE_STATEMENT) + } + + /** + * Unit Test for migrating from database version 7 to 8 for recent Searches Table + */ + @Test + fun migrateTableVersionFrom_v7_to_v8() { + onUpdate(database, 7, 8) + // Table didnt change in version 8 + verifyZeroInteractions(database) + } + + /** + * Unit Test for migrating from creating a row without using ID in recent Searches Table + */ + @Test + fun createFromCursor() { + createCursor(1).let { cursor -> + cursor.moveToFirst() + testObject.fromCursor(cursor).let { + assertEquals(uriForId(1), it.contentUri) + assertEquals("butterfly", it.query) + assertEquals(123, it.lastSearched.time) + } + } + } + + /** + * Unit Test for migrating from updating a row using contentUri in recent Searches Table + */ + @Test + fun saveExistingQuery() { + createCursor(1).let { + val recentSearch = testObject.fromCursor(it.apply { moveToFirst() }) + + testObject.save(recentSearch) + + verify(client).update(eq(recentSearch.contentUri), captor.capture(), isNull(), isNull()) + captor.firstValue.let { cv -> + assertEquals(2, cv.size()) + assertEquals(recentSearch.query, cv.getAsString(COLUMN_NAME)) + assertEquals(recentSearch.lastSearched.time, cv.getAsLong(COLUMN_LAST_USED)) + } + } + } + + /** + * Unit Test for migrating from creating a row using ID in recent Searches Table + */ + @Test + fun saveNewQuery() { + val contentUri = RecentSearchesContentProvider.uriForId(111) + whenever(client.insert(isA(), isA())).thenReturn(contentUri) + val recentSearch = RecentSearch(null, "butterfly", Date(234L)) + + testObject.save(recentSearch) + + verify(client).insert(eq(BASE_URI), captor.capture()) + captor.firstValue.let { cv -> + assertEquals(2, cv.size()) + assertEquals(recentSearch.query, cv.getAsString(COLUMN_NAME)) + assertEquals(recentSearch.lastSearched.time, cv.getAsLong(COLUMN_LAST_USED)) + assertEquals(contentUri, recentSearch.contentUri) + } + } + + /** + * Unit Test for checking translation exceptions in searching a row from DB using recent search query + */ + @Test(expected = RuntimeException::class) + fun findRecentSearchTranslatesExceptions() { + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenThrow(RemoteException("")) + testObject.find("butterfly") + } + + /** + * Unit Test for checking data if it's not present in searching a row from DB using recent search query + */ + @Test + fun whenTheresNoDataFindReturnsNull_nullCursor() { + whenever(client.query(any(), any(), any(), any(), any())).thenReturn(null) + assertNull(testObject.find("butterfly")) + } + + /** + * Unit Test for checking data if it's not present in searching a row from DB using recent search query + */ + @Test + fun whenTheresNoDataFindReturnsNull_emptyCursor() { + whenever(client.query(any(), any(), any(), any(), any())).thenReturn(createCursor(0)) + assertNull(testObject.find("butterfly")) + } + + /** + * Unit Test for checking if cursor's are closed after use or not + */ + @Test + fun cursorsAreClosedAfterUse() { + val mockCursor: Cursor = mock() + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(mockCursor) + whenever(mockCursor.moveToFirst()).thenReturn(false) + + testObject.find("butterfly") + + verify(mockCursor).close() + } + + /** + * Unit Test for checking search results after searching a row from DB using recent search query + */ + @Test + fun findRecentSearchQuery() { + whenever(client.query(any(), any(), any(), any(), anyOrNull())).thenReturn(createCursor(1)) + + val recentSearch = testObject.find("butterfly") + assertNotNull(recentSearch) + + assertEquals(uriForId(1), recentSearch?.contentUri) + assertEquals("butterfly", recentSearch?.query) + assertEquals(123L, recentSearch?.lastSearched?.time) + + verify(client).query( + eq(BASE_URI), + eq(ALL_FIELDS), + eq("$COLUMN_NAME=?"), + queryCaptor.capture(), + isNull() + ) + assertEquals("butterfly", queryCaptor.firstValue[0]) + } + + /** + * Unit Test for checking if cursor's are closed after recent search query or not + */ + @Test + fun cursorsAreClosedAfterRecentSearchQuery() { + val mockCursor: Cursor = mock() + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(mockCursor) + whenever(mockCursor.moveToFirst()).thenReturn(false) + + testObject.recentSearches(1) + + verify(mockCursor).close() + } + + /** + * Unit Test for checking when recent searches returns less than the limit + */ + @Test + fun recentSearchesReturnsLessThanLimit() { + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(1)) + + val result = testObject.recentSearches(10) + + assertEquals(1, result.size) + assertEquals("butterfly", result[0]) + + verify(client).query( + eq(BASE_URI), + eq(ALL_FIELDS), + isNull(), + queryCaptor.capture(), + eq("$COLUMN_LAST_USED DESC") + ) + assertEquals(0, queryCaptor.firstValue.size) + } + + /** + * Unit Test for checking size or list recieved from recent searches + */ + @Test + fun recentSearchesHonorsLimit() { + whenever(client.query(any(), any(), anyOrNull(), any(), any())).thenReturn(createCursor(10)) + + val result = testObject.recentSearches(5) + + assertEquals(5, result.size) + } + + /** + * Unit Test for creating entries in recent searches database. + * @param rowCount No of rows + */ + private fun createCursor(rowCount: Int) = MatrixCursor(columns, rowCount).apply { + for (i in 0 until rowCount) { + addRow(listOf("1", "butterfly", "123")) + } + } + +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt index 686a90ef2..fd6e93fab 100644 --- a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/ApacheHttpClientMediaWikiApiTest.kt @@ -18,6 +18,7 @@ import org.robolectric.RobolectricTestRunner import org.robolectric.RuntimeEnvironment import org.robolectric.annotation.Config import java.net.URLDecoder +import java.text.SimpleDateFormat import java.util.* @RunWith(RobolectricTestRunner::class) @@ -26,15 +27,17 @@ class ApacheHttpClientMediaWikiApiTest { private lateinit var testObject: ApacheHttpClientMediaWikiApi private lateinit var server: MockWebServer + private lateinit var wikidataServer: MockWebServer private lateinit var sharedPreferences: SharedPreferences private lateinit var categoryPreferences: SharedPreferences @Before fun setUp() { server = MockWebServer() + wikidataServer = MockWebServer() sharedPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) categoryPreferences = PreferenceManager.getDefaultSharedPreferences(RuntimeEnvironment.application) - testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", sharedPreferences, categoryPreferences, Gson()) + testObject = ApacheHttpClientMediaWikiApi(RuntimeEnvironment.application, "http://" + server.hostName + ":" + server.port + "/", "http://" + wikidataServer.hostName + ":" + wikidataServer.port + "/", sharedPreferences, categoryPreferences, Gson()) testObject.setWikiMediaToolforgeUrl("http://" + server.hostName + ":" + server.port + "/") } @@ -223,6 +226,86 @@ class ApacheHttpClientMediaWikiApiTest { assertEquals(23, testObserver.values()[0]) } + @Test + fun isUserBlockedFromCommonsForInfinitelyBlockedUser() { + server.enqueue(MockResponse().setBody("")) + + val result = testObject.isUserBlockedFromCommons(); + + assertBasicRequestParameters(server, "GET").let { userBlockedRequest -> + parseQueryParams(userBlockedRequest).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("userinfo", body["meta"]) + assertEquals("blockinfo", body["uiprop"]) + } + } + + assertTrue(result) + } + + @Test + fun isUserBlockedFromCommonsForTimeBlockedUser() { + val currentDate = Date() + val expiredDate = Date(currentDate.time + 10000) + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) + server.enqueue(MockResponse().setBody("")) + + val result = testObject.isUserBlockedFromCommons(); + + assertBasicRequestParameters(server, "GET").let { userBlockedRequest -> + parseQueryParams(userBlockedRequest).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("userinfo", body["meta"]) + assertEquals("blockinfo", body["uiprop"]) + } + } + + assertTrue(result) + } + + @Test + fun isUserBlockedFromCommonsForExpiredBlockedUser() { + val currentDate = Date() + val expiredDate = Date(currentDate.time - 10000) + val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'") + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")) + server.enqueue(MockResponse().setBody("")) + + val result = testObject.isUserBlockedFromCommons(); + + assertBasicRequestParameters(server, "GET").let { userBlockedRequest -> + parseQueryParams(userBlockedRequest).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("userinfo", body["meta"]) + assertEquals("blockinfo", body["uiprop"]) + } + } + + assertFalse(result) + } + + @Test + fun isUserBlockedFromCommonsForNotBlockedUser() { + server.enqueue(MockResponse().setBody("")) + + val result = testObject.isUserBlockedFromCommons(); + + assertBasicRequestParameters(server, "GET").let { userBlockedRequest -> + parseQueryParams(userBlockedRequest).let { body -> + assertEquals("xml", body["format"]) + assertEquals("query", body["action"]) + assertEquals("userinfo", body["meta"]) + assertEquals("blockinfo", body["uiprop"]) + } + } + + assertFalse(result) + } + private fun assertBasicRequestParameters(server: MockWebServer, method: String): RecordedRequest = server.takeRequest().let { assertEquals("/", it.requestUrl.encodedPath()) assertEquals(method, it.method) diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/CategoryApiTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/CategoryApiTest.kt new file mode 100644 index 000000000..76f34d55d --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/CategoryApiTest.kt @@ -0,0 +1,178 @@ +package fr.free.nrw.commons.mwapi + +import com.google.gson.Gson +import fr.free.nrw.commons.mwapi.model.Page +import fr.free.nrw.commons.mwapi.model.PageCategory +import okhttp3.HttpUrl +import okhttp3.OkHttpClient +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class CategoryApiTest { + private lateinit var server: MockWebServer + private lateinit var url: String + private lateinit var categoryApi: CategoryApi + + @Before + fun setUp() { + server = MockWebServer() + url = "http://${server.hostName}:${server.port}/" + categoryApi = CategoryApi(OkHttpClient.Builder().build(), Gson(), HttpUrl.parse(url)) + } + + @After + fun teardown() { + server.shutdown() + } + + @Test + fun apiReturnsEmptyListWhenError() { + server.enqueue(MockResponse().setResponseCode(400).setBody("")) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun apiReturnsEmptyWhenTheresNoQuery() { + server.success(emptyMap()) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun apiReturnsEmptyWhenQueryHasNoPages() { + server.success(mapOf("query" to emptyMap())) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun apiReturnsEmptyWhenQueryHasPagesButTheyreEmpty() { + server.success(mapOf("query" to + mapOf("pages" to emptyList()))) + + assertTrue(categoryApi.request("foo").blockingGet().isEmpty()) + } + + @Test + fun singlePageSingleCategory() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(1, response.size) + assertEquals("one", response[0]) + } + + @Test + fun multiplePagesSingleCategory() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one")), + page(listOf("two")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(2, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + } + + @Test + fun singlePageMultipleCategories() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one", "two")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(2, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + } + + @Test + fun multiplePagesMultipleCategories() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one", "two")), + page(listOf("three", "four")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(4, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + assertEquals("three", response[2]) + assertEquals("four", response[3]) + } + + @Test + fun multiplePagesMultipleCategories_duplicatesRemoved() { + server.success(mapOf("query" to + mapOf("pages" to listOf( + page(listOf("one", "two", "three")), + page(listOf("three", "four", "one")) + )))) + + val response = categoryApi.request("foo").blockingGet() + + assertEquals(4, response.size) + assertEquals("one", response[0]) + assertEquals("two", response[1]) + assertEquals("three", response[2]) + assertEquals("four", response[3]) + } + + @Test + fun requestSendsWhatWeExpect() { + server.success(mapOf("query" to mapOf("pages" to emptyList()))) + + val coords = "foo,bar" + categoryApi.request(coords).blockingGet() + + server.takeRequest().let { request -> + assertEquals("GET", request.method) + assertEquals("/w/api.php", request.requestUrl.encodedPath()) + request.requestUrl.let { url -> + assertEquals("query", url.queryParameter("action")) + assertEquals("categories|coordinates|pageprops", url.queryParameter("prop")) + assertEquals("json", url.queryParameter("format")) + assertEquals("!hidden", url.queryParameter("clshow")) + assertEquals("type|name|dim|country|region|globe", url.queryParameter("coprop")) + assertEquals(coords, url.queryParameter("codistancefrompoint")) + assertEquals("geosearch", url.queryParameter("generator")) + assertEquals(coords, url.queryParameter("ggscoord")) + assertEquals("10000", url.queryParameter("ggsradius")) + assertEquals("10", url.queryParameter("ggslimit")) + assertEquals("6", url.queryParameter("ggsnamespace")) + assertEquals("type|name|dim|country|region|globe", url.queryParameter("ggsprop")) + assertEquals("all", url.queryParameter("ggsprimary")) + assertEquals("2", url.queryParameter("formatversion")) + } + } + } + + private fun page(catList: List) = Page().apply { + categories = catList.map { + PageCategory().apply { + title = "Category:$it" + } + }.toTypedArray() + } +} + +fun MockWebServer.success(response: Map) { + enqueue(MockResponse().setResponseCode(200).setBody(Gson().toJson(response))) +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/ApiResponseTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/ApiResponseTest.kt new file mode 100644 index 000000000..41406a894 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/ApiResponseTest.kt @@ -0,0 +1,28 @@ +package fr.free.nrw.commons.mwapi.model + +import org.junit.Assert.* +import org.junit.Test + +class ApiResponseTest { + @Test + fun hasPages_whenQueryIsNull() { + val response = ApiResponse() + assertFalse(response.hasPages()) + } + + @Test + fun hasPages_whenPagesIsNull() { + val response = ApiResponse() + response.query = Query() + response.query.pages = null + assertFalse(response.hasPages()) + } + + @Test + fun hasPages_defaultsToSafeValue() { + val response = ApiResponse() + response.query = Query() + assertNotNull(response.query.pages) + assertTrue(response.hasPages()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageCategoryTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageCategoryTest.kt new file mode 100644 index 000000000..fcc3d408f --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageCategoryTest.kt @@ -0,0 +1,20 @@ +package fr.free.nrw.commons.mwapi.model + +import org.junit.Assert.assertEquals +import org.junit.Test + +class PageCategoryTest { + @Test + fun stripPrefix_whenPresent() { + val testObject = PageCategory() + testObject.title = "Category:Foo" + assertEquals("Foo", testObject.withoutPrefix()) + } + + @Test + fun stripPrefix_prefixAbsent() { + val testObject = PageCategory() + testObject.title = "Foo_Bar" + assertEquals("Foo_Bar", testObject.withoutPrefix()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageTest.kt new file mode 100644 index 000000000..4179b4fb5 --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/mwapi/model/PageTest.kt @@ -0,0 +1,12 @@ +package fr.free.nrw.commons.mwapi.model + +import org.junit.Assert.assertNotNull +import org.junit.Test + +class PageTest { + @Test + fun categoriesDefaultToSafeValue() { + val page = Page() + assertNotNull(page.getCategories()) + } +} \ No newline at end of file diff --git a/app/src/test/kotlin/fr/free/nrw/commons/upload/GpsCategoryModelTest.kt b/app/src/test/kotlin/fr/free/nrw/commons/upload/GpsCategoryModelTest.kt new file mode 100644 index 000000000..cd2d77ada --- /dev/null +++ b/app/src/test/kotlin/fr/free/nrw/commons/upload/GpsCategoryModelTest.kt @@ -0,0 +1,77 @@ +package fr.free.nrw.commons.upload + +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test + +class GpsCategoryModelTest { + + private lateinit var testObject : GpsCategoryModel + + @Before + fun setUp() { + testObject = GpsCategoryModel() + } + + @Test + fun initiallyTheModelIsEmpty() { + assertFalse(testObject.gpsCatExists) + assertTrue(testObject.categoryList.isEmpty()) + } + + @Test + fun addingCategoriesToTheModel() { + testObject.add("one") + assertTrue(testObject.gpsCatExists) + assertFalse(testObject.categoryList.isEmpty()) + assertEquals(listOf("one"), testObject.categoryList) + } + + @Test + fun duplicatesAreIgnored() { + testObject.add("one") + testObject.add("one") + assertEquals(listOf("one"), testObject.categoryList) + } + + @Test + fun modelProtectsAgainstExternalModification() { + testObject.add("one") + + val list = testObject.categoryList + list.add("two") + + assertEquals(listOf("one"), testObject.categoryList) + } + + @Test + fun clearingTheModel() { + testObject.add("one") + + testObject.clear() + assertFalse(testObject.gpsCatExists) + assertTrue(testObject.categoryList.isEmpty()) + + testObject.add("two") + assertEquals(listOf("two"), testObject.categoryList) + } + + @Test + fun settingTheListHandlesNull() { + testObject.add("one") + + testObject.categoryList = null + + assertFalse(testObject.gpsCatExists) + assertTrue(testObject.categoryList.isEmpty()) + } + + @Test + fun setttingTheListOverwritesExistingValues() { + testObject.add("one") + + testObject.categoryList = listOf("two") + + assertEquals(listOf("two"), testObject.categoryList) + } +} \ No newline at end of file diff --git a/dependency-injection.md b/dependency-injection.md index 5d3599e54..f29d7b603 100644 --- a/dependency-injection.md +++ b/dependency-injection.md @@ -1,6 +1,6 @@ ## Overview -At its core, dependency injection is just the principle of `"tell, dont ask"` put into practice; for instance, if a class needs to use the `MediaWikiApi`, it should be handed an instance of the classs rather than reaching out to get it. This has the effect of decoupling code, making it easier to test and reuse. +At its core, dependency injection is just the principle of `"tell, don't ask"` put into practice; for instance, if a class needs to use the `MediaWikiApi`, it should be handed an instance of the class rather than reaching out to get it. This has the effect of decoupling code, making it easier to test and reuse. ## Dependency Injection in the Commons app diff --git a/gradle.properties b/gradle.properties index 850003852..05aa34949 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,17 +14,17 @@ # org.gradle.parallel=true #Thu Mar 01 15:28:48 IST 2018 systemProp.http.proxyPort=0 -compileSdkVersion=android-26 +compileSdkVersion=android-27 android.useDeprecatedNdk=true BUTTERKNIFE_VERSION=8.6.0 org.gradle.jvmargs=-Xmx1536M -buildToolsVersion=26.0.2 -targetSdkVersion=25 +buildToolsVersion=27.0.0 +targetSdkVersion=27 #TODO: Temporary disabled. https://developer.android.com/studio/build/gradle-plugin-3-0-0-migration.html#aapt2 #Refer to PR: https://github.com/commons-app/apps-android-commons/pull/932 android.enableAapt2=false -SUPPORT_LIB_VERSION=26.0.2 +SUPPORT_LIB_VERSION=27.1.1 minSdkVersion=15 systemProp.http.proxyHost= LEAK_CANARY=1.5.4 diff --git a/script/style/checkstyle.xml b/script/style/checkstyle.xml index cb0b13dca..216d1bce2 100644 --- a/script/style/checkstyle.xml +++ b/script/style/checkstyle.xml @@ -7,6 +7,8 @@ Modified from https://github.com/checkstyle/checkstyle/blob/master/src/main/resources/google_checks.xml. Modifications are: - doubled each value in Indentation + - exceptions for Butter Knife and Espresso + - larger (max)LineLength Checkstyle configuration that checks the Google coding conventions from Google Java Style that can be found at https://google.github.io/styleguide/javaguide.html. @@ -44,7 +46,7 @@ - + @@ -56,7 +58,7 @@ - +