[GSoC] Master rebase. (#4505)

* Localisation updates from https://translatewiki.net.

* Fixes #4357  After switching to different account, contributions screen shows pictures of previous account (#4421)

* Update UploadMediaDetailFragment.java

* Update LoginActivity.java

Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account

* Revert "Update UploadMediaDetailFragment.java"

This reverts commit b1b4257f20.

Co-authored-by: Obsidian_zero <1198474846@qq.com>

* Remove unnecessary whitespace from a message (#4439)

* Merge v3.0.1 into master (#4446)

* Versioning and changelog for v3.0.0 (#4152)

* Versioning for v3.0.0

* Update changelog.md

* Handled migration 8-9-10 in BookmarksLocationDao (#4154)

* #Fixes #4141
- Handled migrations for BookmarkLocationsDao from 8-9-10

* #Fixes #4141
- Handled migrations for BookmarkLocationsDao from 8-9-10

* Fixes #4179 (#4180)

* Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked()
* Updated >ContributionsFragment#backButtonClicked() to handle back press properly

* Fixes #4179 (#4181)

* Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed()

* Cherrypick for hotfix3.1 (#4205)

* Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list.

Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list.

* fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146)

* fixed bug:app crashes on viewing review in Review Fragment #4135

* Fixed the issue with back button in contribution tab. (#4177)

Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>

* Fixed the issue with back navigation button on toolbar in explore tab. (#4175)

* Fix (#4148) Issues on theme change

* fixed themeChange crashes

* fixed comments

* Overlooked the title bar

Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com>
Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com>
Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>
Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com>

* Fixes #4173 (#4396)

* Fix #4147  Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390)

* Update query to fetch descriptions

* Make description added to NearbyResultItem

* Make string operations to display description and label in a combined way

* Fix reviews, remove long description from list and swap label and description texts

* Fix repeated information issue

* Fix double information issue

* fix style issues

* Remove douplicated information

* Changes made (#4354)

* Remove nonexistent method

* Fix #4283 IllegalStateException (#4440)

* Fix #4283 IllegalStateException

* Fix flickering issue

* Versioning for v3.0.1

* Update changelog.md

Co-authored-by: Ashish <ashishkumar468@gmail.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com>
Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com>
Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>
Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com>
Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com>
Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com>

* Localisation updates from https://translatewiki.net.

* Added a feature for editing coordinates (#4418)

* not

* Place Picker added

* Pick location and API call linked

* minor warnings resolved

* Code conventions followed

* issue fixed

* Wikitext edited properly

* minor modification

* Location Picker added

* Bottom sheet removed

* Location picker fully implemented

* credit added

* credit added

* issues fixed

* issues fixed

* minor issue fixed

* Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451)

* Localisation updates from https://translatewiki.net.

* Fixes 4344 - Duplicate Uploads (#4442)

* Fixes 4344
- Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries.

* Update states to IN_PROGRESS before uploads are processed

* Fixes #3694 Pre-select places as depictions (#4452)

* WikidataEditService: stop automatically adding WikidataPlace as a depiction

When the user initiates the upload process from Nearby and also manually adds the place as a depiction, the depiction is added twice. Since this behavior is invisible to the user, it is being removed in preparation for auto-selecting the place as a depiction on the DepictsFragment screen.

* DepictsFragment: auto-select place as a depiction

Pass the Place reference from UploadActivity to DepictsFragment and select the corresponding DepictedItem. Using the place id, retrieve the corresponding Entity to create and select a DepictedItem.

* UploadRepository: use Place from UploadItem to obtain a DepictedItem

Instead of passing a Place object from UploadActivity to DepictsFragment and then passing the Place object up the chain to obtain and select a DepictedItem, retrieve the Place object directly within UploadRepository

* DepictsFragment: select Place depiction when fragment becomes visible

* UploadDepictsAdapter: make adapter aware of selection state

Update selection state when recycled list items are automatically selected, preventing automatically selected items from appearing as unselected until they are forced to re-bind (i.e. after scrolling)

* DepictsFragment: pre-select place depictions for all UploadItems

If several images are selected and set to different places, pre-select all place depictions to reinforce the intended upload workflow philosophy (i.e. all images in a set are intended to be from/of the same place). See discussion in commons-app/apps-android-commons#3694

* DepictsFragment: scroll to the top every time list is updated

* Typo fixes (#4461)

* Fixed typo on class documentation of TextUtils

* corrected comma placement in documentation

* Fixed typos in comments

* fix-issue-4424 (#4445)

Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>

* fix edit categories ui (#4414)

Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>

* Fix doom version issue (#4463)

* Update db version

* DBOpenHelper version update

* fix :Back Pressed Event not work in Explore tab when user not login (#4404)

* fix :Back Pressed Event not work in Explore tab

* minor changes

* fix :Upload count or number of contribution does not get updated when media  is successful uploaded (#4399)

* * fix:Number of Contributions not updated
 * Add javadocs

* minor changes

* made minor changes

* String was nonsense and untranslatible, fixed (#4466)

* Ability to show captions and descriptions in all entered languages (#4355)

* implement Ability to show captions and descriptions in all entered languages
*Add Javadoc

* handle Back event of fragment(mediaDetailFragment)

* fix minor bugs

* add internationalization

* revert previous changes

* fix visibility bug

* resolve conflict

* Fixes #4437 - Changed indentation on files with 2 spaces to 4 spaces (#4462)

* Edited Project.xml to make indent size 4

* Changed files with 2 space indentation to use 4 space indentation

* Edited Project.xml to make indent size 4

* changed files with 2 space indent to 4 space indent

* fix :Back Pressed Event not work in Explore tab when user not login (#4404)

* fix :Back Pressed Event not work in Explore tab

* minor changes

* fix :Upload count or number of contribution does not get updated when media  is successful uploaded (#4399)

* * fix:Number of Contributions not updated
 * Add javadocs

* minor changes

* made minor changes

* String was nonsense and untranslatible, fixed (#4466)

* Ability to show captions and descriptions in all entered languages (#4355)

* implement Ability to show captions and descriptions in all entered languages
*Add Javadoc

* handle Back event of fragment(mediaDetailFragment)

* fix minor bugs

* add internationalization

* revert previous changes

* fix visibility bug

* resolve conflict

Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>

* Use more understandable strings (#4470)

* Fix #3792 Missing Column Issue (#4468)

* Fix Missing Column Issue

* Fix tests

* Add UploadCategoriesFragment Unit Tests (#4473)

* Panorama (#4467)

* panoramic images fixed

* made requested changes

* Minor refactoring

Co-authored-by: Aditya Srivastava <iamaditya2009@gmail.com>

* Localisation updates from https://translatewiki.net.

* Main activity title is sometimes "Contributions", sometimes "Commons" (#4472)

Fixes #4438  Replace == with equals() in onRestoreInstanceState

* Localisation updates from https://translatewiki.net.

* caption and description copyable (#4481)

* Removed next button in quiz (#4382)

* issues resolved

* modification done

* warning fixed

* issues resolved

* Button added

* don't know function added

* Button added

* modification done

* modification done

* Localisation updates from https://translatewiki.net.

* Added option to show and modify location while uploading (#4475)

* initial commit

* Everything done

* minor modification

* minor modification

* Issues fixed

* minor modifications

* issue fixed

* Issues fixed

* Tutorial removed from log out state (#4479)

* tutorial removed from log out state

* Issue removed

* Update changelog.md

* Versioning for v3.0.2

* Fix #4482 (#4484)

* Fix crash when image resolution is very high (#4483)

* Localisation updates from https://translatewiki.net.

* Add Contributions Fragment Unit Tests (#4490)

* Fix Tests Errors (#4491)

* Add UploadMediaDetailFragment Unit Tests (#4492)

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Localisation updates from https://translatewiki.net.

* Initialised xmls, made folder and image item.

* xmls done

* xmls completed

* removed unwanted attribute

* Created models, adapters and view models (#4441)

* created models, adapters and view models

* Added Image Fragment

* back button linked

* Documentation and refractor

* spaces

* Butterknife annotation

* DiffUtil

* Added Examples

* Extended Custom selector From Base Activity

* made view model injectable

* [GSOC] Added Image Fetch (#4449)

* Added basic Fetch

* added permission request

* Folder count rectified

* Loaded thumbnail

* disabled overlay

* Added sha1 function

* Documented the code

* [GSoC] Image Selection (#4457)

* Localisation updates from https://translatewiki.net.

* Fixes #4357  After switching to different account, contributions screen shows pictures of previous account (#4421)

* Update UploadMediaDetailFragment.java

* Update LoginActivity.java

Clear CompositeDisposable after logging in successfully. It may help solve the problem of saving the contribution to the previous account

* Revert "Update UploadMediaDetailFragment.java"

This reverts commit b1b4257f20.

Co-authored-by: Obsidian_zero <1198474846@qq.com>

* Remove unnecessary whitespace from a message (#4439)

* Merge v3.0.1 into master (#4446)

* Versioning and changelog for v3.0.0 (#4152)

* Versioning for v3.0.0

* Update changelog.md

* Handled migration 8-9-10 in BookmarksLocationDao (#4154)

* #Fixes #4141
- Handled migrations for BookmarkLocationsDao from 8-9-10

* #Fixes #4141
- Handled migrations for BookmarkLocationsDao from 8-9-10

* Fixes #4179 (#4180)

* Handled null pointer exception in MainActivity->ContributionsFragment#backButtonClicked()
* Updated >ContributionsFragment#backButtonClicked() to handle back press properly

* Fixes #4179 (#4181)

* Handled possible null check on MediaDetails in BookmarkListRootFragment#backPressed()

* Cherrypick for hotfix3.1 (#4205)

* Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list.

Fixes #4159 On Explore Tab, All Available Options on toolbar in media detail view are only targeting the first media in the list.

* fixed bug: App crashes on viewing review in Review Fragment #4132 (#4146)

* fixed bug:app crashes on viewing review in Review Fragment #4135

* Fixed the issue with back button in contribution tab. (#4177)

Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>

* Fixed the issue with back navigation button on toolbar in explore tab. (#4175)

* Fix (#4148) Issues on theme change

* fixed themeChange crashes

* fixed comments

* Overlooked the title bar

Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com>
Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com>
Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>
Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com>

* Fixes #4173 (#4396)

* Fix #4147  Pre-fill desc in Nearby uploads with Wikidata item's label + description (#4390)

* Update query to fetch descriptions

* Make description added to NearbyResultItem

* Make string operations to display description and label in a combined way

* Fix reviews, remove long description from list and swap label and description texts

* Fix repeated information issue

* Fix double information issue

* fix style issues

* Remove douplicated information

* Changes made (#4354)

* Remove nonexistent method

* Fix #4283 IllegalStateException (#4440)

* Fix #4283 IllegalStateException

* Fix flickering issue

* Versioning for v3.0.1

* Update changelog.md

Co-authored-by: Ashish <ashishkumar468@gmail.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com>
Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com>
Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>
Co-authored-by: Aditya-Srivastav <54016427+4D17Y4@users.noreply.github.com>
Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com>
Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com>

* Localisation updates from https://translatewiki.net.

* Added basic Fetch

* added permission request

* Folder count rectified

* Loaded thumbnail

* disabled overlay

* Added sha1 function

* Documented the code

* Added a feature for editing coordinates (#4418)

* not

* Place Picker added

* Pick location and API call linked

* minor warnings resolved

* Code conventions followed

* issue fixed

* Wikitext edited properly

* minor modification

* Location Picker added

* Bottom sheet removed

* Location picker fully implemented

* credit added

* credit added

* issues fixed

* issues fixed

* minor issue fixed

* Some build issues occured merging release v3.0 are fixed. One paranthesis issue is solved, a method about UploadService is removed, since we don't use it anymore. (#4451)

* Localisation updates from https://translatewiki.net.

* Fixes 4344 - Duplicate Uploads (#4442)

* Fixes 4344
- Update the retention policy of the Work Manager to ExistingWorkPolicy.APPEND_OR_REPLACE- which would append the new work to the end of existing one. This helps remove the while loop in UploadWorker which was meant to handle the cases where a new worker would be created for retries. The while loop seemed to have race conditions uploading duplicate entries.

* Update states to IN_PROGRESS before uploads are processed

* Image selection added

* Forwarded activity result to upload wizard

* Initialised xmls, made folder and image item.

* xmls done

* xmls completed

* removed unwanted attribute

* Created models, adapters and view models (#4441)

* created models, adapters and view models

* Added Image Fragment

* back button linked

* Documentation and refractor

* spaces

* Butterknife annotation

* DiffUtil

* Added Examples

* Extended Custom selector From Base Activity

* made view model injectable

* Added basic Fetch

* added permission request

* Folder count rectified

* Loaded thumbnail

* disabled overlay

* Added sha1 function

* Documented the code

* Image selection added

* Forwarded activity result to upload wizard

* [GSOC] Added Image Fetch (#4449)

* Added basic Fetch

* added permission request

* Folder count rectified

* Loaded thumbnail

* disabled overlay

* Added sha1 function

* Documented the code

* fixed merge errors

* Documented the remaining function

Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net>
Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com>
Co-authored-by: Obsidian_zero <1198474846@qq.com>
Co-authored-by: Amir E. Aharoni <amir.aharoni@mail.huji.ac.il>
Co-authored-by: Josephine Lim <josephinelim86@gmail.com>
Co-authored-by: Ashish <ashishkumar468@gmail.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com>
Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com>
Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>
Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com>
Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com>
Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com>

* [GSoC] Show uploaded images differently. (#4464)

* uploaded images shown differently

* Loaded images before query

* Handled exceptions, Made ImageLoader injectable, Document and clean code

* [GSoC] Added Uploaded status table in room database. (#4476)

* added Uploaded status table in room database

* Added unique property, minor refractoring

* Database intigrated

* Database integrated

* Handled result null exception

* Exceptions handled and refractored

* Introduced constants

* moved to sealed class

* No database insert on network error

* queried original image

* documented the code

* Updated uploaded status on upload success

* Image Helper test (#4485)

* [GSoC] Adapter Tests (#4488)

* Added FolderAdapterTest

* Image Adapter Test

* merge fix

* rebase fix

Co-authored-by: translatewiki.net <l10n-bot@translatewiki.net>
Co-authored-by: obsidian-zero <63155026+obsidian-zero@users.noreply.github.com>
Co-authored-by: Obsidian_zero <1198474846@qq.com>
Co-authored-by: Amir E. Aharoni <amir.aharoni@mail.huji.ac.il>
Co-authored-by: Josephine Lim <josephinelim86@gmail.com>
Co-authored-by: Ashish <ashishkumar468@gmail.com>
Co-authored-by: neslihanturan <tur.neslihan@gmail.com>
Co-authored-by: Pratham Pahariya <54663429+Pratham2305@users.noreply.github.com>
Co-authored-by: Shabir Ahmad <56585337+shabar-shab@users.noreply.github.com>
Co-authored-by: Pratham2305 <Pratham2305@users.noreply.github.com>
Co-authored-by: Madhur Gupta <30932899+madhurgupta10@users.noreply.github.com>
Co-authored-by: Vinayak Aggarwal <56196007+vinayak0505@users.noreply.github.com>
Co-authored-by: Ayan Sarkar <71203077+Ayan-10@users.noreply.github.com>
Co-authored-by: Brigham Byerly <6891883+byerlyb20@users.noreply.github.com>
Co-authored-by: Jamie Brown <jamiejbrown521@gmail.com>
Co-authored-by: Prince kushwaha <65972015+Prince-kushwaha@users.noreply.github.com>
Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
Co-authored-by: Ashar <asharalikhan200@gmail.com>
This commit is contained in:
Aditya-Srivastav 2021-07-14 16:35:08 +05:30 committed by GitHub
parent 93dae58b06
commit 7336881705
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
112 changed files with 5590 additions and 3331 deletions

View file

@ -111,7 +111,6 @@
<option name="WHILE_BRACE_FORCE" value="3" />
<option name="FOR_BRACE_FORCE" value="3" />
<indentOptions>
<option name="INDENT_SIZE" value="2" />
<option name="CONTINUATION_INDENT_SIZE" value="4" />
<option name="TAB_SIZE" value="2" />
</indentOptions>
@ -325,4 +324,4 @@
</indentOptions>
</codeStyleSettings>
</code_scheme>
</component>
</component>

View file

@ -1,5 +1,9 @@
# Wikimedia Commons for Android
## v3.0.2
- Fixed crash when uploading high res image
- Fixed crash when viewing images in Explore
## v3.0.1
- Pre-fill desc in Nearby uploads with Wikidata item's label + description
- Improved ACRA crash reporting

View file

@ -154,8 +154,8 @@ android {
defaultConfig {
//applicationId 'fr.free.nrw.commons'
versionCode 1016
versionName '3.0.1'
versionCode 1021
versionName '3.0.2'
setProperty("archivesBaseName", "app-commons-v$versionName-" + getBranchName())
minSdkVersion 19

View file

@ -57,6 +57,7 @@
<activity android:name=".WelcomeActivity" />
<activity
android:hardwareAccelerated="false"
android:name=".upload.UploadActivity"
android:configChanges="orientation|screenSize|keyboard"
android:icon="@mipmap/ic_launcher"

View file

@ -63,38 +63,39 @@ import org.wikipedia.language.AppLanguageLookUpTable;
import timber.log.Timber;
@AcraCore(
buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
reportFormat = StringFormat.KEY_VALUE_LIST,
reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL, STACK_TRACE}
buildConfigClass = BuildConfig.class,
resReportSendSuccessToast = R.string.crash_dialog_ok_toast,
reportFormat = StringFormat.KEY_VALUE_LIST,
reportContent = {USER_COMMENT, APP_VERSION_CODE, APP_VERSION_NAME, ANDROID_VERSION, PHONE_MODEL,
STACK_TRACE}
)
@AcraMailSender(
mailTo = "commons-app-android-private@googlegroups.com",
reportAsFile = false
mailTo = "commons-app-android-private@googlegroups.com",
reportAsFile = false
)
@AcraDialog(
resTheme = R.style.Theme_AppCompat_Dialog,
resText = R.string.crash_dialog_text,
resTitle = R.string.crash_dialog_title,
resCommentPrompt = R.string.crash_dialog_comment_prompt
resTheme = R.style.Theme_AppCompat_Dialog,
resText = R.string.crash_dialog_text,
resTitle = R.string.crash_dialog_title,
resCommentPrompt = R.string.crash_dialog_comment_prompt
)
public class CommonsApplication extends MultiDexApplication {
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
@Inject
SessionManager sessionManager;
@Inject
DBOpenHelper dbOpenHelper;
public static final String IS_LIMITED_CONNECTION_MODE_ENABLED = "is_limited_connection_mode_enabled";
@Inject
SessionManager sessionManager;
@Inject
DBOpenHelper dbOpenHelper;
@Inject
@Named("default_preferences")
JsonKvStore defaultPrefs;
@Inject
@Named("default_preferences")
JsonKvStore defaultPrefs;
@Inject
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
@Inject
CustomOkHttpNetworkFetcher customOkHttpNetworkFetcher;
/**
* Constants begin
@ -118,23 +119,26 @@ public class CommonsApplication extends MultiDexApplication {
private RefWatcher refWatcher;
private static CommonsApplication INSTANCE;
public static CommonsApplication getInstance() {
return INSTANCE;
}
private AppLanguageLookUpTable languageLookUpTable;
public AppLanguageLookUpTable getLanguageLookUpTable() {
return languageLookUpTable;
}
@Inject ContributionDao contributionDao;
@Inject
ContributionDao contributionDao;
/**
* In memory list of contributios whose uploads ahve been paused by the user
*/
public static Map<String, Boolean> pauseUploads = new HashMap<>();
/**
* In memory list of contributios whose uploads ahve been paused by the user
*/
public static Map<String, Boolean> pauseUploads = new HashMap<>();
/**
/**
* Used to declare and initialize various components and dependencies
*/
@Override
@ -146,15 +150,14 @@ public class CommonsApplication extends MultiDexApplication {
Mapbox.getInstance(this, getString(R.string.mapbox_commons_app_token));
ApplicationlessInjection
.getInstance(this)
.getCommonsApplicationComponent()
.inject(this);
.getInstance(this)
.getCommonsApplicationComponent()
.inject(this);
AppAdapter.set(new CommonsAppAdapter(sessionManager, defaultPrefs));
initTimber();
if (!defaultPrefs.getBoolean("has_user_manually_removed_location")) {
Set<String> defaultExifTagsSet = defaultPrefs.getStringSet(Prefs.MANAGED_EXIF_TAGS);
if (null == defaultExifTagsSet) {
@ -166,9 +169,9 @@ public class CommonsApplication extends MultiDexApplication {
// Set DownsampleEnabled to True to downsample the image in case it's heavy
ImagePipelineConfig config = ImagePipelineConfig.newBuilder(this)
.setNetworkFetcher(customOkHttpNetworkFetcher)
.setDownsampleEnabled(true)
.build();
.setNetworkFetcher(customOkHttpNetworkFetcher)
.setDownsampleEnabled(true)
.build();
try {
Fresco.initialize(this, config);
} catch (Exception e) {
@ -192,46 +195,44 @@ public class CommonsApplication extends MultiDexApplication {
}
/**
* Plants debug and file logging tree.
* Timber lets you plant your own logging trees.
*
* Plants debug and file logging tree. Timber lets you plant your own logging trees.
*/
private void initTimber() {
boolean isBeta = ConfigUtils.isBetaFlavour();
String logFileName =
isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs";
String logDirectory = LogUtils.getLogDirectory();
//Delete stale logs if they have exceeded the specified size
deleteStaleLogs(logFileName, logDirectory);
boolean isBeta = ConfigUtils.isBetaFlavour();
String logFileName =
isBeta ? "CommonsBetaAppLogs" : "CommonsAppLogs";
String logDirectory = LogUtils.getLogDirectory();
//Delete stale logs if they have exceeded the specified size
deleteStaleLogs(logFileName, logDirectory);
FileLoggingTree tree = new FileLoggingTree(
Log.VERBOSE,
logFileName,
logDirectory,
1000,
getFileLoggingThreadPool());
FileLoggingTree tree = new FileLoggingTree(
Log.VERBOSE,
logFileName,
logDirectory,
1000,
getFileLoggingThreadPool());
Timber.plant(tree);
Timber.plant(new Timber.DebugTree());
Timber.plant(tree);
Timber.plant(new Timber.DebugTree());
}
/**
* Deletes the logs zip file at the specified directory and file locations specified in the
* params
*
* @param logFileName
* @param logDirectory
*/
private void deleteStaleLogs(String logFileName, String logDirectory) {
try {
File file = new File(logDirectory + "/zip/" + logFileName + ".zip");
if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs
file.delete();
}
} catch (Exception e) {
Timber.e(e);
/**
* Deletes the logs zip file at the specified directory and file locations specified in the
* params
*
* @param logFileName
* @param logDirectory
*/
private void deleteStaleLogs(String logFileName, String logDirectory) {
try {
File file = new File(logDirectory + "/zip/" + logFileName + ".zip");
if (file.exists() && file.getTotalSpace() > 1000000) {// In Kbs
file.delete();
}
} catch (Exception e) {
Timber.e(e);
}
}
}
public static boolean isRoboUnitTest() {
return "robolectric".equals(Build.FINGERPRINT);
@ -239,30 +240,35 @@ public class CommonsApplication extends MultiDexApplication {
private ThreadPoolService getFileLoggingThreadPool() {
return new ThreadPoolService.Builder("file-logging-thread")
.setPriority(Process.THREAD_PRIORITY_LOWEST)
.setPoolSize(1)
.setExceptionHandler(new BackgroundPoolExceptionHandler())
.build();
.setPriority(Process.THREAD_PRIORITY_LOWEST)
.setPoolSize(1)
.setExceptionHandler(new BackgroundPoolExceptionHandler())
.build();
}
public static void createNotificationChannel(@NonNull Context context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = manager.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL);
NotificationManager manager = (NotificationManager) context
.getSystemService(Context.NOTIFICATION_SERVICE);
NotificationChannel channel = manager
.getNotificationChannel(NOTIFICATION_CHANNEL_ID_ALL);
if (channel == null) {
channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID_ALL,
context.getString(R.string.notifications_channel_name_all), NotificationManager.IMPORTANCE_DEFAULT);
context.getString(R.string.notifications_channel_name_all),
NotificationManager.IMPORTANCE_DEFAULT);
manager.createNotificationChannel(channel);
}
}
}
public String getUserAgent() {
return "Commons/" + ConfigUtils.getVersionNameWithSha(this) + " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
return "Commons/" + ConfigUtils.getVersionNameWithSha(this)
+ " (https://mediawiki.org/wiki/Apps/Commons) Android/" + Build.VERSION.RELEASE;
}
/**
* Helps in setting up LeakCanary library
*
* @return instance of LeakCanary
*/
protected RefWatcher setupLeakCanary() {
@ -272,7 +278,7 @@ public class CommonsApplication extends MultiDexApplication {
return LeakCanary.install(this);
}
/**
/**
* Provides a way to get member refWatcher
*
* @param context Application context
@ -285,7 +291,8 @@ public class CommonsApplication extends MultiDexApplication {
/**
* clears data of current application
* @param context Application context
*
* @param context Application context
* @param logoutListener Implementation of interface LogoutListener
*/
@SuppressLint("CheckResult")
@ -302,13 +309,13 @@ public class CommonsApplication extends MultiDexApplication {
}
sessionManager.logout()
.andThen(Completable.fromAction(() ->{
Timber.d("All accounts have been removed");
clearImageCache();
//TODO: fix preference manager
defaultPrefs.clearAll();
defaultPrefs.putBoolean("firstrun", false);
updateAllDatabases();
.andThen(Completable.fromAction(() -> {
Timber.d("All accounts have been removed");
clearImageCache();
//TODO: fix preference manager
defaultPrefs.clearAll();
defaultPrefs.putBoolean("firstrun", false);
updateAllDatabases();
}
))
.subscribeOn(Schedulers.io())
@ -332,12 +339,13 @@ public class CommonsApplication extends MultiDexApplication {
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
CategoryDao.Table.onDelete(db);
dbOpenHelper.deleteTable(db,CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
dbOpenHelper.deleteTable(db,
CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
try {
contributionDao.deleteAll();
contributionDao.deleteAll();
} catch (SQLiteException e) {
Timber.e(e);
Timber.e(e);
}
BookmarkPicturesDao.Table.onDelete(db);
BookmarkLocationsDao.Table.onDelete(db);
@ -348,6 +356,7 @@ public class CommonsApplication extends MultiDexApplication {
* Interface used to get log-out events
*/
public interface LogoutListener {
void onLogoutComplete();
}
}

View file

@ -2,7 +2,6 @@ package fr.free.nrw.commons.LocationPicker;
import android.app.Activity;
import android.content.Intent;
import androidx.annotation.NonNull;
import com.mapbox.mapboxsdk.camera.CameraPosition;
/**
@ -10,45 +9,57 @@ import com.mapbox.mapboxsdk.camera.CameraPosition;
*/
public final class LocationPicker {
/**
* Getting camera position from the intent using constants
* @param data intent
* @return CameraPosition
*/
public static CameraPosition getCameraPosition(final Intent data) {
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
}
public static class IntentBuilder {
private final Intent intent;
/**
* Creates a new builder that creates an intent to launch the place picker activity.
* Getting camera position from the intent using constants
*
* @param data intent
* @return CameraPosition
*/
public IntentBuilder() {
intent = new Intent();
public static CameraPosition getCameraPosition(final Intent data) {
return data.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
}
/**
* Gets and puts location in intent
* @param position CameraPosition
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder defaultLocation(
final CameraPosition position) {
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
return this;
}
public static class IntentBuilder {
/**
* Gets and sets the activity
* @param activity Activity
* @return Intent
*/
public Intent build(final Activity activity) {
intent.setClass(activity, LocationPickerActivity.class);
return intent;
private final Intent intent;
/**
* Creates a new builder that creates an intent to launch the place picker activity.
*/
public IntentBuilder() {
intent = new Intent();
}
/**
* Gets and puts location in intent
* @param position CameraPosition
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder defaultLocation(
final CameraPosition position) {
intent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION, position);
return this;
}
/**
* Gets and puts activity name in intent
* @param activity activity key
* @return LocationPicker.IntentBuilder
*/
public LocationPicker.IntentBuilder activityKey(
final String activity) {
intent.putExtra(LocationPickerConstants.ACTIVITY_KEY, activity);
return this;
}
/**
* Gets and sets the activity
* @param activity Activity
* @return Intent
*/
public Intent build(final Activity activity) {
intent.setClass(activity, LocationPickerActivity.class);
return intent;
}
}
}
}

View file

@ -1,12 +1,23 @@
package fr.free.nrw.commons.LocationPicker;
import static com.mapbox.mapboxsdk.style.layers.Property.NONE;
import static com.mapbox.mapboxsdk.style.layers.Property.VISIBLE;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconAllowOverlap;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconIgnorePlacement;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.iconImage;
import static com.mapbox.mapboxsdk.style.layers.PropertyFactory.visibility;
import android.content.Intent;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.text.Html;
import android.text.method.LinkMovementMethod;
import android.view.View;
import android.view.Window;
import android.view.animation.OvershootInterpolator;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
@ -17,6 +28,7 @@ import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import com.google.android.material.floatingactionbutton.FloatingActionButton;
import com.mapbox.android.core.permissions.PermissionsManager;
import com.mapbox.geojson.Point;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import com.mapbox.mapboxsdk.camera.CameraPosition.Builder;
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory;
@ -32,7 +44,11 @@ import com.mapbox.mapboxsdk.maps.MapboxMap.OnCameraMoveStartedListener;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Style;
import com.mapbox.mapboxsdk.maps.UiSettings;
import com.mapbox.mapboxsdk.style.layers.Layer;
import com.mapbox.mapboxsdk.style.layers.SymbolLayer;
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import org.jetbrains.annotations.NotNull;
import timber.log.Timber;
@ -42,247 +58,359 @@ import timber.log.Timber;
public class LocationPickerActivity extends AppCompatActivity implements OnMapReadyCallback,
OnCameraMoveStartedListener, OnCameraIdleListener, Observer<CameraPosition> {
/**
* cameraPosition : position of picker
*/
private CameraPosition cameraPosition;
/**
* markerImage : picker image
*/
private ImageView markerImage;
/**
* mapboxMap : map
*/
private MapboxMap mapboxMap;
/**
* mapView : view of the map
*/
private MapView mapView;
/**
* tvAttribution : credit
*/
private AppCompatTextView tvAttribution;
/**
* DROPPED_MARKER_LAYER_ID : id for layer
*/
private static final String DROPPED_MARKER_LAYER_ID = "DROPPED_MARKER_LAYER_ID";
/**
* cameraPosition : position of picker
*/
private CameraPosition cameraPosition;
/**
* markerImage : picker image
*/
private ImageView markerImage;
/**
* mapboxMap : map
*/
private MapboxMap mapboxMap;
/**
* mapView : view of the map
*/
private MapView mapView;
/**
* tvAttribution : credit
*/
private AppCompatTextView tvAttribution;
/**
* activity : activity key
*/
private String activity;
/**
* modifyLocationButton : button for start editing location
*/
Button modifyLocationButton;
/**
* showInMapButton : button for showing in map
*/
TextView showInMapButton;
/**
* placeSelectedButton : fab for selecting location
*/
FloatingActionButton placeSelectedButton;
/**
* droppedMarkerLayer : Layer for static screen
*/
private Layer droppedMarkerLayer;
/**
* shadow : imageview of shadow
*/
private ImageView shadow;
/**
* largeToolbarText : textView of shadow
*/
private TextView largeToolbarText;
/**
* smallToolbarText : textView of shadow
*/
private TextView smallToolbarText;
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@Override
protected void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
setContentView(R.layout.activity_location_picker);
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
final ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.hide();
}
setContentView(R.layout.activity_location_picker);
if (savedInstanceState == null) {
cameraPosition = getIntent().getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
if (savedInstanceState == null) {
cameraPosition = getIntent()
.getParcelableExtra(LocationPickerConstants.MAP_CAMERA_POSITION);
activity = getIntent().getStringExtra(LocationPickerConstants.ACTIVITY_KEY);
}
final LocationPickerViewModel viewModel = new ViewModelProvider(this)
.get(LocationPickerViewModel.class);
viewModel.getResult().observe(this, this);
bindViews();
addBackButtonListener();
addPlaceSelectedButton();
addCredits();
getToolbarUI();
if (activity.equals("UploadActivity")) {
placeSelectedButton.setVisibility(View.GONE);
modifyLocationButton.setVisibility(View.VISIBLE);
showInMapButton.setVisibility(View.VISIBLE);
largeToolbarText.setText(getResources().getString(R.string.image_location));
smallToolbarText.setText(getResources().
getString(R.string.check_whether_location_is_correct));
}
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(this);
}
final LocationPickerViewModel viewModel = new ViewModelProvider(this)
.get(LocationPickerViewModel.class);
viewModel.getResult().observe(this, this);
bindViews();
addBackButtonListener();
addPlaceSelectedButton();
addCredits();
getToolbarUI();
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(this);
}
/**
* For showing credits
*/
private void addCredits() {
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
}
/**
* Clicking back button destroy locationPickerActivity
*/
private void addBackButtonListener() {
final ImageView backButton = findViewById(R.id.mapbox_place_picker_toolbar_back_button);
backButton.setOnClickListener(view -> finish());
}
/**
* Binds mapView and location picker icon
*/
private void bindViews() {
mapView = findViewById(R.id.map_view);
markerImage = findViewById(R.id.location_picker_image_view_marker);
tvAttribution = findViewById(R.id.tv_attribution);
}
/**
* Binds the listeners
*/
private void bindListeners() {
mapboxMap.addOnCameraMoveStartedListener(
this);
mapboxMap.addOnCameraIdleListener(
this);
}
/**
* Gets toolbar color
*/
private void getToolbarUI() {
final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
}
/**
* Takes action when map is ready to show
* @param mapboxMap map
*/
@Override
public void onMapReady(final MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
adjustCameraBasedOnOptions();
bindListeners();
enableLocationComponent(style);
});
}
/**
* move the location to the current media coordinates
*/
private void adjustCameraBasedOnOptions() {
mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
/**
* Enables location components
* @param loadedMapStyle Style
*/
@SuppressWarnings( {"MissingPermission"})
private void enableLocationComponent(@NonNull final Style loadedMapStyle) {
final UiSettings uiSettings = mapboxMap.getUiSettings();
uiSettings.setAttributionEnabled(false);
// Check if permissions are enabled and if not request
if (PermissionsManager.areLocationPermissionsGranted(this)) {
// Get an instance of the component
final LocationComponent locationComponent = mapboxMap.getLocationComponent();
// Activate with options
locationComponent.activateLocationComponent(
LocationComponentActivationOptions.builder(this, loadedMapStyle).build());
// Enable to make component visible
locationComponent.setLocationComponentEnabled(true);
// Set the component's camera mode
locationComponent.setCameraMode(CameraMode.NONE);
// Set the component's render mode
locationComponent.setRenderMode(RenderMode.NORMAL);
/**
* For showing credits
*/
private void addCredits() {
tvAttribution.setText(Html.fromHtml(getString(R.string.map_attribution)));
tvAttribution.setMovementMethod(LinkMovementMethod.getInstance());
}
}
/**
* Acts on camera moving
* @param reason int
*/
@Override
public void onCameraMoveStarted(final int reason) {
Timber.v("Map camera has begun moving.");
if (markerImage.getTranslationY() == 0) {
markerImage.animate().translationY(-75)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
/**
* Clicking back button destroy locationPickerActivity
*/
private void addBackButtonListener() {
final ImageView backButton = findViewById(R.id.mapbox_place_picker_toolbar_back_button);
backButton.setOnClickListener(view -> finish());
}
}
/**
* Acts on camera idle
*/
@Override
public void onCameraIdle() {
Timber.v("Map camera is now idling.");
markerImage.animate().translationY(0)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
/**
* Takes action on camera position
* @param position position of picker
*/
@Override
public void onChanged(@Nullable CameraPosition position) {
if (position == null) {
position = new Builder()
.target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(),
mapboxMap.getCameraPosition().target.getLongitude()))
.zoom(16).build();
/**
* Binds mapView and location picker icon
*/
private void bindViews() {
mapView = findViewById(R.id.map_view);
markerImage = findViewById(R.id.location_picker_image_view_marker);
tvAttribution = findViewById(R.id.tv_attribution);
modifyLocationButton = findViewById(R.id.modify_location);
showInMapButton = findViewById(R.id.show_in_map);
showInMapButton.setText(getResources().getString(R.string.show_in_map_app).toUpperCase());
shadow = findViewById(R.id.location_picker_image_view_shadow);
}
cameraPosition = position;
}
/**
* Select the preferable location
*/
private void addPlaceSelectedButton() {
final FloatingActionButton placeSelectedButton = findViewById(R.id.location_chosen_button);
placeSelectedButton.setOnClickListener(view -> placeSelected());
}
/**
* Binds the listeners
*/
private void bindListeners() {
mapboxMap.addOnCameraMoveStartedListener(
this);
mapboxMap.addOnCameraIdleListener(
this);
}
/**
* Return the intent with required data
*/
void placeSelected() {
final Intent returningIntent = new Intent();
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
mapboxMap.getCameraPosition());
setResult(AppCompatActivity.RESULT_OK, returningIntent);
finish();
}
/**
* Gets toolbar color
*/
private void getToolbarUI() {
final ConstraintLayout toolbar = findViewById(R.id.location_picker_toolbar);
largeToolbarText = findViewById(R.id.location_picker_toolbar_primary_text_view);
smallToolbarText = findViewById(R.id.location_picker_toolbar_secondary_text_view);
toolbar.setBackgroundColor(getResources().getColor(R.color.primaryColor));
}
@Override
protected void onStart() {
super.onStart();
mapView.onStart();
}
/**
* Takes action when map is ready to show
* @param mapboxMap map
*/
@Override
public void onMapReady(final MapboxMap mapboxMap) {
this.mapboxMap = mapboxMap;
mapboxMap.setStyle(Style.MAPBOX_STREETS, style -> {
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
if (modifyLocationButton.getVisibility() == View.VISIBLE) {
initDroppedMarker(style);
adjustCameraBasedOnOptions();
enableLocationComponent(style);
if (style.getLayer(DROPPED_MARKER_LAYER_ID) != null) {
final GeoJsonSource source = style.getSourceAs("dropped-marker-source-id");
if (source != null) {
source.setGeoJson(Point.fromLngLat(cameraPosition.target.getLongitude(),
cameraPosition.target.getLatitude()));
}
droppedMarkerLayer = style.getLayer(DROPPED_MARKER_LAYER_ID);
if (droppedMarkerLayer != null) {
droppedMarkerLayer.setProperties(visibility(VISIBLE));
markerImage.setVisibility(View.GONE);
shadow.setVisibility(View.GONE);
}
}
} else {
adjustCameraBasedOnOptions();
enableLocationComponent(style);
bindListeners();
}
modifyLocationButton.setOnClickListener(v -> {
placeSelectedButton.setVisibility(View.VISIBLE);
modifyLocationButton.setVisibility(View.GONE);
showInMapButton.setVisibility(View.GONE);
droppedMarkerLayer.setProperties(visibility(NONE));
markerImage.setVisibility(View.VISIBLE);
shadow.setVisibility(View.VISIBLE);
largeToolbarText.setText(getResources().getString(R.string.choose_a_location));
smallToolbarText.setText(getResources().getString(R.string.pan_and_zoom_to_adjust));
bindListeners();
});
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
showInMapButton.setOnClickListener(v -> showInMap());
});
}
@Override
protected void onStop() {
super.onStop();
mapView.onStop();
}
/**
* Show the location in map app
*/
public void showInMap(){
Utils.handleGeoCoordinates(this,
new fr.free.nrw.commons.location.LatLng(cameraPosition.target.getLatitude(),
cameraPosition.target.getLongitude(), 0.0f));
}
@Override
protected void onSaveInstanceState(final @NotNull Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
/**
* Initialize Dropped Marker and layer without showing
* @param loadedMapStyle style
*/
private void initDroppedMarker(@NonNull final Style loadedMapStyle) {
// Add the marker image to map
loadedMapStyle.addImage("dropped-icon-image", BitmapFactory.decodeResource(
getResources(), R.drawable.map_default_map_marker));
loadedMapStyle.addSource(new GeoJsonSource("dropped-marker-source-id"));
loadedMapStyle.addLayer(new SymbolLayer(DROPPED_MARKER_LAYER_ID,
"dropped-marker-source-id").withProperties(
iconImage("dropped-icon-image"),
visibility(NONE),
iconAllowOverlap(true),
iconIgnorePlacement(true)
));
}
/**
* move the location to the current media coordinates
*/
private void adjustCameraBasedOnOptions() {
mapboxMap.moveCamera(CameraUpdateFactory.newCameraPosition(cameraPosition));
}
@Override
protected void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
/**
* Enables location components
* @param loadedMapStyle Style
*/
@SuppressWarnings( {"MissingPermission"})
private void enableLocationComponent(@NonNull final Style loadedMapStyle) {
final UiSettings uiSettings = mapboxMap.getUiSettings();
uiSettings.setAttributionEnabled(false);
@Override
public void onLowMemory() {
super.onLowMemory();
mapView.onLowMemory();
}
// Check if permissions are enabled and if not request
if (PermissionsManager.areLocationPermissionsGranted(this)) {
// Get an instance of the component
final LocationComponent locationComponent = mapboxMap.getLocationComponent();
// Activate with options
locationComponent.activateLocationComponent(
LocationComponentActivationOptions.builder(this, loadedMapStyle).build());
// Enable to make component visible
locationComponent.setLocationComponentEnabled(true);
// Set the component's camera mode
locationComponent.setCameraMode(CameraMode.NONE);
// Set the component's render mode
locationComponent.setRenderMode(RenderMode.NORMAL);
}
}
/**
* Acts on camera moving
* @param reason int
*/
@Override
public void onCameraMoveStarted(final int reason) {
Timber.v("Map camera has begun moving.");
if (markerImage.getTranslationY() == 0) {
markerImage.animate().translationY(-75)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
}
/**
* Acts on camera idle
*/
@Override
public void onCameraIdle() {
Timber.v("Map camera is now idling.");
markerImage.animate().translationY(0)
.setInterpolator(new OvershootInterpolator()).setDuration(250).start();
}
/**
* Takes action on camera position
* @param position position of picker
*/
@Override
public void onChanged(@Nullable CameraPosition position) {
if (position == null) {
position = new Builder()
.target(new LatLng(mapboxMap.getCameraPosition().target.getLatitude(),
mapboxMap.getCameraPosition().target.getLongitude()))
.zoom(16).build();
}
cameraPosition = position;
}
/**
* Select the preferable location
*/
private void addPlaceSelectedButton() {
placeSelectedButton = findViewById(R.id.location_chosen_button);
placeSelectedButton.setOnClickListener(view -> placeSelected());
}
/**
* Return the intent with required data
*/
void placeSelected() {
final Intent returningIntent = new Intent();
returningIntent.putExtra(LocationPickerConstants.MAP_CAMERA_POSITION,
mapboxMap.getCameraPosition());
setResult(AppCompatActivity.RESULT_OK, returningIntent);
finish();
}
@Override
protected void onStart() {
super.onStart();
mapView.onStart();
}
@Override
protected void onResume() {
super.onResume();
mapView.onResume();
}
@Override
protected void onPause() {
super.onPause();
mapView.onPause();
}
@Override
protected void onStop() {
super.onStop();
mapView.onStop();
}
@Override
protected void onSaveInstanceState(final @NotNull Bundle outState) {
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
@Override
protected void onDestroy() {
super.onDestroy();
mapView.onDestroy();
}
@Override
public void onLowMemory() {
super.onLowMemory();
mapView.onLowMemory();
}
}

View file

@ -5,9 +5,13 @@ package fr.free.nrw.commons.LocationPicker;
*/
public final class LocationPickerConstants {
public static final String MAP_CAMERA_POSITION
= "location.picker.cameraPosition";
public static final String ACTIVITY_KEY
= "location.picker.activity";
private LocationPickerConstants() {
}
public static final String MAP_CAMERA_POSITION
= "location.picker.cameraPosition";
private LocationPickerConstants() {
}
}

View file

@ -16,45 +16,48 @@ import timber.log.Timber;
*/
public class LocationPickerViewModel extends AndroidViewModel implements Callback<CameraPosition> {
/**
* Wrapping CameraPosition with MutableLiveData
*/
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
/**
* Wrapping CameraPosition with MutableLiveData
*/
private final MutableLiveData<CameraPosition> result = new MutableLiveData<>();
/**
* Constructor for this class
* @param application Application
*/
public LocationPickerViewModel(@NonNull final Application application) {
super(application);
}
/**
* Responses on camera position changing
* @param call Call<CameraPosition>
* @param response Response<CameraPosition>
*/
@Override
public void onResponse(final @NotNull Call<CameraPosition> call,
final Response<CameraPosition> response) {
if(response.body()==null){
result.setValue(null);
return;
/**
* Constructor for this class
*
* @param application Application
*/
public LocationPickerViewModel(@NonNull final Application application) {
super(application);
}
result.setValue(response.body());
}
@Override
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
Timber.e(t);
}
/**
* Responses on camera position changing
*
* @param call Call<CameraPosition>
* @param response Response<CameraPosition>
*/
@Override
public void onResponse(final @NotNull Call<CameraPosition> call,
final Response<CameraPosition> response) {
if (response.body() == null) {
result.setValue(null);
return;
}
result.setValue(response.body());
}
/**
* Gets live CameraPosition
* @return MutableLiveData<CameraPosition>
*/
public MutableLiveData<CameraPosition> getResult() {
return result;
}
@Override
public void onFailure(final @NotNull Call<CameraPosition> call, final @NotNull Throwable t) {
Timber.e(t);
}
/**
* Gets live CameraPosition
*
* @return MutableLiveData<CameraPosition>
*/
public MutableLiveData<CameraPosition> getResult() {
return result;
}
}

View file

@ -48,4 +48,6 @@ class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClien
)
}
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title);
}

View file

@ -6,7 +6,7 @@ import org.wikipedia.csrf.CsrfTokenClient
/**
* This class acts as a Client to facilitate wiki page editing
* services to various dependency providing modules such as the Network module, the Review Controller ,etc
* services to various dependency providing modules such as the Network module, the Review Controller, etc.
*
* The methods provided by this class will post to the Media wiki api
* documented at: https://commons.wikimedia.org/w/api.php?action=help&modules=edit

View file

@ -208,9 +208,6 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@Override
protected void onResume() {
super.onResume();
if (applicationKvStore.getBoolean("firstrun", true)) {
WelcomeActivity.startYourself(this);
}
if (sessionManager.getCurrentAccount() != null
&& sessionManager.isUserLoggedIn()) {
@ -464,7 +461,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
@Override
protected void onSaveInstanceState(Bundle outState) {
// if progressDialog is visible during the configuration change then store state as true else false so that
// we maintain visiblity of progressDailog after configuration change
// we maintain visibility of progressDailog after configuration change
if(progressDialog!=null&&progressDialog.isShowing()) {
outState.putBoolean(saveProgressDailog,true);
} else {
@ -472,7 +469,7 @@ public class LoginActivity extends AccountAuthenticatorActivity {
}
outState.putString(saveErrorMessage,errorMessage.getText().toString()); //Save the errorMessage
outState.putString(saveUsername,getUsername()); // Save the username
outState.putString(savePassword,getPassword()); // Save thte password
outState.putString(savePassword,getPassword()); // Save the password
}
private String getUsername() {
return usernameEdit.getText().toString();

View file

@ -27,84 +27,85 @@ import javax.inject.Named;
public class BookmarkFragment extends CommonsDaggerSupportFragment {
private FragmentManager supportFragmentManager;
private BookmarksPagerAdapter adapter;
@BindView(R.id.viewPagerBookmarks)
ParentViewPager viewPager;
@BindView(R.id.tab_layout)
TabLayout tabLayout;
@BindView(R.id.fragmentContainer)
FrameLayout fragmentContainer;
private FragmentManager supportFragmentManager;
private BookmarksPagerAdapter adapter;
@BindView(R.id.viewPagerBookmarks)
ParentViewPager viewPager;
@BindView(R.id.tab_layout)
TabLayout tabLayout;
@BindView(R.id.fragmentContainer)
FrameLayout fragmentContainer;
@Inject
ContributionController controller;
/**
* To check if the user is loggedIn or not.
*/
@Inject
@Named("default_preferences")
public
JsonKvStore applicationKvStore;
@Inject
ContributionController controller;
/**
* To check if the user is loggedIn or not.
*/
@Inject
@Named("default_preferences")
public
JsonKvStore applicationKvStore;
@NonNull
public static BookmarkFragment newInstance() {
BookmarkFragment fragment = new BookmarkFragment();
fragment.setRetainInstance(true);
return fragment;
}
public void setScroll(boolean canScroll){
viewPager.setCanScroll(canScroll);
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_bookmarks, container, false);
ButterKnife.bind(this, view);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
supportFragmentManager = getChildFragmentManager();
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
applicationKvStore.getBoolean("login_skipped"));
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
((MainActivity)getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
setupTabLayout();
return view;
}
/**
* This method sets up the tab layout.
* If the adapter has only one element it sets the visibility of tabLayout to gone.
*/
public void setupTabLayout(){
tabLayout.setVisibility(View.VISIBLE);
if (adapter.getCount() == 1) {
tabLayout.setVisibility(View.GONE);
@NonNull
public static BookmarkFragment newInstance() {
BookmarkFragment fragment = new BookmarkFragment();
fragment.setRetainInstance(true);
return fragment;
}
}
public void onBackPressed() {
if(((BookmarkListRootFragment)(adapter.getItem(tabLayout.getSelectedTabPosition()))).backPressed()) {
// The event is handled internally by the adapter , no further action required.
return;
public void setScroll(boolean canScroll) {
viewPager.setCanScroll(canScroll);
}
@Override
public void onCreate(@Nullable final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
@Nullable
@Override
public View onCreateView(@NonNull final LayoutInflater inflater,
@Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
View view = inflater.inflate(R.layout.fragment_bookmarks, container, false);
ButterKnife.bind(this, view);
// Activity can call methods in the fragment by acquiring a
// reference to the Fragment from FragmentManager, using findFragmentById()
supportFragmentManager = getChildFragmentManager();
adapter = new BookmarksPagerAdapter(supportFragmentManager, getContext(),
applicationKvStore.getBoolean("login_skipped"));
viewPager.setAdapter(adapter);
tabLayout.setupWithViewPager(viewPager);
((MainActivity) getActivity()).showTabs();
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
setupTabLayout();
return view;
}
/**
* This method sets up the tab layout. If the adapter has only one element it sets the
* visibility of tabLayout to gone.
*/
public void setupTabLayout() {
tabLayout.setVisibility(View.VISIBLE);
if (adapter.getCount() == 1) {
tabLayout.setVisibility(View.GONE);
}
}
public void onBackPressed() {
if (((BookmarkListRootFragment) (adapter.getItem(tabLayout.getSelectedTabPosition())))
.backPressed()) {
// The event is handled internally by the adapter , no further action required.
return;
}
// Event is not handled by the adapter ( performed back action ) change action bar.
((BaseActivity) getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
// Event is not handled by the adapter ( performed back action ) change action bar.
((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
}
}

View file

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

View file

@ -284,7 +284,7 @@ public class BookmarkLocationsDao {
}
return;
}
if (from == 12) {
if (from >= 12) {
try {
db.execSQL(
"ALTER TABLE bookmarksLocations ADD COLUMN location_destroyed STRING;");
@ -292,14 +292,14 @@ public class BookmarkLocationsDao {
Timber.e(exception);
}
}
if (from == 13){
if (from >= 13){
try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_language STRING;");
} catch (SQLiteException exception){
Timber.e(exception);
}
}
if (from == 14){
if (from >= 14){
try {
db.execSQL("ALTER TABLE bookmarksLocations ADD COLUMN location_exists STRING;");
} catch (SQLiteException exception){

View file

@ -18,61 +18,61 @@ import java.util.List;
@Dao
public abstract class ContributionDao {
@Query("SELECT * FROM contribution order by media_dateUploaded DESC")
abstract DataSource.Factory<Integer, Contribution> fetchContributions();
@Query("SELECT * FROM contribution order by media_dateUploaded DESC")
abstract DataSource.Factory<Integer, Contribution> fetchContributions();
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void saveSynchronous(Contribution contribution);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract void saveSynchronous(Contribution contribution);
public Completable save(final Contribution contribution) {
return Completable
.fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
saveSynchronous(contribution);
});
}
public Completable save(final Contribution contribution) {
return Completable
.fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
saveSynchronous(contribution);
});
}
@Transaction
public void deleteAndSaveContribution(final Contribution oldContribution,
final Contribution newContribution) {
deleteSynchronous(oldContribution);
saveSynchronous(newContribution);
}
@Transaction
public void deleteAndSaveContribution(final Contribution oldContribution,
final Contribution newContribution) {
deleteSynchronous(oldContribution);
saveSynchronous(newContribution);
}
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<List<Long>> save(List<Contribution> contribution);
@Insert(onConflict = OnConflictStrategy.REPLACE)
public abstract Single<List<Long>> save(List<Contribution> contribution);
@Delete
public abstract void deleteSynchronous(Contribution contribution);
@Delete
public abstract void deleteSynchronous(Contribution contribution);
public Completable delete(final Contribution contribution) {
return Completable
.fromAction(() -> deleteSynchronous(contribution));
}
public Completable delete(final Contribution contribution) {
return Completable
.fromAction(() -> deleteSynchronous(contribution));
}
@Query("SELECT * from contribution WHERE media_filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName);
@Query("SELECT * from contribution WHERE media_filename=:fileName")
public abstract List<Contribution> getContributionWithTitle(String fileName);
@Query("SELECT * from contribution WHERE pageId=:pageId")
public abstract Contribution getContribution(String pageId);
@Query("SELECT * from contribution WHERE pageId=:pageId")
public abstract Contribution getContribution(String pageId);
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract Single<List<Contribution>> getContribution(List<Integer> states);
@Query("SELECT * from contribution WHERE state IN (:states) order by media_dateUploaded DESC")
public abstract Single<List<Contribution>> getContribution(List<Integer> states);
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);
@Query("SELECT COUNT(*) from contribution WHERE state in (:toUpdateStates)")
public abstract Single<Integer> getPendingUploads(int[] toUpdateStates);
@Query("Delete FROM contribution")
public abstract void deleteAll() throws SQLiteException;
@Query("Delete FROM contribution")
public abstract void deleteAll() throws SQLiteException;
@Update
public abstract void updateSynchronous(Contribution contribution);
@Update
public abstract void updateSynchronous(Contribution contribution);
public Completable update(final Contribution contribution) {
return Completable
.fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
updateSynchronous(contribution);
});
}
public Completable update(final Contribution contribution) {
return Completable
.fromAction(() -> {
contribution.setDateModified(Calendar.getInstance().getTime());
updateSynchronous(contribution);
});
}
}

View file

@ -24,244 +24,243 @@ import io.reactivex.schedulers.Schedulers;
public class ContributionViewHolder extends RecyclerView.ViewHolder {
private final Callback callback;
@BindView(R.id.contributionImage)
SimpleDraweeView imageView;
@BindView(R.id.contributionTitle)
TextView titleView;
@BindView(R.id.authorView)
TextView authorView;
@BindView(R.id.contributionState)
TextView stateView;
@BindView(R.id.contributionSequenceNumber)
TextView seqNumView;
@BindView(R.id.contributionProgress)
ProgressBar progressView;
@BindView(R.id.image_options)
RelativeLayout imageOptions;
@BindView(R.id.wikipediaButton)
ImageButton addToWikipediaButton;
@BindView(R.id.retryButton)
ImageButton retryButton;
@BindView(R.id.cancelButton)
ImageButton cancelButton;
@BindView(R.id.pauseResumeButton)
ImageButton pauseResumeButton;
private final Callback callback;
@BindView(R.id.contributionImage)
SimpleDraweeView imageView;
@BindView(R.id.contributionTitle)
TextView titleView;
@BindView(R.id.authorView)
TextView authorView;
@BindView(R.id.contributionState)
TextView stateView;
@BindView(R.id.contributionSequenceNumber)
TextView seqNumView;
@BindView(R.id.contributionProgress)
ProgressBar progressView;
@BindView(R.id.image_options)
RelativeLayout imageOptions;
@BindView(R.id.wikipediaButton)
ImageButton addToWikipediaButton;
@BindView(R.id.retryButton)
ImageButton retryButton;
@BindView(R.id.cancelButton)
ImageButton cancelButton;
@BindView(R.id.pauseResumeButton)
ImageButton pauseResumeButton;
private int position;
private Contribution contribution;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private final MediaClient mediaClient;
private boolean isWikipediaButtonDisplayed;
private int position;
private Contribution contribution;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private final MediaClient mediaClient;
private boolean isWikipediaButtonDisplayed;
ContributionViewHolder(final View parent, final Callback callback,
final MediaClient mediaClient) {
super(parent);
this.mediaClient = mediaClient;
ButterKnife.bind(this, parent);
this.callback = callback;
}
public void init(final int position, final Contribution contribution) {
//handling crashes when the contribution is null.
if( null == contribution) {
return;
ContributionViewHolder(final View parent, final Callback callback,
final MediaClient mediaClient) {
super(parent);
this.mediaClient = mediaClient;
ButterKnife.bind(this, parent);
this.callback = callback;
}
this.contribution = contribution;
this.position = position;
titleView.setText(contribution.getMedia().getMostRelevantCaption());
authorView.setText(contribution.getMedia().getAuthor());
public void init(final int position, final Contribution contribution) {
//Removes flicker of loading image.
imageView.getHierarchy().setFadeDuration(0);
imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
imageView.setImageRequest(imageRequest);
}
seqNumView.setText(String.valueOf(position + 1));
seqNumView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
switch (contribution.getState()) {
case Contribution.STATE_COMPLETED:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.GONE);
imageOptions.setVisibility(View.GONE);
stateView.setText("");
checkIfMediaExistsOnWikipediaPage(contribution);
break;
case Contribution.STATE_QUEUED:
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
stateView.setVisibility(View.VISIBLE);
progressView.setVisibility(View.GONE);
stateView.setText(R.string.contribution_state_queued);
imageOptions.setVisibility(View.GONE);
break;
case Contribution.STATE_IN_PROGRESS:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) {
progressView.setIndeterminate(true);
} else {
progressView.setProgress((int) (((double) transferred / (double) total) * 100));
//handling crashes when the contribution is null.
if (null == contribution) {
return;
}
break;
case Contribution.STATE_PAUSED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.paused);
this.contribution = contribution;
this.position = position;
titleView.setText(contribution.getMedia().getMostRelevantCaption());
authorView.setText(contribution.getMedia().getAuthor());
//Removes flicker of loading image.
imageView.getHierarchy().setFadeDuration(0);
imageView.getHierarchy().setPlaceholderImage(R.drawable.image_placeholder);
imageView.getHierarchy().setFailureImage(R.drawable.image_placeholder);
final String imageSource = chooseImageSource(contribution.getMedia().getThumbUrl(),
contribution.getLocalUri());
if (!TextUtils.isEmpty(imageSource)) {
final ImageRequest imageRequest =
ImageRequestBuilder.newBuilderWithSource(Uri.parse(imageSource))
.setProgressiveRenderingEnabled(true)
.build();
imageView.setImageRequest(imageRequest);
}
seqNumView.setText(String.valueOf(position + 1));
seqNumView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
switch (contribution.getState()) {
case Contribution.STATE_COMPLETED:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.GONE);
imageOptions.setVisibility(View.GONE);
stateView.setText("");
checkIfMediaExistsOnWikipediaPage(contribution);
break;
case Contribution.STATE_QUEUED:
case Contribution.STATE_QUEUED_LIMITED_CONNECTION_MODE:
stateView.setVisibility(View.VISIBLE);
progressView.setVisibility(View.GONE);
stateView.setText(R.string.contribution_state_queued);
imageOptions.setVisibility(View.GONE);
break;
case Contribution.STATE_IN_PROGRESS:
stateView.setVisibility(View.GONE);
progressView.setVisibility(View.VISIBLE);
addToWikipediaButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
final long total = contribution.getDataLength();
final long transferred = contribution.getTransferred();
if (transferred == 0 || transferred >= total) {
progressView.setIndeterminate(true);
} else {
progressView.setProgress((int) (((double) transferred / (double) total) * 100));
}
break;
case Contribution.STATE_PAUSED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.paused);
setResume();
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
imageOptions.setVisibility(View.VISIBLE);
break;
case Contribution.STATE_FAILED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.contribution_state_failed);
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.VISIBLE);
retryButton.setVisibility(View.VISIBLE);
pauseResumeButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
break;
}
}
/**
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made
* for the device's current language Wikipedia
*
* @param contribution
*/
private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) {
if (contribution.getWikidataPlace() == null
|| contribution.getWikidataPlace().getWikipediaArticle() == null) {
return;
}
final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle();
compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(mediaExists -> {
displayWikipediaButton(mediaExists);
}));
}
/**
* Handle action buttons visibility if the corresponding wikipedia page doesn't contain any
* media. This method needs to control the state of just the scenario where media does not
* exists as other scenarios are already handled in the init method.
*
* @param mediaExists
*/
private void displayWikipediaButton(Boolean mediaExists) {
if (!mediaExists) {
addToWikipediaButton.setVisibility(View.VISIBLE);
isWikipediaButtonDisplayed = true;
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
}
}
/**
* Returns the image source for the image view, first preference is given to thumbUrl if that is
* null, moves to local uri and if both are null return null
*
* @param thumbUrl
* @param localUri
* @return
*/
@Nullable
private String chooseImageSource(final String thumbUrl, final Uri localUri) {
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
localUri != null ? localUri.toString() :
null;
}
/**
* Retry upload when it is failed
*/
@OnClick(R.id.retryButton)
public void retryUpload() {
callback.retryUpload(contribution);
}
/**
* Delete a failed upload attempt
*/
@OnClick(R.id.cancelButton)
public void deleteUpload() {
callback.deleteUpload(contribution);
}
@OnClick(R.id.contributionImage)
public void imageClicked() {
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
}
@OnClick(R.id.wikipediaButton)
public void wikipediaButtonClicked() {
callback.addImageToWikipedia(contribution);
}
/**
* Triggers a callback for pause/resume
*/
@OnClick(R.id.pauseResumeButton)
public void onPauseResumeButtonClicked() {
if (pauseResumeButton.getTag().toString().equals("pause")) {
pause();
} else {
resume();
}
}
private void resume() {
callback.resumeUpload(contribution);
setPaused();
}
private void pause() {
callback.pauseUpload(contribution);
setResume();
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
pauseResumeButton.setVisibility(View.VISIBLE);
imageOptions.setVisibility(View.VISIBLE);
break;
case Contribution.STATE_FAILED:
stateView.setVisibility(View.VISIBLE);
stateView.setText(R.string.contribution_state_failed);
progressView.setVisibility(View.GONE);
cancelButton.setVisibility(View.VISIBLE);
retryButton.setVisibility(View.VISIBLE);
pauseResumeButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
break;
}
}
/**
* Checks if a media exists on the corresponding Wikipedia article Currently the check is made for
* the device's current language Wikipedia
*
* @param contribution
*/
private void checkIfMediaExistsOnWikipediaPage(final Contribution contribution) {
if (contribution.getWikidataPlace() == null
|| contribution.getWikidataPlace().getWikipediaArticle() == null) {
return;
/**
* Update pause/resume button to show pause state
*/
private void setPaused() {
pauseResumeButton.setImageResource(R.drawable.pause_icon);
pauseResumeButton.setTag(R.string.pause);
}
final String wikipediaArticle = contribution.getWikidataPlace().getWikipediaPageTitle();
compositeDisposable.add(mediaClient.doesPageContainMedia(wikipediaArticle)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(mediaExists -> {
displayWikipediaButton(mediaExists);
}));
}
/**
* Handle action buttons visibility if the corresponding wikipedia page doesn't contain any media.
* This method needs to control the state of just the scenario where media does not exists as
* other scenarios are already handled in the init method.
*
* @param mediaExists
*/
private void displayWikipediaButton(Boolean mediaExists) {
if (!mediaExists) {
addToWikipediaButton.setVisibility(View.VISIBLE);
isWikipediaButtonDisplayed = true;
cancelButton.setVisibility(View.GONE);
retryButton.setVisibility(View.GONE);
imageOptions.setVisibility(View.VISIBLE);
/**
* Update pause/resume button to show resume state
*/
private void setResume() {
pauseResumeButton.setImageResource(R.drawable.play_icon);
pauseResumeButton.setTag(R.string.resume);
}
}
/**
* Returns the image source for the image view, first preference is given to thumbUrl if that is
* null, moves to local uri and if both are null return null
*
* @param thumbUrl
* @param localUri
* @return
*/
@Nullable
private String chooseImageSource(final String thumbUrl, final Uri localUri) {
return !TextUtils.isEmpty(thumbUrl) ? thumbUrl :
localUri != null ? localUri.toString() :
null;
}
/**
* Retry upload when it is failed
*/
@OnClick(R.id.retryButton)
public void retryUpload() {
callback.retryUpload(contribution);
}
/**
* Delete a failed upload attempt
*/
@OnClick(R.id.cancelButton)
public void deleteUpload() {
callback.deleteUpload(contribution);
}
@OnClick(R.id.contributionImage)
public void imageClicked() {
callback.openMediaDetail(position, isWikipediaButtonDisplayed);
}
@OnClick(R.id.wikipediaButton)
public void wikipediaButtonClicked() {
callback.addImageToWikipedia(contribution);
}
/**
* Triggers a callback for pause/resume
*/
@OnClick(R.id.pauseResumeButton)
public void onPauseResumeButtonClicked() {
if (pauseResumeButton.getTag().toString().equals("pause")) {
pause();
} else {
resume();
}
}
private void resume() {
callback.resumeUpload(contribution);
setPaused();
}
private void pause() {
callback.pauseUpload(contribution);
setResume();
}
/**
* Update pause/resume button to show pause state
*/
private void setPaused() {
pauseResumeButton.setImageResource(R.drawable.pause_icon);
pauseResumeButton.setTag(R.string.pause);
}
/**
* Update pause/resume button to show resume state
*/
private void setResume() {
pauseResumeButton.setImageResource(R.drawable.play_icon);
pauseResumeButton.setTag(R.string.resume);
}
}

View file

@ -9,6 +9,7 @@ import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.content.Context;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@ -33,6 +34,8 @@ import fr.free.nrw.commons.theme.BaseActivity;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
@ -60,6 +63,7 @@ import fr.free.nrw.commons.notification.Notification;
import fr.free.nrw.commons.notification.NotificationActivity;
import fr.free.nrw.commons.notification.NotificationController;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.upload.worker.UploadWorker;
import fr.free.nrw.commons.utils.ConfigUtils;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.NetworkUtils;
@ -154,7 +158,7 @@ public class ContributionsFragment
}
initFragments();
upDateUploadCount();
if(shouldShowMediaDetailsFragment){
showMediaDetailPagerFragment();
}else{
@ -644,6 +648,21 @@ public class ContributionsFragment
return mediaDetailPagerFragment;
}
/**
* this function updates the number of contributions
*/
void upDateUploadCount() {
WorkManager.getInstance(getContext())
.getWorkInfosForUniqueWorkLiveData(UploadWorker.class.getSimpleName()).observe(
getViewLifecycleOwner(), workInfos -> {
if (workInfos.size() > 0) {
setUploadCount();
}
});
}
/**
* Reload media detail fragment once media is nominated
*
@ -658,7 +677,7 @@ public class ContributionsFragment
showMediaDetailPagerFragment();
}
}
// click listener to toggle description that means uses can press the limited connection
// banner and description will hide. Tap again to show description.
private View.OnClickListener toggleDescriptionListener = new View.OnClickListener() {
@ -674,5 +693,6 @@ public class ContributionsFragment
}
};
}

View file

@ -8,17 +8,17 @@ import java.util.List;
*/
public class ContributionsListContract {
public interface View {
public interface View {
void showWelcomeTip(boolean numberOfUploads);
void showWelcomeTip(boolean numberOfUploads);
void showProgress(boolean shouldShow);
void showProgress(boolean shouldShow);
void showNoContributionsUI(boolean shouldShow);
}
void showNoContributionsUI(boolean shouldShow);
}
public interface UserActionListener extends BasePresenter<View> {
public interface UserActionListener extends BasePresenter<View> {
void deleteUpload(Contribution contribution);
}
void deleteUpload(Contribution contribution);
}
}

View file

@ -55,213 +55,215 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
ContributionsListContract.View, ContributionsListAdapter.Callback,
WikipediaInstructionsDialogFragment.Callback {
private static final String RV_STATE = "rv_scroll_state";
private static final String RV_STATE = "rv_scroll_state";
@BindView(R.id.contributionsList)
RecyclerView rvContributionsList;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
FloatingActionButton fabPlus;
@BindView(R.id.fab_camera)
FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery)
FloatingActionButton fabGallery;
@BindView(R.id.noContributionsYet)
TextView noContributionsYet;
@BindView(R.id.fab_layout)
LinearLayout fab_layout;
@BindView(R.id.fab_custom_gallery)
FloatingActionButton fabCustomGallery;
@BindView(R.id.contributionsList)
RecyclerView rvContributionsList;
@BindView(R.id.loadingContributionsProgressBar)
ProgressBar progressBar;
@BindView(R.id.fab_plus)
FloatingActionButton fabPlus;
@BindView(R.id.fab_camera)
FloatingActionButton fabCamera;
@BindView(R.id.fab_gallery)
FloatingActionButton fabGallery;
@BindView(R.id.noContributionsYet)
TextView noContributionsYet;
@BindView(R.id.fab_layout)
LinearLayout fab_layout;
@BindView(R.id.fab_custom_gallery)
FloatingActionButton fabCustomGallery;
@Inject
SystemThemeUtils systemThemeUtils;
@Inject
ContributionController controller;
@Inject
MediaClient mediaClient;
@Inject
ContributionController controller;
@Inject
MediaClient mediaClient;
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
@Inject
WikiSite languageWikipediaSite;
@Named(NAMED_LANGUAGE_WIKI_PEDIA_WIKI_SITE)
@Inject
WikiSite languageWikipediaSite;
@Inject
ContributionsListPresenter contributionsListPresenter;
@Inject
ContributionsListPresenter contributionsListPresenter;
private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private Animation rotate_backward;
private Animation fab_close;
private Animation fab_open;
private Animation rotate_forward;
private Animation rotate_backward;
private boolean isFabOpen;
private boolean isFabOpen;
private ContributionsListAdapter adapter;
private ContributionsListAdapter adapter;
private Callback callback;
private Callback callback;
private final int SPAN_COUNT_LANDSCAPE = 3;
private final int SPAN_COUNT_PORTRAIT = 1;
private final int SPAN_COUNT_LANDSCAPE = 3;
private final int SPAN_COUNT_PORTRAIT = 1;
private int contributionsSize;
private int contributionsSize;
@Override
public View onCreateView(
final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view);
contributionsListPresenter.onAttachView(this);
initAdapter();
return view;
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getParentFragment() != null && getParentFragment() instanceof ContributionsFragment) {
callback = ((ContributionsFragment) getParentFragment());
}
}
@Override
public void onDetach() {
super.onDetach();
callback = null;//To avoid possible memory leak
}
private void initAdapter() {
adapter = new ContributionsListAdapter(this, mediaClient);
}
@Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
initializeAnimations();
setListeners();
}
private void initRecyclerView() {
final GridLayoutManager layoutManager = new GridLayoutManager(getContext(),
getSpanCount(getResources().getConfiguration().orientation));
rvContributionsList.setLayoutManager(layoutManager);
//Setting flicker animation of recycler view to false.
final ItemAnimator animator = rvContributionsList.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
@Override
public View onCreateView(
final LayoutInflater inflater, @Nullable final ViewGroup container,
@Nullable final Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.fragment_contributions_list, container, false);
ButterKnife.bind(this, view);
contributionsListPresenter.onAttachView(this);
initAdapter();
return view;
}
contributionsListPresenter.setup();
contributionsListPresenter.contributionList.observe(this.getViewLifecycleOwner(), list -> {
contributionsSize = list.size();
adapter.submitList(list);
callback.notifyDataSetChanged();
});
rvContributionsList.setAdapter(adapter);
adapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if (itemCount > 0 && positionStart == 0) {
if(adapter.getContributionForPosition(positionStart)!=null) {
rvContributionsList.scrollToPosition(0);//Newly upload items are always added to the top
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
if (getParentFragment() != null && getParentFragment() instanceof ContributionsFragment) {
callback = ((ContributionsFragment) getParentFragment());
}
}
}
/**
* Called whenever items in the list have changed
* Calls viewPagerNotifyDataSetChanged() that will notify the viewpager
*/
@Override
public void onItemRangeChanged(final int positionStart, final int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
callback.viewPagerNotifyDataSetChanged();
}
});
@Override
public void onDetach() {
super.onDetach();
callback = null;//To avoid possible memory leak
}
//Fab close on touch outside (Scrolling or taping on item triggers this action).
rvContributionsList.addOnItemTouchListener(new OnItemTouchListener() {
private void initAdapter() {
adapter = new ContributionsListAdapter(this, mediaClient);
}
/**
* Silently observe and/or take over touch events sent to the RecyclerView before
* they are handled by either the RecyclerView itself or its child views.
*/
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
if (isFabOpen) {
@Override
public void onViewCreated(final View view, @Nullable final Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRecyclerView();
initializeAnimations();
setListeners();
}
private void initRecyclerView() {
final GridLayoutManager layoutManager = new GridLayoutManager(getContext(),
getSpanCount(getResources().getConfiguration().orientation));
rvContributionsList.setLayoutManager(layoutManager);
//Setting flicker animation of recycler view to false.
final ItemAnimator animator = rvContributionsList.getItemAnimator();
if (animator instanceof SimpleItemAnimator) {
((SimpleItemAnimator) animator).setSupportsChangeAnimations(false);
}
contributionsListPresenter.setup();
contributionsListPresenter.contributionList.observe(this.getViewLifecycleOwner(), list -> {
contributionsSize = list.size();
adapter.submitList(list);
callback.notifyDataSetChanged();
});
rvContributionsList.setAdapter(adapter);
adapter.registerAdapterDataObserver(new AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
super.onItemRangeInserted(positionStart, itemCount);
if (itemCount > 0 && positionStart == 0) {
if (adapter.getContributionForPosition(positionStart) != null) {
rvContributionsList
.scrollToPosition(0);//Newly upload items are always added to the top
}
}
}
/**
* Called whenever items in the list have changed
* Calls viewPagerNotifyDataSetChanged() that will notify the viewpager
*/
@Override
public void onItemRangeChanged(final int positionStart, final int itemCount) {
super.onItemRangeChanged(positionStart, itemCount);
callback.viewPagerNotifyDataSetChanged();
}
});
//Fab close on touch outside (Scrolling or taping on item triggers this action).
rvContributionsList.addOnItemTouchListener(new OnItemTouchListener() {
/**
* Silently observe and/or take over touch events sent to the RecyclerView before
* they are handled by either the RecyclerView itself or its child views.
*/
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
if (e.getAction() == MotionEvent.ACTION_DOWN) {
if (isFabOpen) {
animateFAB(isFabOpen);
}
}
return false;
}
/**
* Process a touch event as part of a gesture that was claimed by returning true
* from a previous call to {@link #onInterceptTouchEvent}.
*
* @param rv
* @param e MotionEvent describing the touch event. All coordinates are in the
* RecyclerView's coordinate system.
*/
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
//required abstract method DO NOT DELETE
}
/**
* Called when a child of RecyclerView does not want RecyclerView and its ancestors
* to intercept touch events with {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
*
* @param disallowIntercept True if the child does not want the parent to intercept
* touch events.
*/
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//required abstract method DO NOT DELETE
}
});
}
private int getSpanCount(final int orientation) {
return orientation == Configuration.ORIENTATION_LANDSCAPE ?
SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT;
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(
new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
}
private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
}
}
return false;
}
/**
* Process a touch event as part of a gesture that was claimed by returning true
* from a previous call to {@link #onInterceptTouchEvent}.
*
* @param rv
* @param e MotionEvent describing the touch event. All coordinates are in the
* RecyclerView's coordinate system.
*/
@Override
public void onTouchEvent(@NonNull RecyclerView rv, @NonNull MotionEvent e) {
//required abstract method DO NOT DELETE
}
/**
* Called when a child of RecyclerView does not want RecyclerView and its ancestors
* to intercept touch events with {@link ViewGroup#onInterceptTouchEvent(MotionEvent)}.
*
* @param disallowIntercept True if the child does not want the parent to intercept
* touch events.
*/
@Override
public void onRequestDisallowInterceptTouchEvent(boolean disallowIntercept) {
//required abstract method DO NOT DELETE
}
});
}
private int getSpanCount(final int orientation) {
return orientation == Configuration.ORIENTATION_LANDSCAPE ?
SPAN_COUNT_LANDSCAPE : SPAN_COUNT_PORTRAIT;
}
@Override
public void onConfigurationChanged(final Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// check orientation
fab_layout.setOrientation(newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE ?
LinearLayout.HORIZONTAL : LinearLayout.VERTICAL);
rvContributionsList
.setLayoutManager(new GridLayoutManager(getContext(), getSpanCount(newConfig.orientation)));
}
private void initializeAnimations() {
fab_open = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_open);
fab_close = AnimationUtils.loadAnimation(getActivity(), R.anim.fab_close);
rotate_forward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_forward);
rotate_backward = AnimationUtils.loadAnimation(getActivity(), R.anim.rotate_backward);
}
private void setListeners() {
fabPlus.setOnClickListener(view -> animateFAB(isFabOpen));
fabCamera.setOnClickListener(view -> {
controller.initiateCameraPick(getActivity());
animateFAB(isFabOpen);
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
});
fabGallery.setOnClickListener(view -> {
controller.initiateGalleryPick(getActivity(), true);
animateFAB(isFabOpen);
});
}
@OnClick(R.id.fab_custom_gallery)
void launchCustomSelector(){
@ -292,161 +294,163 @@ public class ContributionsListFragment extends CommonsDaggerSupportFragment impl
}
}
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
@Override
public void showWelcomeTip(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
@Override
public void showProgress(final boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
@Override
public void showNoContributionsUI(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList
.getLayoutManager();
outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState());
}
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (null != savedInstanceState) {
final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE);
rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
}
}
@Override
public void retryUpload(final Contribution contribution) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.retryUpload(contribution);
}
}
@Override
public void deleteUpload(final Contribution contribution) {
contributionsListPresenter.deleteUpload(contribution);
}
@Override
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.showDetail(position, isWikipediaButtonDisplayed);
}
}
/**
* Handle callback for wikipedia icon clicked
*
* @param contribution
*/
@Override
public void addImageToWikipedia(Contribution contribution) {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.add_picture_to_wikipedia_article_title),
String.format(getString(R.string.add_picture_to_wikipedia_article_desc),
Locale.getDefault().getDisplayLanguage()),
() -> {
showAddImageToWikipediaInstructions(contribution);
}, () -> {
// do nothing
});
}
/**
* Pauses the current upload
* @param contribution
*/
@Override
public void pauseUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.pausing_upload);
callback.pauseUpload(contribution);
}
/**
* Resumes the current upload
* @param contribution
*/
@Override
public void resumeUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.resuming_upload);
callback.retryUpload(contribution);
}
/**
* Display confirmation dialog with instructions when the user tries to add image to wikipedia
*
* @param contribution
*/
private void showAddImageToWikipediaInstructions(Contribution contribution) {
FragmentManager fragmentManager = getFragmentManager();
WikipediaInstructionsDialogFragment fragment = WikipediaInstructionsDialogFragment
.newInstance(contribution);
fragment.setCallback(this::onConfirmClicked);
fragment.show(fragmentManager, "WikimediaFragment");
}
public Media getMediaAtPosition(final int i) {
if(adapter.getContributionForPosition(i) != null) {
return adapter.getContributionForPosition(i).getMedia();
}
return null;
}
public int getTotalMediaCount() {
return contributionsSize;
}
/**
* Open the editor for the language Wikipedia
*
* @param contribution
*/
@Override
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
if (copyWikicode) {
String wikicode = contribution.getMedia().getWikiCode();
Utils.copy("wikicode", wikicode, getContext());
/**
* Shows welcome message if user has no contributions yet i.e. new user.
*/
@Override
public void showWelcomeTip(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
final String url =
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
.getWikipediaPageTitle();
Utils.handleWebUrl(getContext(), Uri.parse(url));
}
/**
* Responsible to set progress bar invisible and visible
*
* @param shouldShow True when contributions list should be hidden.
*/
@Override
public void showProgress(final boolean shouldShow) {
progressBar.setVisibility(shouldShow ? VISIBLE : GONE);
}
public Integer getContributionStateAt(int position) {
return adapter.getContributionForPosition(position).getState();
}
@Override
public void showNoContributionsUI(final boolean shouldShow) {
noContributionsYet.setVisibility(shouldShow ? VISIBLE : GONE);
}
public interface Callback {
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
final GridLayoutManager layoutManager = (GridLayoutManager) rvContributionsList
.getLayoutManager();
outState.putParcelable(RV_STATE, layoutManager.onSaveInstanceState());
}
void notifyDataSetChanged();
@Override
public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
super.onViewStateRestored(savedInstanceState);
if (null != savedInstanceState) {
final Parcelable savedRecyclerLayoutState = savedInstanceState.getParcelable(RV_STATE);
rvContributionsList.getLayoutManager().onRestoreInstanceState(savedRecyclerLayoutState);
}
}
void retryUpload(Contribution contribution);
@Override
public void retryUpload(final Contribution contribution) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.retryUpload(contribution);
}
}
void showDetail(int position, boolean isWikipediaButtonDisplayed);
@Override
public void deleteUpload(final Contribution contribution) {
contributionsListPresenter.deleteUpload(contribution);
}
void pauseUpload(Contribution contribution);
@Override
public void openMediaDetail(final int position, boolean isWikipediaButtonDisplayed) {
if (null != callback) {//Just being safe, ideally they won't be called when detached
callback.showDetail(position, isWikipediaButtonDisplayed);
}
}
// Notify the viewpager that number of items have changed.
void viewPagerNotifyDataSetChanged();
}
/**
* Handle callback for wikipedia icon clicked
*
* @param contribution
*/
@Override
public void addImageToWikipedia(Contribution contribution) {
DialogUtil.showAlertDialog(getActivity(),
getString(R.string.add_picture_to_wikipedia_article_title),
String.format(getString(R.string.add_picture_to_wikipedia_article_desc),
Locale.getDefault().getDisplayLanguage()),
() -> {
showAddImageToWikipediaInstructions(contribution);
}, () -> {
// do nothing
});
}
/**
* Pauses the current upload
*
* @param contribution
*/
@Override
public void pauseUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.pausing_upload);
callback.pauseUpload(contribution);
}
/**
* Resumes the current upload
*
* @param contribution
*/
@Override
public void resumeUpload(Contribution contribution) {
ViewUtil.showShortToast(getContext(), R.string.resuming_upload);
callback.retryUpload(contribution);
}
/**
* Display confirmation dialog with instructions when the user tries to add image to wikipedia
*
* @param contribution
*/
private void showAddImageToWikipediaInstructions(Contribution contribution) {
FragmentManager fragmentManager = getFragmentManager();
WikipediaInstructionsDialogFragment fragment = WikipediaInstructionsDialogFragment
.newInstance(contribution);
fragment.setCallback(this::onConfirmClicked);
fragment.show(fragmentManager, "WikimediaFragment");
}
public Media getMediaAtPosition(final int i) {
if (adapter.getContributionForPosition(i) != null) {
return adapter.getContributionForPosition(i).getMedia();
}
return null;
}
public int getTotalMediaCount() {
return contributionsSize;
}
/**
* Open the editor for the language Wikipedia
*
* @param contribution
*/
@Override
public void onConfirmClicked(@Nullable Contribution contribution, boolean copyWikicode) {
if (copyWikicode) {
String wikicode = contribution.getMedia().getWikiCode();
Utils.copy("wikicode", wikicode, getContext());
}
final String url =
languageWikipediaSite.mobileUrl() + "/wiki/" + contribution.getWikidataPlace()
.getWikipediaPageTitle();
Utils.handleWebUrl(getContext(), Uri.parse(url));
}
public Integer getContributionStateAt(int position) {
return adapter.getContributionForPosition(position).getState();
}
public interface Callback {
void notifyDataSetChanged();
void retryUpload(Contribution contribution);
void showDetail(int position, boolean isWikipediaButtonDisplayed);
void pauseUpload(Contribution contribution);
// Notify the viewpager that number of items have changed.
void viewPagerNotifyDataSetChanged();
}
}

View file

@ -15,57 +15,58 @@ import javax.inject.Named;
*/
public class ContributionsListPresenter implements UserActionListener {
private final ContributionBoundaryCallback contributionBoundaryCallback;
private final ContributionsRepository repository;
private final Scheduler ioThreadScheduler;
private final ContributionBoundaryCallback contributionBoundaryCallback;
private final ContributionsRepository repository;
private final Scheduler ioThreadScheduler;
private final CompositeDisposable compositeDisposable;
private final CompositeDisposable compositeDisposable;
LiveData<PagedList<Contribution>> contributionList;
LiveData<PagedList<Contribution>> contributionList;
@Inject
ContributionsListPresenter(
final ContributionBoundaryCallback contributionBoundaryCallback,
final ContributionsRepository repository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Inject
ContributionsListPresenter(
final ContributionBoundaryCallback contributionBoundaryCallback,
final ContributionsRepository repository,
@Named(CommonsApplicationModule.IO_THREAD) final Scheduler ioThreadScheduler) {
this.contributionBoundaryCallback = contributionBoundaryCallback;
this.repository = repository;
this.ioThreadScheduler = ioThreadScheduler;
compositeDisposable = new CompositeDisposable();
}
@Override
public void onAttachView(final ContributionsListContract.View view) {
}
@Override
public void onAttachView(final ContributionsListContract.View view) {
}
/**
* Setup the paged list. This method sets the configuration for paged list and ties it up with the
* live data object. This method can be tweaked to update the lazy loading behavior of the
* contributions list
*/
void setup() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
contributionList = (new LivePagedListBuilder(repository.fetchContributions(), pagedListConfig)
.setBoundaryCallback(contributionBoundaryCallback)).build();
}
/**
* Setup the paged list. This method sets the configuration for paged list and ties it up with
* the live data object. This method can be tweaked to update the lazy loading behavior of the
* contributions list
*/
void setup() {
final PagedList.Config pagedListConfig =
(new PagedList.Config.Builder())
.setPrefetchDistance(50)
.setPageSize(10).build();
contributionList = (new LivePagedListBuilder(repository.fetchContributions(),
pagedListConfig)
.setBoundaryCallback(contributionBoundaryCallback)).build();
}
@Override
public void onDetachView() {
compositeDisposable.clear();
}
@Override
public void onDetachView() {
compositeDisposable.clear();
}
/**
* Delete a failed contribution from the local db
*/
@Override
public void deleteUpload(final Contribution contribution) {
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
/**
* Delete a failed contribution from the local db
*/
@Override
public void deleteUpload(final Contribution contribution) {
compositeDisposable.add(repository
.deleteContributionFromDB(contribution)
.subscribeOn(ioThreadScheduler)
.subscribe());
}
}

View file

@ -4,13 +4,12 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.Toolbar;
import androidx.fragment.app.Fragment;
@ -22,6 +21,7 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import fr.free.nrw.commons.CommonsApplication;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.WelcomeActivity;
import fr.free.nrw.commons.auth.SessionManager;
import fr.free.nrw.commons.bookmarks.BookmarkFragment;
import fr.free.nrw.commons.explore.ExploreFragment;
@ -247,17 +247,23 @@ public class MainActivity extends BaseActivity
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
String currentFragmentName = savedInstanceState.getString("activeFragment");
if(currentFragmentName == ActiveFragment.CONTRIBUTIONS.name()) {
String activeFragmentName = savedInstanceState.getString("activeFragment");
if(activeFragmentName != null) {
restoreActiveFragment(activeFragmentName);
}
}
private void restoreActiveFragment(@NonNull String fragmentName) {
if(fragmentName.equals(ActiveFragment.CONTRIBUTIONS.name())) {
setTitle(getString(R.string.contributions_fragment));
loadFragment(ContributionsFragment.newInstance(),false);
}else if(currentFragmentName == ActiveFragment.NEARBY.name()) {
}else if(fragmentName.equals(ActiveFragment.NEARBY.name())) {
setTitle(getString(R.string.nearby_fragment));
loadFragment(NearbyParentFragment.newInstance(),false);
}else if(currentFragmentName == ActiveFragment.EXPLORE.name()) {
}else if(fragmentName.equals(ActiveFragment.EXPLORE.name())) {
setTitle(getString(R.string.navigation_item_explore));
loadFragment(ExploreFragment.newInstance(),false);
}else if(currentFragmentName == ActiveFragment.BOOKMARK.name()) {
}else if(fragmentName.equals(ActiveFragment.BOOKMARK.name())) {
setTitle(getString(R.string.favorites));
loadFragment(BookmarkFragment.newInstance(),false);
}
@ -281,7 +287,11 @@ public class MainActivity extends BaseActivity
}
} else if (exploreFragment != null && activeFragment == ActiveFragment.EXPLORE) {
// Means that explore fragment is visible
exploreFragment.onBackPressed();
if (!exploreFragment.onBackPressed()) {
if (applicationKvStore.getBoolean("login_skipped")) {
super.onBackPressed();
}
}
} else if (bookmarkFragment != null && activeFragment == ActiveFragment.BOOKMARK) {
// Means that bookmark fragment is visible
bookmarkFragment.onBackPressed();
@ -347,6 +357,11 @@ public class MainActivity extends BaseActivity
@Override
protected void onResume() {
super.onResume();
if ((applicationKvStore.getBoolean("firstrun", true)) &&
(!applicationKvStore.getBoolean("login_skipped"))) {
WelcomeActivity.startYourself(this);
}
}
@Override

View file

@ -17,7 +17,7 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class], version = 8, exportSchema = false)
@TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao
abstract fun DepictsDao (): DepictsDao;
abstract fun UploadedStatusDao(): UploadedStatusDao;
abstract fun contributionDao(): ContributionDao
abstract fun DepictsDao(): DepictsDao;
abstract fun UploadedStatusDao(): UploadedStatusDao;
}

View file

@ -17,10 +17,13 @@ import fr.free.nrw.commons.R;
import fr.free.nrw.commons.ViewPagerAdapter;
import fr.free.nrw.commons.contributions.MainActivity;
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.theme.BaseActivity;
import fr.free.nrw.commons.utils.ActivityUtils;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
public class ExploreFragment extends CommonsDaggerSupportFragment {
@ -35,6 +38,9 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
ViewPagerAdapter viewPagerAdapter;
private ExploreListRootFragment featuredRootFragment;
private ExploreListRootFragment mobileRootFragment;
@Inject
@Named("default_preferences")
public JsonKvStore applicationKvStore;
public void setScroll(boolean canScroll){
viewPager.setCanScroll(canScroll);
@ -95,20 +101,21 @@ public class ExploreFragment extends CommonsDaggerSupportFragment {
viewPagerAdapter.notifyDataSetChanged();
}
public void onBackPressed() {
public boolean onBackPressed() {
if (tabLayout.getSelectedTabPosition() == 0) {
if(featuredRootFragment.backPressed()){
// Event is handled by the Fragment we need not do anything.
return;
if (featuredRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
} else {
if(mobileRootFragment.backPressed()){
// Event is handled by the Fragment we need not do anything.
return;
if (mobileRootFragment.backPressed()) {
((BaseActivity) getActivity()).getSupportActionBar()
.setDisplayHomeAsUpEnabled(false);
return true;
}
}
// Event is not handled by the fragment ( i.e performed back action ) therefore change action bar.
((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(false);
return false;
}
/**

View file

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

View file

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

View file

@ -27,8 +27,11 @@ 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;
@Inject
RecentSearchesDao recentSearchesDao;
@BindView(R.id.recent_searches_list)
ListView recentSearchesList;
List<String> recentSearches;
ArrayAdapter adapter;
@BindView(R.id.recent_searches_delete_button)
@ -38,16 +41,16 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
Bundle savedInstanceState) {
View rootView = inflater.inflate(R.layout.fragment_search_history, container, false);
ButterKnife.bind(this, rootView);
recentSearches = recentSearchesDao.recentSearches(10);
if(recentSearches.isEmpty()) {
if (recentSearches.isEmpty()) {
recent_searches_delete_button.setVisibility(View.GONE);
recent_searches_text_view.setText(R.string.no_recent_searches);
}
recent_searches_delete_button.setOnClickListener(v -> {
new AlertDialog.Builder(getContext())
.setMessage(getString(R.string.delete_recent_searches_dialog))
@ -55,9 +58,11 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
recentSearchesDao.deleteAll();
recent_searches_delete_button.setVisibility(View.GONE);
recent_searches_text_view.setText(R.string.no_recent_searches);
Toast.makeText(getContext(),getString(R.string.search_history_deleted),Toast.LENGTH_SHORT).show();
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);
adapter = new ArrayAdapter<>(getContext(), R.layout.item_recent_searches,
recentSearches);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
dialog.dismiss();
@ -67,24 +72,26 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
.show();
});
adapter = new ArrayAdapter<>(requireContext(), R.layout.item_recent_searches, recentSearches);
adapter = new ArrayAdapter<>(requireContext(), R.layout.item_recent_searches,
recentSearches);
recentSearchesList.setAdapter(adapter);
recentSearchesList.setOnItemClickListener((parent, view, position, id) -> (
(SearchActivity)getContext()).updateText(recentSearches.get(position)));
(SearchActivity) getContext()).updateText(recentSearches.get(position)));
recentSearchesList.setOnItemLongClickListener((parent, view, position, id) -> {
new AlertDialog.Builder(getContext())
.setMessage(R.string.delete_search_dialog)
.setPositiveButton(getString(R.string.delete).toUpperCase(),((dialog, which) -> {
recentSearchesDao.delete(recentSearchesDao.find(recentSearches.get(position)));
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<>(getContext(), R.layout.item_recent_searches, recentSearches);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
dialog.dismiss();
}))
.setNegativeButton(android.R.string.cancel,null)
.create()
.show();
.setMessage(R.string.delete_search_dialog)
.setPositiveButton(getString(R.string.delete).toUpperCase(), ((dialog, which) -> {
recentSearchesDao.delete(recentSearchesDao.find(recentSearches.get(position)));
recentSearches = recentSearchesDao.recentSearches(10);
adapter = new ArrayAdapter<>(getContext(), R.layout.item_recent_searches,
recentSearches);
recentSearchesList.setAdapter(adapter);
adapter.notifyDataSetChanged();
dialog.dismiss();
}))
.setNegativeButton(android.R.string.cancel, null)
.create()
.show();
return true;
});
updateRecentSearches();
@ -92,8 +99,8 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
}
/**
* This method is called on back press of activity
* so we are updating the list from database to refresh the recent searches list.
* 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() {
@ -108,7 +115,7 @@ public class RecentSearchesFragment extends CommonsDaggerSupportFragment {
recentSearches = recentSearchesDao.recentSearches(10);
adapter.notifyDataSetChanged();
if(!recentSearches.isEmpty()) {
if (!recentSearches.isEmpty()) {
recent_searches_delete_button.setVisibility(View.VISIBLE);
recent_searches_text_view.setText(R.string.search_recent_header);
}

View file

@ -0,0 +1,67 @@
package fr.free.nrw.commons.media;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
import fr.free.nrw.commons.R;
import java.util.List;
/**
* Adapter for Caption Listview
*/
public class CaptionListViewAdapter extends BaseAdapter {
List<Caption> captions;
public CaptionListViewAdapter(final List<Caption> captions) {
this.captions = captions;
}
/**
* @return size of captions list
*/
@Override
public int getCount() {
return captions.size();
}
/**
* @return Object at position i
*/
@Override
public Object getItem(final int i) {
return null;
}
/**
* @return id for current item
*/
@Override
public long getItemId(final int i) {
return 0;
}
/**
* inflate the view and bind data with UI
*/
@Override
public View getView(final int i, final View view, final ViewGroup viewGroup) {
final TextView captionLanguageTextView;
final TextView captionTextView;
final View captionLayout = LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.caption_item, null);
captionLanguageTextView = captionLayout.findViewById(R.id.caption_language_textview);
captionTextView = captionLayout.findViewById(R.id.caption_text);
if (captions.size() == 1 && captions.get(0).getValue().equals("No Caption")) {
captionLanguageTextView.setText(captions.get(i).getLanguage());
captionTextView.setText(captions.get(i).getValue());
} else {
captionLanguageTextView.setText(captions.get(i).getLanguage() + ":");
captionTextView.setText(captions.get(i).getValue());
}
return captionLayout;
}
}

View file

@ -35,195 +35,199 @@ import timber.log.Timber;
public class CustomOkHttpNetworkFetcher
extends BaseNetworkFetcher<CustomOkHttpNetworkFetcher.OkHttpNetworkFetchState> {
private static final String QUEUE_TIME = "queue_time";
private static final String FETCH_TIME = "fetch_time";
private static final String TOTAL_TIME = "total_time";
private static final String IMAGE_SIZE = "image_size";
private final Call.Factory mCallFactory;
private final @Nullable
CacheControl mCacheControl;
private Executor mCancellationExecutor;
private JsonKvStore defaultKvStore;
private static final String QUEUE_TIME = "queue_time";
private static final String FETCH_TIME = "fetch_time";
private static final String TOTAL_TIME = "total_time";
private static final String IMAGE_SIZE = "image_size";
private final Call.Factory mCallFactory;
private final @Nullable
CacheControl mCacheControl;
private Executor mCancellationExecutor;
private JsonKvStore defaultKvStore;
/**
* @param okHttpClient client to use
*/
@Inject
public CustomOkHttpNetworkFetcher(OkHttpClient okHttpClient,
@Named("default_preferences") JsonKvStore defaultKvStore) {
this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore);
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
*/
public CustomOkHttpNetworkFetcher(Call.Factory callFactory, Executor cancellationExecutor,
JsonKvStore defaultKvStore) {
this(callFactory, cancellationExecutor, defaultKvStore, true);
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
*/
public CustomOkHttpNetworkFetcher(
Call.Factory callFactory, Executor cancellationExecutor, JsonKvStore defaultKvStore,
boolean disableOkHttpCache) {
this.defaultKvStore = defaultKvStore;
mCallFactory = callFactory;
mCancellationExecutor = cancellationExecutor;
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
}
@Override
public OkHttpNetworkFetchState createFetchState(
Consumer<EncodedImage> consumer, ProducerContext context) {
return new OkHttpNetworkFetchState(consumer, context);
}
@Override
public void fetch(
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
fetchState.submitTime = SystemClock.elapsedRealtime();
final Uri uri = fetchState.getUri();
try {
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
Timber.d("Skipping loading of image as limited connection mode is enabled");
callback.onFailure(
new Exception("Failing image request as limited connection mode is enabled"));
return;
}
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
if (mCacheControl != null) {
requestBuilder.cacheControl(mCacheControl);
}
final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
if (bytesRange != null) {
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
}
fetchWithRequest(fetchState, callback, requestBuilder.build());
} catch (Exception e) {
// handle error while creating the request
callback.onFailure(e);
/**
* @param okHttpClient client to use
*/
@Inject
public CustomOkHttpNetworkFetcher(OkHttpClient okHttpClient,
@Named("default_preferences") JsonKvStore defaultKvStore) {
this(okHttpClient, okHttpClient.dispatcher().executorService(), defaultKvStore);
}
}
@Override
public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) {
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
*/
public CustomOkHttpNetworkFetcher(Call.Factory callFactory, Executor cancellationExecutor,
JsonKvStore defaultKvStore) {
this(callFactory, cancellationExecutor, defaultKvStore, true);
}
@Override
public Map<String, String> getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) {
Map<String, String> extraMap = new HashMap<>(4);
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
extraMap.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
extraMap.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
return extraMap;
}
/**
* @param callFactory custom {@link Call.Factory} for fetching image from the network
* @param cancellationExecutor executor on which fetching cancellation is performed if
* cancellation is requested from the UI Thread
* @param disableOkHttpCache true if network requests should not be cached by OkHttp
*/
public CustomOkHttpNetworkFetcher(
Call.Factory callFactory, Executor cancellationExecutor, JsonKvStore defaultKvStore,
boolean disableOkHttpCache) {
this.defaultKvStore = defaultKvStore;
mCallFactory = callFactory;
mCancellationExecutor = cancellationExecutor;
mCacheControl = disableOkHttpCache ? new CacheControl.Builder().noStore().build() : null;
}
protected void fetchWithRequest(
final OkHttpNetworkFetchState fetchState,
final NetworkFetcher.Callback callback,
final Request request) {
final Call call = mCallFactory.newCall(request);
@Override
public OkHttpNetworkFetchState createFetchState(
Consumer<EncodedImage> consumer, ProducerContext context) {
return new OkHttpNetworkFetchState(consumer, context);
}
fetchState
.getContext()
.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(
new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});
@Override
public void fetch(
final OkHttpNetworkFetchState fetchState, final NetworkFetcher.Callback callback) {
fetchState.submitTime = SystemClock.elapsedRealtime();
final Uri uri = fetchState.getUri();
call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
fetchState.responseTime = SystemClock.elapsedRealtime();
final ResponseBody body = response.body();
try {
if (!response.isSuccessful()) {
handleException(
call, new IOException("Unexpected HTTP code " + response), callback);
try {
if (defaultKvStore
.getBoolean(CommonsApplication.IS_LIMITED_CONNECTION_MODE_ENABLED, false)) {
Timber.d("Skipping loading of image as limited connection mode is enabled");
callback.onFailure(
new Exception("Failing image request as limited connection mode is enabled"));
return;
}
BytesRange responseRange =
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
if (responseRange != null
&& !(responseRange.from == 0
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
// Only treat as a partial image if the range is not all of the content
fetchState.setResponseBytesRange(responseRange);
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
}
long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
body.close();
}
}
final Request.Builder requestBuilder = new Request.Builder().url(uri.toString()).get();
@Override
public void onFailure(Call call, IOException e) {
handleException(call, e, callback);
}
});
}
if (mCacheControl != null) {
requestBuilder.cacheControl(mCacheControl);
}
/**
* Handles exceptions.
*
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught after
* request cancellation, then the exception is interpreted as successful cancellation and
* onCancellation is called. Otherwise onFailure is called.
*/
private void handleException(final Call call, final Exception e, final Callback callback) {
if (call.isCanceled()) {
callback.onCancellation();
} else {
callback.onFailure(e);
final BytesRange bytesRange = fetchState.getContext().getImageRequest().getBytesRange();
if (bytesRange != null) {
requestBuilder.addHeader("Range", bytesRange.toHttpRangeHeaderValue());
}
fetchWithRequest(fetchState, callback, requestBuilder.build());
} catch (Exception e) {
// handle error while creating the request
callback.onFailure(e);
}
}
}
public static class OkHttpNetworkFetchState extends FetchState {
public long submitTime;
public long responseTime;
public long fetchCompleteTime;
public OkHttpNetworkFetchState(
Consumer<EncodedImage> consumer, ProducerContext producerContext) {
super(consumer, producerContext);
@Override
public void onFetchCompletion(OkHttpNetworkFetchState fetchState, int byteSize) {
fetchState.fetchCompleteTime = SystemClock.elapsedRealtime();
}
@Override
public Map<String, String> getExtraMap(OkHttpNetworkFetchState fetchState, int byteSize) {
Map<String, String> extraMap = new HashMap<>(4);
extraMap.put(QUEUE_TIME, Long.toString(fetchState.responseTime - fetchState.submitTime));
extraMap
.put(FETCH_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.responseTime));
extraMap
.put(TOTAL_TIME, Long.toString(fetchState.fetchCompleteTime - fetchState.submitTime));
extraMap.put(IMAGE_SIZE, Integer.toString(byteSize));
return extraMap;
}
protected void fetchWithRequest(
final OkHttpNetworkFetchState fetchState,
final NetworkFetcher.Callback callback,
final Request request) {
final Call call = mCallFactory.newCall(request);
fetchState
.getContext()
.addCallbacks(
new BaseProducerContextCallbacks() {
@Override
public void onCancellationRequested() {
if (Looper.myLooper() != Looper.getMainLooper()) {
call.cancel();
} else {
mCancellationExecutor.execute(
new Runnable() {
@Override
public void run() {
call.cancel();
}
});
}
}
});
call.enqueue(
new okhttp3.Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
fetchState.responseTime = SystemClock.elapsedRealtime();
final ResponseBody body = response.body();
try {
if (!response.isSuccessful()) {
handleException(
call, new IOException("Unexpected HTTP code " + response),
callback);
return;
}
BytesRange responseRange =
BytesRange.fromContentRangeHeader(response.header("Content-Range"));
if (responseRange != null
&& !(responseRange.from == 0
&& responseRange.to == BytesRange.TO_END_OF_CONTENT)) {
// Only treat as a partial image if the range is not all of the content
fetchState.setResponseBytesRange(responseRange);
fetchState.setOnNewResultStatusFlags(Consumer.IS_PARTIAL_RESULT);
}
long contentLength = body.contentLength();
if (contentLength < 0) {
contentLength = 0;
}
callback.onResponse(body.byteStream(), (int) contentLength);
} catch (Exception e) {
handleException(call, e, callback);
} finally {
body.close();
}
}
@Override
public void onFailure(Call call, IOException e) {
handleException(call, e, callback);
}
});
}
/**
* Handles exceptions.
*
* <p>OkHttp notifies callers of cancellations via an IOException. If IOException is caught
* after
* request cancellation, then the exception is interpreted as successful cancellation and
* onCancellation is called. Otherwise onFailure is called.
*/
private void handleException(final Call call, final Exception e, final Callback callback) {
if (call.isCanceled()) {
callback.onCancellation();
} else {
callback.onFailure(e);
}
}
public static class OkHttpNetworkFetchState extends FetchState {
public long submitTime;
public long responseTime;
public long fetchCompleteTime;
public OkHttpNetworkFetchState(
Consumer<EncodedImage> consumer, ProducerContext producerContext) {
super(consumer, producerContext);
}
}
}
}

View file

@ -18,16 +18,20 @@ import android.os.Bundle;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.webkit.WebView;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.ScrollView;
import android.widget.SearchView;
@ -86,6 +90,7 @@ import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Named;
import org.apache.commons.lang3.StringUtils;
import org.wikipedia.language.AppLanguageLookUpTable;
import org.wikipedia.util.DateUtil;
import timber.log.Timber;
@ -140,7 +145,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
JsonKvStore applicationKvStore;
private int initialListTop = 0;
@BindView(R.id.description_webview)
WebView descriptionWebView;
@BindView(R.id.mediaDetailFrameLayout)
FrameLayout frameLayout;
@BindView(R.id.mediaDetailImageView)
@ -203,6 +209,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
TextView existingCategories;
@BindView(R.id.no_results_found)
TextView noResultsFound;
@BindView(R.id.dummy_caption_description_container)
LinearLayout showCaptionAndDescriptionContainer;
@BindView(R.id.show_caption_description_textview)
TextView showCaptionDescriptionTextView;
@BindView(R.id.caption_listview)
ListView captionsListView;
@BindView(R.id.caption_label)
TextView captionLabel;
@BindView(R.id.description_label)
TextView descriptionLabel;
@BindView(R.id.pb_circular)
ProgressBar progressBar;
String descriptionHtmlCode;
@BindView(R.id.progressBarDeletion)
ProgressBar progressBarDeletion;
@ -302,6 +321,9 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
if(applicationKvStore.getBoolean("login_skipped")){
delete.setVisibility(GONE);
}
handleBackEvent(view);
/**
* Gets the height of the frame layout as soon as the view is ready and updates aspect ratio
* of the picture.
@ -317,14 +339,6 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
return view;
}
@Override
public void onAttach(final Context context) {
super.onAttach(context);
if (getParentFragment() != null) {
callback = (Callback) getParentFragment();
}
}
@OnClick(R.id.mediaDetailImageViewSpacer)
public void launchZoomActivity(View view) {
if (media.getImageUrl() != null) {
@ -626,6 +640,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
categoryEditSearchRecyclerViewAdapter.addToCategories(media.getCategories());
updateSelectedCategoriesTextView(categoryEditSearchRecyclerViewAdapter.getCategories());
categoryRecyclerView.setVisibility(GONE);
updateCategoryList();
if (media.getAuthor() == null || media.getAuthor().equals("")) {
@ -661,20 +676,28 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
public void updateSelectedCategoriesTextView(List<String> selectedCategories) {
if (selectedCategories == null || selectedCategories.size() == 0) {
updateCategoriesButton.setClickable(false);
}
if (selectedCategories != null) {
updateCategoriesButton.setAlpha(.5f);
} else {
existingCategories.setText(StringUtils.join(selectedCategories,", "));
updateCategoriesButton.setClickable(true);
if (selectedCategories.equals(media.getCategories())) {
updateCategoriesButton.setClickable(false);
updateCategoriesButton.setAlpha(.5f);
} else {
updateCategoriesButton.setClickable(true);
updateCategoriesButton.setAlpha(1f);
}
}
}
@Override
public void noResultsFound() {
categoryRecyclerView.setVisibility(GONE);
noResultsFound.setVisibility(VISIBLE);
}
@Override
public void someResultsFound() {
categoryRecyclerView.setVisibility(VISIBLE);
noResultsFound.setVisibility(GONE);
}
@ -760,6 +783,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
public void displayHideCategorySearch() {
showCaptionAndDescriptionContainer.setVisibility(GONE);
if (dummyCategoryEditContainer.getVisibility() != VISIBLE) {
dummyCategoryEditContainer.setVisibility(VISIBLE);
} else {
@ -791,6 +815,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
.defaultLocation(new CameraPosition.Builder()
.target(new LatLng(defaultLatitude, defaultLongitude))
.zoom(16).build())
.activityKey("MediaActivity")
.build(getActivity()), REQUEST_CODE);
}
@ -1127,6 +1152,97 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment implements
}
}
@OnClick(R.id.show_caption_description_textview)
void showCaptionAndDescription() {
dummyCategoryEditContainer.setVisibility(GONE);
if (showCaptionAndDescriptionContainer.getVisibility() == GONE) {
showCaptionAndDescriptionContainer.setVisibility(VISIBLE);
setUpCaptionAndDescriptionLayout();
} else {
showCaptionAndDescriptionContainer.setVisibility(GONE);
}
}
/**
* setUp Caption And Description Layout
*/
private void setUpCaptionAndDescriptionLayout() {
List<Caption> captions = getCaptions();
if (descriptionHtmlCode == null) {
progressBar.setVisibility(VISIBLE);
}
getDescription();
CaptionListViewAdapter adapter = new CaptionListViewAdapter(captions);
captionsListView.setAdapter(adapter);
}
/**
* Generate the caption with language
*/
private List<Caption> getCaptions() {
List<Caption> captionList = new ArrayList<>();
Map<String, String> captions = media.getCaptions();
AppLanguageLookUpTable appLanguageLookUpTable = new AppLanguageLookUpTable(getContext());
for (Map.Entry<String, String> map : captions.entrySet()) {
String language = appLanguageLookUpTable.getLocalizedName(map.getKey());
String languageCaption = map.getValue();
captionList.add(new Caption(language, languageCaption));
}
if (captionList.size() == 0) {
captionList.add(new Caption("", "No Caption"));
}
return captionList;
}
private void getDescription() {
compositeDisposable.add(mediaDataExtractor.getHtmlOfPage(media.getFilename())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::extractDescription, Timber::e));
}
/**
* extract the description from html of imagepage
*/
private void extractDescription(String s) {
String descriptionClassName = "<td class=\"description\">";
int start = s.indexOf(descriptionClassName) + descriptionClassName.length();
int end = s.indexOf("</td>", start);
descriptionHtmlCode = "";
for (int i = start; i < end; i++) {
descriptionHtmlCode = descriptionHtmlCode + s.toCharArray()[i];
}
descriptionWebView
.loadDataWithBaseURL(null, descriptionHtmlCode, "text/html", "utf-8", null);
progressBar.setVisibility(GONE);
}
/**
* Handle back event when fragment when showCaptionAndDescriptionContainer is visible
*/
private void handleBackEvent(View view) {
view.setFocusableInTouchMode(true);
view.requestFocus();
view.setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(View view, int keycode, KeyEvent keyEvent) {
if (keycode == KeyEvent.KEYCODE_BACK) {
if (showCaptionAndDescriptionContainer.getVisibility() == VISIBLE) {
showCaptionAndDescriptionContainer.setVisibility(GONE);
return true;
}
}
return false;
}
});
}
public interface Callback {
void nominatingForDeletion(int index);
}

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import io.reactivex.Observable;
import io.reactivex.Single;
import org.wikipedia.wikidata.Entities;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;

View file

@ -12,6 +12,8 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AppCompatActivity;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
@ -100,7 +102,16 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
pager.addOnPageChangeListener(this);
adapter = new MediaDetailAdapter(getChildFragmentManager());
((BaseActivity)getActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
if (getActivity() != null) {
final ActionBar actionBar = ((AppCompatActivity) getActivity()).getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
}
}
pager.setAdapter(adapter);
if (savedInstanceState != null) {
final int pageNumber = savedInstanceState.getInt("current-page");
pager.setCurrentItem(pageNumber, false);

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.media;
import io.reactivex.Single;
import java.util.Map;
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;

View file

@ -42,319 +42,325 @@ import timber.log.Timber;
@Singleton
public class OkHttpJsonApiClient {
private final OkHttpClient okHttpClient;
private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final HttpUrl wikiMediaTestToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final Gson gson;
private final OkHttpClient okHttpClient;
private final DepictsClient depictsClient;
private final HttpUrl wikiMediaToolforgeUrl;
private final HttpUrl wikiMediaTestToolforgeUrl;
private final String sparqlQueryUrl;
private final String campaignsUrl;
private final Gson gson;
@Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl,
HttpUrl wikiMediaTestToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.gson = gson;
}
/**
* The method will gradually calls the leaderboard API and fetches the leaderboard
* @param userName username of leaderboard user
* @param duration duration for leaderboard
* @param category category for leaderboard
* @param limit page size limit for list
* @param offset offset for the list
* @return LeaderboardResponse object
*/
@NonNull
public Observable<LeaderboardResponse> getLeaderboard(String userName, String duration, String category, String limit, String offset) {
final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ LEADERBOARD_END_POINT;
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new LeaderboardResponse();
}
Timber.d("Response for leaderboard is %s", json);
try {
return gson.fromJson(json, LeaderboardResponse.class);
} catch (Exception e) {
return new LeaderboardResponse();
}
}
return new LeaderboardResponse();
});
}
/**
* This method will update the leaderboard user avatar
* @param username username to update
* @param avatar url of the new avatar
* @return UpdateAvatarResponse object
*/
@NonNull
public Single<UpdateAvatarResponse> setAvatar(String username, String avatar) {
final String urlTemplate = wikiMediaTestToolforgeUrl
+ UPDATE_AVATAR_END_POINT;
return Single.fromCallable(() -> {
String url = String.format(Locale.ENGLISH,
urlTemplate,
username,
avatar);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", username);
urlBuilder.addQueryParameter("avatar", avatar);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
try {
return gson.fromJson(json, UpdateAvatarResponse.class);
} catch (Exception e) {
return new UpdateAvatarResponse();
}
}
return null;
});
}
@NonNull
public Single<Integer> getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("uploadsbyuser.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
@Inject
public OkHttpJsonApiClient(OkHttpClient okHttpClient,
DepictsClient depictsClient,
HttpUrl wikiMediaToolforgeUrl,
HttpUrl wikiMediaTestToolforgeUrl,
String sparqlQueryUrl,
String campaignsUrl,
Gson gson) {
this.okHttpClient = okHttpClient;
this.depictsClient = depictsClient;
this.wikiMediaToolforgeUrl = wikiMediaToolforgeUrl;
this.wikiMediaTestToolforgeUrl = wikiMediaTestToolforgeUrl;
this.sparqlQueryUrl = sparqlQueryUrl;
this.campaignsUrl = campaignsUrl;
this.gson = gson;
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.isSuccessful()) {
ResponseBody responseBody = response.body();
if (null != responseBody) {
String responseBodyString = responseBody.string().trim();
if (!TextUtils.isEmpty(responseBodyString)) {
try {
return Integer.parseInt(responseBodyString);
} catch (NumberFormatException e) {
Timber.e(e);
/**
* The method will gradually calls the leaderboard API and fetches the leaderboard
*
* @param userName username of leaderboard user
* @param duration duration for leaderboard
* @param category category for leaderboard
* @param limit page size limit for list
* @param offset offset for the list
* @return LeaderboardResponse object
*/
@NonNull
public Observable<LeaderboardResponse> getLeaderboard(String userName, String duration,
String category, String limit, String offset) {
final String fetchLeaderboardUrlTemplate = wikiMediaTestToolforgeUrl
+ LEADERBOARD_END_POINT;
String url = String.format(Locale.ENGLISH,
fetchLeaderboardUrlTemplate,
userName,
duration,
category,
limit,
offset);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
urlBuilder.addQueryParameter("duration", duration);
urlBuilder.addQueryParameter("category", category);
urlBuilder.addQueryParameter("limit", limit);
urlBuilder.addQueryParameter("offset", offset);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new LeaderboardResponse();
}
Timber.d("Response for leaderboard is %s", json);
try {
return gson.fromJson(json, LeaderboardResponse.class);
} catch (Exception e) {
return new LeaderboardResponse();
}
}
}
}
}
return 0;
});
}
@NonNull
public Single<Integer> getWikidataEdits(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("wikidataedits.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
return new LeaderboardResponse();
});
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
/**
* This method will update the leaderboard user avatar
*
* @param username username to update
* @param avatar url of the new avatar
* @return UpdateAvatarResponse object
*/
@NonNull
public Single<UpdateAvatarResponse> setAvatar(String username, String avatar) {
final String urlTemplate = wikiMediaTestToolforgeUrl
+ UPDATE_AVATAR_END_POINT;
return Single.fromCallable(() -> {
String url = String.format(Locale.ENGLISH,
urlTemplate,
username,
avatar);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", username);
urlBuilder.addQueryParameter("avatar", avatar);
Timber.i("Url %s", urlBuilder.toString());
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
try {
return gson.fromJson(json, UpdateAvatarResponse.class);
} catch (Exception e) {
return new UpdateAvatarResponse();
}
}
return null;
});
}
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null &&
response.isSuccessful() && response.body() != null) {
String json = response.body().string();
if (json == null) {
return 0;
}
GetWikidataEditCountResponse countResponse = gson
.fromJson(json, GetWikidataEditCountResponse.class);
if (null != countResponse) {
return countResponse.getWikidataEditCount();
}
}
return 0;
});
}
@NonNull
public Single<Integer> getUploadCount(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("uploadsbyuser.py")
.addQueryParameter("user", userName);
/**
* 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 MediaWiki user name
* @return
*/
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
: "/feedback.py");
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
} catch (Exception e) {
return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, "");
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
}
return null;
});
}
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.isSuccessful()) {
ResponseBody responseBody = response.body();
if (null != responseBody) {
String responseBodyString = responseBody.string().trim();
if (!TextUtils.isEmpty(responseBodyString)) {
try {
return Integer.parseInt(responseBodyString);
} catch (NumberFormatException e) {
Timber.e(e);
}
}
}
}
return 0;
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String language, double radius) throws IOException {
@NonNull
public Single<Integer> getWikidataEdits(String userName) {
HttpUrl.Builder urlBuilder = wikiMediaToolforgeUrl.newBuilder();
urlBuilder
.addPathSegments("wikidataedits.py")
.addQueryParameter("user", userName);
if (ConfigUtils.isBetaFlavour()) {
urlBuilder.addQueryParameter("labs", "commonswiki");
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Single.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null &&
response.isSuccessful() && response.body() != null) {
String json = response.body().string();
if (json == null) {
return 0;
}
GetWikidataEditCountResponse countResponse = gson
.fromJson(json, GetWikidataEditCountResponse.class);
if (null != countResponse) {
return countResponse.getWikidataEditCount();
}
}
return 0;
});
}
/**
* 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 MediaWiki user name
* @return
*/
public Single<FeedbackResponse> getAchievements(String userName) {
final String fetchAchievementUrlTemplate =
wikiMediaToolforgeUrl + (ConfigUtils.isBetaFlavour() ? "/feedback.py?labs=commonswiki"
: "/feedback.py");
return Single.fromCallable(() -> {
String url = String.format(
Locale.ENGLISH,
fetchAchievementUrlTemplate,
userName);
HttpUrl.Builder urlBuilder = HttpUrl.parse(url).newBuilder();
urlBuilder.addQueryParameter("user", userName);
Request request = new Request.Builder()
.url(urlBuilder.toString())
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
Timber.d("Response for achievements is %s", json);
try {
return gson.fromJson(json, FeedbackResponse.class);
} catch (Exception e) {
return new FeedbackResponse(0, 0, 0, new FeaturedImages(0, 0), 0, "");
}
}
return null;
});
}
public Observable<List<Place>> getNearbyPlaces(LatLng cur, String language, double radius)
throws IOException {
String wikidataQuery = FileUtils.readFromResource("/queries/nearby_query.rq");
String query = wikidataQuery
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", language);
.replace("${RAD}", String.format(Locale.ROOT, "%.2f", radius))
.replace("${LAT}", String.format(Locale.ROOT, "%.4f", cur.getLatitude()))
.replace("${LONG}", String.format(Locale.ROOT, "%.4f", cur.getLongitude()))
.replace("${LANG}", language);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
Request request = new Request.Builder()
.url(urlBuilder.build())
.build();
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new ArrayList<>();
}
NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
List<Place> places = new ArrayList<>();
for (NearbyResultItem item : bindings) {
places.add(Place.from(item));
}
return places;
}
return new ArrayList<>();
});
}
return Observable.fromCallable(() -> {
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return new ArrayList<>();
}
NearbyResponse nearbyResponse = gson.fromJson(json, NearbyResponse.class);
List<NearbyResultItem> bindings = nearbyResponse.getResults().getBindings();
List<Place> places = new ArrayList<>();
for (NearbyResultItem item : bindings) {
places.add(Place.from(item));
}
return places;
}
return new ArrayList<>();
});
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Single<List<DepictedItem>> getChildDepictions(String qid, int startPosition,
int limit) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,"/queries/subclasses_query.rq"));
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Single<List<DepictedItem>> getChildDepictions(String qid, int startPosition,
int limit) throws IOException {
return depictedItemsFrom(
sparqlQuery(qid, startPosition, limit, "/queries/subclasses_query.rq"));
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Single<List<DepictedItem>> getParentDepictions(String qid, int startPosition,
int limit) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,
"/queries/parentclasses_query.rq"));
}
/**
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item. Example:
* bridge -> suspended bridge, aqueduct, etc
*/
public Single<List<DepictedItem>> getParentDepictions(String qid, int startPosition,
int limit) throws IOException {
return depictedItemsFrom(sparqlQuery(qid, startPosition, limit,
"/queries/parentclasses_query.rq"));
}
private Single<List<DepictedItem>> depictedItemsFrom(Request request) {
return depictsClient.toDepictions(Single.fromCallable(() -> {
try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
return gson.fromJson(body.string(), SparqlResponse.class);
}
}).doOnError(Timber::e));
}
private Single<List<DepictedItem>> depictedItemsFrom(Request request) {
return depictsClient.toDepictions(Single.fromCallable(() -> {
try (ResponseBody body = okHttpClient.newCall(request).execute().body()) {
return gson.fromJson(body.string(), SparqlResponse.class);
}
}).doOnError(Timber::e));
}
@NotNull
private Request sparqlQuery(String qid, int startPosition, int limit, String fileName) throws IOException {
String query = FileUtils.readFromResource(fileName)
.replace("${QID}", qid)
.replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"")
.replace("${LIMIT}",""+ limit)
.replace("${OFFSET}",""+ startPosition);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
return new Request.Builder()
.url(urlBuilder.build())
.build();
}
@NotNull
private Request sparqlQuery(String qid, int startPosition, int limit, String fileName)
throws IOException {
String query = FileUtils.readFromResource(fileName)
.replace("${QID}", qid)
.replace("${LANG}", "\"" + Locale.getDefault().getLanguage() + "\"")
.replace("${LIMIT}", "" + limit)
.replace("${OFFSET}", "" + startPosition);
HttpUrl.Builder urlBuilder = HttpUrl
.parse(sparqlQueryUrl)
.newBuilder()
.addQueryParameter("query", query)
.addQueryParameter("format", "json");
return new Request.Builder()
.url(urlBuilder.build())
.build();
}
public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(campaignsUrl)
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, CampaignResponseDTO.class);
}
return null;
});
}
public Single<CampaignResponseDTO> getCampaigns() {
return Single.fromCallable(() -> {
Request request = new Request.Builder().url(campaignsUrl)
.build();
Response response = okHttpClient.newCall(request).execute();
if (response != null && response.body() != null && response.isSuccessful()) {
String json = response.body().string();
if (json == null) {
return null;
}
return gson.fromJson(json, CampaignResponseDTO.class);
}
return null;
});
}
}

View file

@ -89,11 +89,6 @@ public class MoreBottomSheetLoggedOutFragment extends BottomSheetDialogFragment
getActivity().startActivity(intent);
}
@OnClick(R.id.more_tutorial)
public void onTutorialClicked() {
WelcomeActivity.startYourself(getActivity());
}
@OnClick(R.id.more_settings)
public void onSettingsClicked() {
final Intent intent = new Intent(getActivity(), SettingsActivity.class);

View file

@ -16,82 +16,81 @@ import org.wikipedia.model.EnumCodeMap;
import fr.free.nrw.commons.R;
public enum NavTab implements EnumCode {
CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) {
CONTRIBUTIONS(R.string.contributions_fragment, R.drawable.ic_baseline_person_24) {
@NonNull
@Override
public Fragment newInstance() {
return ContributionsFragment.newInstance();
}
},
NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return NearbyParentFragment.newInstance();
}
},
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
}
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTab> MAP = new EnumCodeMap<>(NavTab.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
@Override
public Fragment newInstance() {
return ContributionsFragment.newInstance();
public static NavTab of(int code) {
return MAP.get(code);
}
},
NEARBY(R.string.nearby_fragment, R.drawable.ic_location_on_black_24dp){
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public Fragment newInstance() {
return NearbyParentFragment.newInstance();
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
},
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
NavTab(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTab> MAP = new EnumCodeMap<>(NavTab.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
public static NavTab of(int code) {
return MAP.get(code);
}
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
NavTab(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
}

View file

@ -8,28 +8,31 @@ import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentPagerAdapter;
public class NavTabFragmentPagerAdapter extends FragmentPagerAdapter {
private Fragment currentFragment;
public NavTabFragmentPagerAdapter(FragmentManager mgr) {
super(mgr);
}
private Fragment currentFragment;
@Nullable
public Fragment getCurrentFragment() {
return currentFragment;
}
public NavTabFragmentPagerAdapter(FragmentManager mgr) {
super(mgr);
}
@Override public Fragment getItem(int pos) {
return NavTab.of(pos).newInstance();
}
@Nullable
public Fragment getCurrentFragment() {
return currentFragment;
}
@Override public int getCount() {
return NavTab.size();
}
@Override
public Fragment getItem(int pos) {
return NavTab.of(pos).newInstance();
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
currentFragment = ((Fragment) object);
super.setPrimaryItem(container, position, object);
}
@Override
public int getCount() {
return NavTab.size();
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
currentFragment = ((Fragment) object);
super.setPrimaryItem(container, position, object);
}
}

View file

@ -10,32 +10,32 @@ import fr.free.nrw.commons.contributions.MainActivity;
public class NavTabLayout extends BottomNavigationView {
public NavTabLayout(Context context) {
super(context);
setTabViews();
}
public NavTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setTabViews();
}
public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setTabViews();
}
private void setTabViews() {
if (((MainActivity)getContext()).applicationKvStore.getBoolean("login_skipped") == true) {
for (int i = 0; i < NavTabLoggedOut.size(); i++) {
NavTabLoggedOut navTab = NavTabLoggedOut.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
}
} else {
for (int i = 0; i < NavTab.size(); i++) {
NavTab navTab = NavTab.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
}
public NavTabLayout(Context context) {
super(context);
setTabViews();
}
public NavTabLayout(Context context, AttributeSet attrs) {
super(context, attrs);
setTabViews();
}
public NavTabLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setTabViews();
}
private void setTabViews() {
if (((MainActivity) getContext()).applicationKvStore.getBoolean("login_skipped") == true) {
for (int i = 0; i < NavTabLoggedOut.size(); i++) {
NavTabLoggedOut navTab = NavTabLoggedOut.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
}
} else {
for (int i = 0; i < NavTab.size(); i++) {
NavTab navTab = NavTab.of(i);
getMenu().add(Menu.NONE, i, i, navTab.text()).setIcon(navTab.icon());
}
}
}
}
}

View file

@ -13,66 +13,67 @@ import org.wikipedia.model.EnumCodeMap;
public enum NavTabLoggedOut implements EnumCode {
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
EXPLORE(R.string.navigation_item_explore, R.drawable.ic_globe) {
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
}
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
@NonNull
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
}
};
private static final EnumCodeMap<NavTabLoggedOut> MAP = new EnumCodeMap<>(
NavTabLoggedOut.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
@Override
public Fragment newInstance() {
return ExploreFragment.newInstance();
public static NavTabLoggedOut of(int code) {
return MAP.get(code);
}
},
FAVORITES(R.string.favorites, R.drawable.ic_round_star_border_24px) {
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public Fragment newInstance() {
return BookmarkFragment.newInstance();
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
},
MORE(R.string.more, R.drawable.ic_menu_black_24dp) {
@NonNull
@Override
public Fragment newInstance() {
return null;
NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
};
private static final EnumCodeMap<NavTabLoggedOut> MAP = new EnumCodeMap<>(NavTabLoggedOut.class);
@StringRes
private final int text;
@DrawableRes
private final int icon;
@NonNull
public static NavTabLoggedOut of(int code) {
return MAP.get(code);
}
public static int size() {
return MAP.size();
}
@StringRes
public int text() {
return text;
}
@DrawableRes
public int icon() {
return icon;
}
@NonNull
public abstract Fragment newInstance();
@Override
public int code() {
// This enumeration is not marshalled so tying declaration order to presentation order is
// convenient and consistent.
return ordinal();
}
NavTabLoggedOut(@StringRes int text, @DrawableRes int icon) {
this.text = text;
this.icon = icon;
}
}

View file

@ -222,6 +222,15 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private PlaceAdapter adapter;
private NearbyParentFragmentInstanceReadyCallback nearbyParentFragmentInstanceReadyCallback;
/**
* Holds filtered markers that are to be shown
*/
private List<NearbyBaseMarker> filteredMarkers;
/**
* Holds all the markers
*/
private List<NearbyBaseMarker> allMarkers;
@NonNull
public static NearbyParentFragment newInstance() {
NearbyParentFragment fragment = new NearbyParentFragment();
@ -1241,6 +1250,8 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
// Remove the previous markers before updating them
hideAllMarkers();
filteredMarkers = new ArrayList<>();
for (final MarkerPlaceGroup markerPlaceGroup : NearbyController.markerLabelList) {
final Place place = markerPlaceGroup.getPlace();
@ -1272,6 +1283,9 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
updateMarker(markerPlaceGroup.getIsBookmarked(), place, NearbyController.currentLocation);
}
}
mapBox.clear();
mapBox.addMarkers(filteredMarkers);
}
@Override
@ -1289,25 +1303,35 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
VectorDrawableCompat vectorDrawable = VectorDrawableCompat.create(
getContext().getResources(), getIconFor(place, isBookmarked), getContext().getTheme());
for (Marker marker : mapBox.getMarkers()) {
if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) {
if(curLatLng != null) {
for (NearbyBaseMarker nearbyMarker : allMarkers) {
if (nearbyMarker.getMarker().getTitle() != null && nearbyMarker.getMarker().getTitle().equals(place.getName())) {
final Bitmap icon = UiUtils.getBitmap(vectorDrawable);
final Bitmap icon = UiUtils.getBitmap(vectorDrawable);
if (curLatLng != null) {
final String distance = formatDistanceBetween(curLatLng, place.location);
place.setDistance(distance);
}
final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
nearbyBaseMarker.title(place.name);
nearbyBaseMarker.position(
final NearbyBaseMarker nearbyBaseMarker = new NearbyBaseMarker();
nearbyBaseMarker.title(place.name);
nearbyBaseMarker.position(
new com.mapbox.mapboxsdk.geometry.LatLng(
place.location.getLatitude(),
place.location.getLongitude()));
nearbyBaseMarker.place(place);
nearbyBaseMarker.icon(IconFactory.getInstance(getContext())
place.location.getLatitude(),
place.location.getLongitude()));
nearbyBaseMarker.place(place);
nearbyBaseMarker.icon(IconFactory.getInstance(getContext())
.fromBitmap(icon));
marker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon));
nearbyMarker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon));
filteredMarkers.add(nearbyBaseMarker);
}
}
} else {
for (Marker marker : mapBox.getMarkers()) {
if (marker.getTitle() != null && marker.getTitle().equals(place.getName())) {
final Bitmap icon = UiUtils.getBitmap(vectorDrawable);
marker.setIcon(IconFactory.getInstance(getContext()).fromBitmap(icon));
}
}
}
}
@ -1348,6 +1372,7 @@ public class NearbyParentFragment extends CommonsDaggerSupportFragment
private void addNearbyMarkersToMapBoxMap(final List<NearbyBaseMarker> nearbyBaseMarkers, final Marker selectedMarker) {
if (isMapBoxReady && mapBox != null) {
allMarkers = new ArrayList<>(nearbyBaseMarkers);
mapBox.addMarkers(nearbyBaseMarkers);
setMapMarkerActions(selectedMarker);
presenter.updateMapMarkersToController(nearbyBaseMarkers);

View file

@ -2,7 +2,7 @@ package fr.free.nrw.commons.quiz;
import android.content.Intent;
import android.os.Bundle;
import android.widget.RadioButton;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AlertDialog;
@ -26,14 +26,22 @@ public class QuizActivity extends AppCompatActivity {
@BindView(R.id.question_image) SimpleDraweeView imageView;
@BindView(R.id.question_text) TextView questionText;
@BindView(R.id.question_title) TextView questionTitle;
@BindView(R.id.quiz_positive_answer) RadioButton positiveAnswer;
@BindView(R.id.quiz_negative_answer) RadioButton negativeAnswer;
@BindView(R.id.quiz_positive_answer) Button positiveAnswer;
@BindView(R.id.quiz_negative_answer) Button negativeAnswer;
@BindView(R.id.toolbar) Toolbar toolbar;
private QuizController quizController = new QuizController();
private ArrayList<QuizQuestion> quiz = new ArrayList<>();
private int questionIndex = 0;
private int score;
/**
* isPositiveAnswerChecked : represents yes click event
*/
private boolean isPositiveAnswerChecked;
/**
* isNegativeAnswerChecked : represents no click event
*/
private boolean isNegativeAnswerChecked;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -43,25 +51,23 @@ public class QuizActivity extends AppCompatActivity {
quizController.initialize(this);
ButterKnife.bind(this);
setSupportActionBar(toolbar);
positiveAnswer = findViewById(R.id.quiz_positive_answer);
negativeAnswer = findViewById(R.id.quiz_negative_answer);
displayQuestion();
}
/**
* to move to next question and check whether answer is selected or not
*/
@OnClick(R.id.next_button)
public void setNextQuestion(){
if ( questionIndex <= quiz.size() && (positiveAnswer.isChecked() || negativeAnswer.isChecked())) {
if ( questionIndex <= quiz.size() && (isPositiveAnswerChecked || isNegativeAnswerChecked)) {
evaluateScore();
} else if ( !positiveAnswer.isChecked() && !negativeAnswer.isChecked()){
AlertDialog.Builder alert = new AlertDialog.Builder(this);
alert.setTitle(getResources().getString(R.string.warning));
alert.setMessage(getResources().getString(R.string.warning_for_no_answer));
alert.setPositiveButton(R.string.continue_message, (dialog, which) -> dialog.dismiss());
AlertDialog dialog = alert.create();
dialog.show();
}
}
@OnClick(R.id.next_button)
public void notKnowAnswer(){
customAlert("Information", quiz.get(questionIndex).getAnswerMessage());
}
/**
@ -98,17 +104,24 @@ public class QuizActivity extends AppCompatActivity {
.build());
imageView.setImageURI(quiz.get(questionIndex).getUrl());
new RadioGroupHelper(this, R.id.quiz_positive_answer, R.id.quiz_negative_answer);
positiveAnswer.setChecked(false);
negativeAnswer.setChecked(false);
isPositiveAnswerChecked = false;
isNegativeAnswerChecked = false;
positiveAnswer.setOnClickListener(view -> {
isPositiveAnswerChecked = true;
setNextQuestion();
});
negativeAnswer.setOnClickListener(view -> {
isNegativeAnswerChecked = true;
setNextQuestion();
});
}
/**
* to evaluate score and check whether answer is correct or wrong
*/
public void evaluateScore() {
if ((quiz.get(questionIndex).isAnswer() && positiveAnswer.isChecked()) ||
(!quiz.get(questionIndex).isAnswer() && negativeAnswer.isChecked()) ){
if ((quiz.get(questionIndex).isAnswer() && isPositiveAnswerChecked) ||
(!quiz.get(questionIndex).isAnswer() && isNegativeAnswerChecked) ){
customAlert(getResources().getString(R.string.correct),quiz.get(questionIndex).getAnswerMessage() );
score++;
} else {

View file

@ -19,8 +19,11 @@ import io.reactivex.Flowable;
import io.reactivex.Observable;
import io.reactivex.Single;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -254,6 +257,23 @@ public class UploadRepository {
return depictModel.searchAllEntities(query);
}
/**
* Gets the depiction for each unique {@link Place} associated with an {@link UploadItem}
* from {@link #getUploads()}
*
* @return a single that provides the depictions
*/
public Single<List<DepictedItem>> getPlaceDepictions() {
final Set<Place> places = new HashSet<>();
for (final UploadItem item : getUploads()) {
final Place place = item.getPlace();
if (place != null) {
places.add(place);
}
}
return depictModel.getPlaceDepictions(new ArrayList<>(places));
}
/**
* Returns nearest place matching the passed latitude and longitude
* @param decLatitude

View file

@ -20,65 +20,65 @@ import timber.log.Timber;
@Singleton
public class FileUtilsWrapper {
@Inject
public FileUtilsWrapper() {
@Inject
public FileUtilsWrapper() {
}
public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName);
}
public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is);
}
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath);
}
public String getGeolocationOfFile(String filePath) {
return FileUtils.getGeolocationOfFile(filePath);
}
/**
* Takes a file as input and returns an Observable of files with the specified chunk size
*/
public List<File> getFileChunks(Context context, File file, final int chunkSize)
throws IOException {
final byte[] buffer = new byte[chunkSize];
//try-with-resources to ensure closing stream
try (final FileInputStream fis = new FileInputStream(file);
final BufferedInputStream bis = new BufferedInputStream(fis)) {
final List<File> buffers = new ArrayList<>();
int size;
while ((size = bis.read(buffer)) > 0) {
buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
getFileExt(file.getName())));
}
return buffers;
}
}
/**
* Create a temp file containing the passed byte data.
*/
private File writeToFile(Context context, final byte[] data, final String fileName,
String fileExtension)
throws IOException {
final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir());
try {
if (!file.exists()) {
file.createNewFile();
}
final FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
} catch (final Exception throwable) {
Timber.e(throwable, "Failed to create file");
public String getFileExt(String fileName) {
return FileUtils.getFileExt(fileName);
}
public String getSHA1(InputStream is) {
return FileUtils.getSHA1(is);
}
public FileInputStream getFileInputStream(String filePath) throws FileNotFoundException {
return FileUtils.getFileInputStream(filePath);
}
public String getGeolocationOfFile(String filePath) {
return FileUtils.getGeolocationOfFile(filePath);
}
/**
* Takes a file as input and returns an Observable of files with the specified chunk size
*/
public List<File> getFileChunks(Context context, File file, final int chunkSize)
throws IOException {
final byte[] buffer = new byte[chunkSize];
//try-with-resources to ensure closing stream
try (final FileInputStream fis = new FileInputStream(file);
final BufferedInputStream bis = new BufferedInputStream(fis)) {
final List<File> buffers = new ArrayList<>();
int size;
while ((size = bis.read(buffer)) > 0) {
buffers.add(writeToFile(context, Arrays.copyOf(buffer, size), file.getName(),
getFileExt(file.getName())));
}
return buffers;
}
}
/**
* Create a temp file containing the passed byte data.
*/
private File writeToFile(Context context, final byte[] data, final String fileName,
String fileExtension)
throws IOException {
final File file = File.createTempFile(fileName, fileExtension, context.getCacheDir());
try {
if (!file.exists()) {
file.createNewFile();
}
final FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
} catch (final Exception throwable) {
Timber.e(throwable, "Failed to create file");
}
return file;
}
return file;
}
}

View file

@ -22,6 +22,7 @@ import timber.log.Timber;
*/
@Singleton
public class ImageProcessingService {
private final FileUtilsWrapper fileUtilsWrapper;
private final ImageUtilsWrapper imageUtilsWrapper;
private final ReadFBMD readFBMD;
@ -30,9 +31,9 @@ public class ImageProcessingService {
@Inject
public ImageProcessingService(FileUtilsWrapper fileUtilsWrapper,
ImageUtilsWrapper imageUtilsWrapper,
ReadFBMD readFBMD, EXIFReader EXIFReader,
MediaClient mediaClient, Context context) {
ImageUtilsWrapper imageUtilsWrapper,
ReadFBMD readFBMD, EXIFReader EXIFReader,
MediaClient mediaClient, Context context) {
this.fileUtilsWrapper = fileUtilsWrapper;
this.imageUtilsWrapper = imageUtilsWrapper;
this.readFBMD = readFBMD;
@ -41,33 +42,34 @@ public class ImageProcessingService {
}
/**
* Check image quality before upload - checks duplicate image - checks dark image - checks
* geolocation for image - check for valid title
*/
Single<Integer> validateImage(UploadItem uploadItem) {
int currentImageQuality = uploadItem.getImageQuality();
Timber.d("Current image quality is %d", currentImageQuality);
if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
return Single.just(ImageUtils.IMAGE_OK);
}
Timber.d("Checking the validity of image");
String filePath = uploadItem.getMediaUri().getPath();
return Single.zip(
checkDuplicateImage(filePath),
checkImageGeoLocation(uploadItem.getPlace(), filePath),
checkDarkImage(filePath),
validateItemTitle(uploadItem),
checkFBMD(filePath),
checkEXIF(filePath),
(duplicateImage, wrongGeoLocation, darkImage, itemTitle, fbmd, exif) -> {
Timber.d("duplicate: %d, geo: %d, dark: %d, title: %d" + "fbmd:" + fbmd + "exif:" + exif,
duplicateImage, wrongGeoLocation, darkImage, itemTitle);
return duplicateImage | wrongGeoLocation | darkImage | itemTitle | fbmd | exif;
/**
* Check image quality before upload - checks duplicate image - checks dark image - checks
* geolocation for image - check for valid title
*/
Single<Integer> validateImage(UploadItem uploadItem) {
int currentImageQuality = uploadItem.getImageQuality();
Timber.d("Current image quality is %d", currentImageQuality);
if (currentImageQuality == ImageUtils.IMAGE_KEEP) {
return Single.just(ImageUtils.IMAGE_OK);
}
);
}
Timber.d("Checking the validity of image");
String filePath = uploadItem.getMediaUri().getPath();
return Single.zip(
checkDuplicateImage(filePath),
checkImageGeoLocation(uploadItem.getPlace(), filePath),
checkDarkImage(filePath),
validateItemTitle(uploadItem),
checkFBMD(filePath),
checkEXIF(filePath),
(duplicateImage, wrongGeoLocation, darkImage, itemTitle, fbmd, exif) -> {
Timber.d("duplicate: %d, geo: %d, dark: %d, title: %d" + "fbmd:" + fbmd + "exif:"
+ exif,
duplicateImage, wrongGeoLocation, darkImage, itemTitle);
return duplicateImage | wrongGeoLocation | darkImage | itemTitle | fbmd | exif;
}
);
}
/**
* We want to discourage users from uploading images to Commons that were taken from Facebook.
@ -79,10 +81,10 @@ public class ImageProcessingService {
}
/**
* We try to minimize uploads from the Commons app that might be copyright violations.
* If an image does not have any Exif metadata, then it was likely downloaded from the internet,
* and is probably not an original work by the user. We detect these kinds of images by looking
* for the presence of some basic Exif metadata.
* We try to minimize uploads from the Commons app that might be copyright violations. If an
* image does not have any Exif metadata, then it was likely downloaded from the internet, and
* is probably not an original work by the user. We detect these kinds of images by looking for
* the presence of some basic Exif metadata.
*/
private Single<Integer> checkEXIF(String filepath) {
return EXIFReader.processMetadata(filepath);
@ -90,9 +92,7 @@ public class ImageProcessingService {
/**
* Checks item caption
* - empty caption
* - existing caption
* Checks item caption - empty caption - existing caption
*
* @param uploadItem
* @return
@ -105,11 +105,11 @@ public class ImageProcessingService {
}
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())
.map(doesFileExist -> {
Timber.d("Result for valid title is %s", doesFileExist);
return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
})
.subscribeOn(Schedulers.io());
.map(doesFileExist -> {
Timber.d("Result for valid title is %s", doesFileExist);
return doesFileExist ? FILE_NAME_EXISTS : IMAGE_OK;
})
.subscribeOn(Schedulers.io());
}
/**
@ -121,13 +121,13 @@ public class ImageProcessingService {
private Single<Integer> checkDuplicateImage(String filePath) {
Timber.d("Checking for duplicate image %s", filePath);
return Single.fromCallable(() -> fileUtilsWrapper.getFileInputStream(filePath))
.map(fileUtilsWrapper::getSHA1)
.flatMap(mediaClient::checkFileExistsUsingSha)
.map(b -> {
Timber.d("Result for duplicate image %s", b);
return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;
})
.subscribeOn(Schedulers.io());
.map(fileUtilsWrapper::getSHA1)
.flatMap(mediaClient::checkFileExistsUsingSha)
.map(b -> {
Timber.d("Result for duplicate image %s", b);
return b ? ImageUtils.IMAGE_DUPLICATE : ImageUtils.IMAGE_OK;
})
.subscribeOn(Schedulers.io());
}
/**
@ -142,8 +142,8 @@ public class ImageProcessingService {
}
/**
* Checks for image geolocation
* returns IMAGE_OK if the place is null or if the file doesn't contain a geolocation
* Checks for image geolocation returns IMAGE_OK if the place is null or if the file doesn't
* contain a geolocation
*
* @param filePath file to be checked
* @return IMAGE_GEOLOCATION_DIFFERENT or IMAGE_OK
@ -154,14 +154,15 @@ public class ImageProcessingService {
return Single.just(ImageUtils.IMAGE_OK);
}
return Single.fromCallable(() -> filePath)
.map(fileUtilsWrapper::getGeolocationOfFile)
.flatMap(geoLocation -> {
if (StringUtils.isBlank(geoLocation)) {
return Single.just(ImageUtils.IMAGE_OK);
}
return imageUtilsWrapper.checkImageGeolocationIsDifferent(geoLocation, place.getLocation());
})
.subscribeOn(Schedulers.io());
.map(fileUtilsWrapper::getGeolocationOfFile)
.flatMap(geoLocation -> {
if (StringUtils.isBlank(geoLocation)) {
return Single.just(ImageUtils.IMAGE_OK);
}
return imageUtilsWrapper
.checkImageGeolocationIsDifferent(geoLocation, place.getLocation());
})
.subscribeOn(Schedulers.io());
}
}

View file

@ -16,96 +16,96 @@ import org.apache.commons.lang3.StringUtils;
class PageContentsCreator {
//{{According to Exif data|2009-01-09}}
private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
//{{According to Exif data|2009-01-09}}
private static final String TEMPLATE_DATE_ACC_TO_EXIF = "{{According to Exif data|%s}}";
//2009-01-09 9 January 2009
private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
//2009-01-09 9 January 2009
private static final String TEMPLATE_DATA_OTHER_SOURCE = "%s";
private final Context context;
private final Context context;
@Inject
public PageContentsCreator(Context context) {
this.context = context;
}
public String createFrom(Contribution contribution) {
StringBuilder buffer = new StringBuilder();
final Media media = contribution.getMedia();
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(media.getFallbackDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(media.getAuthor()).append("|")
.append(media.getAuthor()).append("]]\n");
String templatizedCreatedDate = getTemplatizedCreatedDate(
contribution.getDateCreated(), contribution.getDateCreatedSource());
if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("|date=").append(templatizedCreatedDate);
@Inject
public PageContentsCreator(Context context) {
this.context = context;
}
buffer.append("}}").append("\n");
public String createFrom(Contribution contribution) {
StringBuilder buffer = new StringBuilder();
final Media media = contribution.getMedia();
buffer
.append("== {{int:filedesc}} ==\n")
.append("{{Information\n")
.append("|description=").append(media.getFallbackDescription()).append("\n")
.append("|source=").append("{{own}}\n")
.append("|author=[[User:").append(media.getAuthor()).append("|")
.append(media.getAuthor()).append("]]\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
final String decimalCoords = contribution.getDecimalCoords();
if (decimalCoords != null) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
String templatizedCreatedDate = getTemplatizedCreatedDate(
contribution.getDateCreated(), contribution.getDateCreatedSource());
if (!StringUtils.isBlank(templatizedCreatedDate)) {
buffer.append("|date=").append(templatizedCreatedDate);
}
buffer.append("}}").append("\n");
//Only add Location template (e.g. {{Location|37.51136|-77.602615}} ) if coords is not null
final String decimalCoords = contribution.getDecimalCoords();
if (decimalCoords != null) {
buffer.append("{{Location|").append(decimalCoords).append("}}").append("\n");
}
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(media.getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=")
.append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n");
final List<String> categories = media.getCategories();
if (categories != null && categories.size() != 0) {
for (int i = 0; i < categories.size(); i++) {
buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
}
} else {
buffer.append("{{subst:unc}}");
}
return buffer.toString();
}
buffer.append("== {{int:license-header}} ==\n")
.append(licenseTemplateFor(media.getLicense())).append("\n\n")
.append("{{Uploaded from Mobile|platform=Android|version=")
.append(ConfigUtils.getVersionNameWithSha(context)).append("}}\n");
final List<String> categories = media.getCategories();
if (categories != null && categories.size() != 0) {
for (int i = 0; i < categories.size(); i++) {
buffer.append("\n[[Category:").append(categories.get(i)).append("]]");
}
} else {
buffer.append("{{subst:unc}}");
}
return buffer.toString();
}
/**
* Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
*
* @param dateCreated
* @param dateCreatedSource
* @return
*/
private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
if (dateCreated != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
return String.format(Locale.ENGLISH,
isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
dateFormat.format(dateCreated)
) + "\n";
}
return "";
}
private boolean isExif(String dateCreatedSource) {
return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Licenses.CC0:
return "{{self|cc-zero}}";
/**
* Returns upload date in either TEMPLATE_DATE_ACC_TO_EXIF or TEMPLATE_DATA_OTHER_SOURCE
*
* @param dateCreated
* @param dateCreatedSource
* @return
*/
private String getTemplatizedCreatedDate(Date dateCreated, String dateCreatedSource) {
if (dateCreated != null) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
return String.format(Locale.ENGLISH,
isExif(dateCreatedSource) ? TEMPLATE_DATE_ACC_TO_EXIF : TEMPLATE_DATA_OTHER_SOURCE,
dateFormat.format(dateCreated)
) + "\n";
}
return "";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
private boolean isExif(String dateCreatedSource) {
return DateTimeWithSource.EXIF_SOURCE.equals(dateCreatedSource);
}
@NonNull
private String licenseTemplateFor(String license) {
switch (license) {
case Licenses.CC_BY_3:
return "{{self|cc-by-3.0}}";
case Licenses.CC_BY_4:
return "{{self|cc-by-4.0}}";
case Licenses.CC_BY_SA_3:
return "{{self|cc-by-sa-3.0}}";
case Licenses.CC_BY_SA_4:
return "{{self|cc-by-sa-4.0}}";
case Licenses.CC0:
return "{{self|cc-zero}}";
}
throw new RuntimeException("Unrecognized license value: " + license);
}
}

View file

@ -15,34 +15,34 @@ import javax.inject.Singleton;
@Singleton
public class ReadFBMD {
@Inject
public ReadFBMD() {
}
@Inject
public ReadFBMD() {
}
public Single<Integer> processMetadata(String path) {
return Single.fromCallable(() -> {
try {
int psBlockOffset;
int fbmdOffset;
public Single<Integer> processMetadata(String path) {
return Single.fromCallable(() -> {
try {
int psBlockOffset;
int fbmdOffset;
try (FileInputStream fs = new FileInputStream(path)) {
byte[] bytes = new byte[4096];
fs.read(bytes);
fs.close();
String fileStr = new String(bytes);
psBlockOffset = fileStr.indexOf("8BIM");
fbmdOffset = fileStr.indexOf("FBMD");
}
try (FileInputStream fs = new FileInputStream(path)) {
byte[] bytes = new byte[4096];
fs.read(bytes);
fs.close();
String fileStr = new String(bytes);
psBlockOffset = fileStr.indexOf("8BIM");
fbmdOffset = fileStr.indexOf("FBMD");
}
if (psBlockOffset > 0 && fbmdOffset > 0
&& fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) {
return ImageUtils.FILE_FBMD;
}
} catch (IOException e) {
e.printStackTrace();
}
return ImageUtils.IMAGE_OK;
});
}
if (psBlockOffset > 0 && fbmdOffset > 0
&& fbmdOffset > psBlockOffset && fbmdOffset - psBlockOffset < 0x80) {
return ImageUtils.FILE_FBMD;
}
} catch (IOException e) {
e.printStackTrace();
}
return ImageUtils.IMAGE_OK;
});
}
}

View file

@ -35,199 +35,204 @@ import timber.log.Timber;
@Singleton
public class UploadClient {
private final int CHUNK_SIZE = 512 * 1024; // 512 KB
private final int CHUNK_SIZE = 512 * 1024; // 512 KB
//This is maximum duration for which a stash is persisted on MediaWiki
// https://www.mediawiki.org/wiki/Manual:$wgUploadStashMaxAge
private final int MAX_CHUNK_AGE = 6 * 3600 * 1000; // 6 hours
//This is maximum duration for which a stash is persisted on MediaWiki
// https://www.mediawiki.org/wiki/Manual:$wgUploadStashMaxAge
private final int MAX_CHUNK_AGE = 6 * 3600 * 1000; // 6 hours
private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator;
private final FileUtilsWrapper fileUtilsWrapper;
private final Gson gson;
private final UploadInterface uploadInterface;
private final CsrfTokenClient csrfTokenClient;
private final PageContentsCreator pageContentsCreator;
private final FileUtilsWrapper fileUtilsWrapper;
private final Gson gson;
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
@Inject
public UploadClient(final UploadInterface uploadInterface,
@Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
final PageContentsCreator pageContentsCreator,
final FileUtilsWrapper fileUtilsWrapper, final Gson gson) {
this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient;
this.pageContentsCreator = pageContentsCreator;
this.fileUtilsWrapper = fileUtilsWrapper;
this.gson = gson;
}
/**
* Upload file to stash in chunks of specified size. Uploading files in chunks will make handling
* of large files easier. Also, it will be useful in supporting pause/resume of uploads
*/
public Observable<StashUploadResult> uploadFileToStash(
final Context context, final String filename, final Contribution contribution,
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
if (contribution.getChunkInfo() != null
&& contribution.getChunkInfo().getTotalChunks() == contribution.getChunkInfo()
.getIndexOfNextChunkToUpload()) {
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
contribution.getChunkInfo().getUploadResult().getFilekey()));
@Inject
public UploadClient(final UploadInterface uploadInterface,
@Named(NAMED_COMMONS_CSRF) final CsrfTokenClient csrfTokenClient,
final PageContentsCreator pageContentsCreator,
final FileUtilsWrapper fileUtilsWrapper, final Gson gson) {
this.uploadInterface = uploadInterface;
this.csrfTokenClient = csrfTokenClient;
this.pageContentsCreator = pageContentsCreator;
this.fileUtilsWrapper = fileUtilsWrapper;
this.gson = gson;
}
CommonsApplication.pauseUploads.put(contribution.getPageId(), false);
/**
* Upload file to stash in chunks of specified size. Uploading files in chunks will make
* handling of large files easier. Also, it will be useful in supporting pause/resume of
* uploads
*/
public Observable<StashUploadResult> uploadFileToStash(
final Context context, final String filename, final Contribution contribution,
final NotificationUpdateProgressListener notificationUpdater) throws IOException {
if (contribution.getChunkInfo() != null
&& contribution.getChunkInfo().getTotalChunks() == contribution.getChunkInfo()
.getIndexOfNextChunkToUpload()) {
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
contribution.getChunkInfo().getUploadResult().getFilekey()));
}
final File file = new File(contribution.getLocalUri().getPath());
final List<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
CommonsApplication.pauseUploads.put(contribution.getPageId(), false);
final int totalChunks = fileChunks.size();
final File file = new File(contribution.getLocalUri().getPath());
final List<File> fileChunks = fileUtilsWrapper.getFileChunks(context, file, CHUNK_SIZE);
final MediaType mediaType = MediaType
.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
final int totalChunks = fileChunks.size();
final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>();
if (isStashValid(contribution)) {
chunkInfo.set(contribution.getChunkInfo());
final MediaType mediaType = MediaType
.parse(FileUtils.getMimeType(context, Uri.parse(file.getPath())));
Timber.d("Chunk: Next Chunk: %s, Total Chunks: %s",
contribution.getChunkInfo().getIndexOfNextChunkToUpload(),
contribution.getChunkInfo().getTotalChunks());
}
final AtomicReference<ChunkInfo> chunkInfo = new AtomicReference<>();
if (isStashValid(contribution)) {
chunkInfo.set(contribution.getChunkInfo());
final AtomicInteger index = new AtomicInteger();
final AtomicBoolean failures = new AtomicBoolean();
Timber.d("Chunk: Next Chunk: %s, Total Chunks: %s",
contribution.getChunkInfo().getIndexOfNextChunkToUpload(),
contribution.getChunkInfo().getTotalChunks());
}
compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> {
if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) {
return;
}
final AtomicInteger index = new AtomicInteger();
final AtomicBoolean failures = new AtomicBoolean();
if (chunkInfo.get() != null && index.get() < chunkInfo.get().getIndexOfNextChunkToUpload()) {
index.incrementAndGet();
Timber.d("Chunk: Increment and return: %s", index.get());
return;
}
index.getAndIncrement();
final int offset =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getOffset() : 0;
Timber.d("Chunk: Sending Chunk number: %s, offset: %s", index.get(), offset);
final String filekey =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getFilekey() : null;
final RequestBody requestBody = RequestBody
.create(mediaType, chunkFile);
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
notificationUpdater::onProgress, offset,
file.length());
compositeDisposable.add(uploadChunkToStash(filename,
file.length(),
offset,
filekey,
countingRequestBody).subscribe(uploadResult -> {
Timber.d("Chunk: Received Chunk number: %s, offset: %s", index.get(),
uploadResult.getOffset());
chunkInfo.set(
new ChunkInfo(uploadResult, index.get(), totalChunks));
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
}, throwable -> {
Timber.e(throwable, "Received error in chunk upload");
failures.set(true);
}));
}));
if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
Timber.d("Upload stash paused %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
} else if (failures.get()) {
Timber.d("Upload stash contains failures %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
} else if (chunkInfo.get() != null) {
Timber.d("Upload stash success %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
chunkInfo.get().getUploadResult().getFilekey()));
} else {
Timber.d("Upload stash failed %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
}
}
/**
* Stash is valid for 6 hours. This function checks the validity of stash
* @param contribution
* @return
*/
private boolean isStashValid(Contribution contribution) {
return contribution.getChunkInfo() != null &&
contribution.getDateModified()
.after(new Date(System.currentTimeMillis() - MAX_CHUNK_AGE));
}
/**
* Uploads a file chunk to stash
*
* @param filename The name of the file being uploaded
* @param fileSize The total size of the file
* @param offset The offset returned by the previous chunk upload
* @param fileKey The filekey returned by the previous chunk upload
* @param countingRequestBody Request body with chunk file
* @return
*/
Observable<UploadResult> uploadChunkToStash(final String filename,
final long fileSize,
final long offset,
final String fileKey,
final CountingRequestBody countingRequestBody) {
final MultipartBody.Part filePart;
try {
filePart = MultipartBody.Part
.createFormData("chunk", URLEncoder.encode(filename, "utf-8"), countingRequestBody);
return uploadInterface.uploadFileToStash(toRequestBody(filename),
toRequestBody(String.valueOf(fileSize)),
toRequestBody(String.valueOf(offset)),
toRequestBody(fileKey),
toRequestBody(csrfTokenClient.getTokenBlocking()),
filePart)
.map(UploadResponse::getUpload);
} catch (final Throwable throwable) {
Timber.e(throwable, "Failed to upload chunk to stash");
return Observable.error(throwable);
}
}
/**
* Converts string value to request body
*/
@Nullable
private RequestBody toRequestBody(@Nullable final String value) {
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
}
public Observable<UploadResult> uploadFileFromStash(
final Contribution contribution,
final String uniqueFileName,
final String fileKey) {
try {
return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
pageContentsCreator.createFrom(contribution),
CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName,
fileKey).map(uploadResponse -> {
UploadResponse uploadResult = gson.fromJson(uploadResponse, UploadResponse.class);
if (uploadResult.getUpload() == null) {
final MwException exception = gson.fromJson(uploadResponse, MwException.class);
Timber.e(exception, "Error in uploading file from stash");
throw new RuntimeException(exception.getErrorCode());
compositeDisposable.add(Observable.fromIterable(fileChunks).forEach(chunkFile -> {
if (CommonsApplication.pauseUploads.get(contribution.getPageId()) || failures.get()) {
return;
}
return uploadResult.getUpload();
});
} catch (final Throwable throwable) {
Timber.e(throwable, "Exception occurred in uploading file from stash");
return Observable.error(throwable);
if (chunkInfo.get() != null && index.get() < chunkInfo.get()
.getIndexOfNextChunkToUpload()) {
index.incrementAndGet();
Timber.d("Chunk: Increment and return: %s", index.get());
return;
}
index.getAndIncrement();
final int offset =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getOffset() : 0;
Timber.d("Chunk: Sending Chunk number: %s, offset: %s", index.get(), offset);
final String filekey =
chunkInfo.get() != null ? chunkInfo.get().getUploadResult().getFilekey() : null;
final RequestBody requestBody = RequestBody
.create(mediaType, chunkFile);
final CountingRequestBody countingRequestBody = new CountingRequestBody(requestBody,
notificationUpdater::onProgress, offset,
file.length());
compositeDisposable.add(uploadChunkToStash(filename,
file.length(),
offset,
filekey,
countingRequestBody).subscribe(uploadResult -> {
Timber.d("Chunk: Received Chunk number: %s, offset: %s", index.get(),
uploadResult.getOffset());
chunkInfo.set(
new ChunkInfo(uploadResult, index.get(), totalChunks));
notificationUpdater.onChunkUploaded(contribution, chunkInfo.get());
}, throwable -> {
Timber.e(throwable, "Received error in chunk upload");
failures.set(true);
}));
}));
if (CommonsApplication.pauseUploads.get(contribution.getPageId())) {
Timber.d("Upload stash paused %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.PAUSED, null));
} else if (failures.get()) {
Timber.d("Upload stash contains failures %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
} else if (chunkInfo.get() != null) {
Timber.d("Upload stash success %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.SUCCESS,
chunkInfo.get().getUploadResult().getFilekey()));
} else {
Timber.d("Upload stash failed %s", contribution.getPageId());
return Observable.just(new StashUploadResult(StashUploadState.FAILED, null));
}
}
/**
* Stash is valid for 6 hours. This function checks the validity of stash
*
* @param contribution
* @return
*/
private boolean isStashValid(Contribution contribution) {
return contribution.getChunkInfo() != null &&
contribution.getDateModified()
.after(new Date(System.currentTimeMillis() - MAX_CHUNK_AGE));
}
/**
* Uploads a file chunk to stash
*
* @param filename The name of the file being uploaded
* @param fileSize The total size of the file
* @param offset The offset returned by the previous chunk upload
* @param fileKey The filekey returned by the previous chunk upload
* @param countingRequestBody Request body with chunk file
* @return
*/
Observable<UploadResult> uploadChunkToStash(final String filename,
final long fileSize,
final long offset,
final String fileKey,
final CountingRequestBody countingRequestBody) {
final MultipartBody.Part filePart;
try {
filePart = MultipartBody.Part
.createFormData("chunk", URLEncoder.encode(filename, "utf-8"), countingRequestBody);
return uploadInterface.uploadFileToStash(toRequestBody(filename),
toRequestBody(String.valueOf(fileSize)),
toRequestBody(String.valueOf(offset)),
toRequestBody(fileKey),
toRequestBody(csrfTokenClient.getTokenBlocking()),
filePart)
.map(UploadResponse::getUpload);
} catch (final Throwable throwable) {
Timber.e(throwable, "Failed to upload chunk to stash");
return Observable.error(throwable);
}
}
/**
* Converts string value to request body
*/
@Nullable
private RequestBody toRequestBody(@Nullable final String value) {
return value == null ? null : RequestBody.create(okhttp3.MultipartBody.FORM, value);
}
public Observable<UploadResult> uploadFileFromStash(
final Contribution contribution,
final String uniqueFileName,
final String fileKey) {
try {
return uploadInterface
.uploadFileFromStash(csrfTokenClient.getTokenBlocking(),
pageContentsCreator.createFrom(contribution),
CommonsApplication.DEFAULT_EDIT_SUMMARY,
uniqueFileName,
fileKey).map(uploadResponse -> {
UploadResponse uploadResult = gson
.fromJson(uploadResponse, UploadResponse.class);
if (uploadResult.getUpload() == null) {
final MwException exception = gson
.fromJson(uploadResponse, MwException.class);
Timber.e(exception, "Error in uploading file from stash");
throw new RuntimeException(exception.getErrorCode());
}
return uploadResult.getUpload();
});
} catch (final Throwable throwable) {
Timber.e(throwable, "Exception occurred in uploading file from stash");
return Observable.error(throwable);
}
}
}
}

View file

@ -13,17 +13,17 @@ import java.util.Collections;
import java.util.List;
public class UploadItem {
private final Uri mediaUri;
private final String mimeType;
private ImageCoordinates gpsCoords;
private List<UploadMediaDetail> uploadMediaDetails;
private Place place;
private final long createdTimestamp;
private final String createdTimestampSource;
private final BehaviorSubject<Integer> imageQuality;
private boolean hasInvalidLocation;
private final Uri contentUri;
private final Uri mediaUri;
private final String mimeType;
private ImageCoordinates gpsCoords;
private List<UploadMediaDetail> uploadMediaDetails;
private Place place;
private final long createdTimestamp;
private final String createdTimestampSource;
private final BehaviorSubject<Integer> imageQuality;
private boolean hasInvalidLocation;
private final Uri contentUri;
@SuppressLint("CheckResult")
@ -45,29 +45,29 @@ public class UploadItem {
imageQuality = BehaviorSubject.createDefault(ImageUtils.IMAGE_WAIT);
}
public String getCreatedTimestampSource() {
return createdTimestampSource;
}
public String getCreatedTimestampSource() {
return createdTimestampSource;
}
public ImageCoordinates getGpsCoords() {
return gpsCoords;
}
public ImageCoordinates getGpsCoords() {
return gpsCoords;
}
public List<UploadMediaDetail> getUploadMediaDetails() {
return uploadMediaDetails;
}
public List<UploadMediaDetail> getUploadMediaDetails() {
return uploadMediaDetails;
}
public long getCreatedTimestamp() {
return createdTimestamp;
}
public long getCreatedTimestamp() {
return createdTimestamp;
}
public Uri getMediaUri() {
return mediaUri;
}
public Uri getMediaUri() {
return mediaUri;
}
public int getImageQuality() {
return imageQuality.getValue();
}
public int getImageQuality() {
return imageQuality.getValue();
}
public Uri getContentUri() { return contentUri; }
@ -75,54 +75,55 @@ public class UploadItem {
this.imageQuality.onNext(imageQuality);
}
/**
* Sets the corresponding place to the uploadItem
* @param place geolocated Wikidata item
*/
public void setPlace(Place place) {
this.place = place;
}
public Place getPlace() {
return place;
}
public void setMediaDetails(final List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
}
@Override
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof UploadItem)) {
return false;
/**
* Sets the corresponding place to the uploadItem
*
* @param place geolocated Wikidata item
*/
public void setPlace(Place place) {
this.place = place;
}
return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
}
public Place getPlace() {
return place;
}
@Override
public int hashCode() {
return mediaUri.hashCode();
}
public void setMediaDetails(final List<UploadMediaDetail> uploadMediaDetails) {
this.uploadMediaDetails = uploadMediaDetails;
}
/**
* Choose a filename for the media. Currently, the caption is used as a filename. If several
* languages have been entered, the first language is used.
*/
public String getFileName() {
return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
}
@Override
public boolean equals(@Nullable final Object obj) {
if (!(obj instanceof UploadItem)) {
return false;
}
return mediaUri.toString().contains(((UploadItem) (obj)).mediaUri.toString());
public void setGpsCoords(final ImageCoordinates gpsCoords) {
this.gpsCoords = gpsCoords;
}
}
public void setHasInvalidLocation(boolean hasInvalidLocation) {
this.hasInvalidLocation=hasInvalidLocation;
}
@Override
public int hashCode() {
return mediaUri.hashCode();
}
public boolean hasInvalidLocation() {
return hasInvalidLocation;
}
/**
* Choose a filename for the media. Currently, the caption is used as a filename. If several
* languages have been entered, the first language is used.
*/
public String getFileName() {
return Utils.fixExtension(uploadMediaDetails.get(0).getCaptionText(),
MimeTypeMapWrapper.getExtensionFromMimeType(mimeType));
}
public void setGpsCoords(final ImageCoordinates gpsCoords) {
this.gpsCoords = gpsCoords;
}
public void setHasInvalidLocation(boolean hasInvalidLocation) {
this.hasInvalidLocation = hasInvalidLocation;
}
public boolean hasInvalidLocation() {
return hasInvalidLocation;
}
}

View file

@ -60,6 +60,11 @@ public interface DepictsContract {
*/
void searchForDepictions(String query);
/**
* Selects all associated places (if any) as depictions
*/
void selectPlaceDepictions();
/**
* Check if depictions were selected
* from the depiction list

View file

@ -100,6 +100,14 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
depictsRecyclerView.setAdapter(adapter);
}
@Override
protected void onBecameVisible() {
super.onBecameVisible();
// Select Place depiction as the fragment becomes visible to ensure that the most up to date
// Place is used (i.e. if the user accepts a nearby place dialog)
presenter.selectPlaceDepictions();
}
@Override
public void goToNextScreen() {
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
@ -146,6 +154,7 @@ public class DepictsFragment extends UploadBaseFragment implements DepictsContra
@Override
public void setDepictsList(List<DepictedItem> depictedItemList) {
adapter.setItems(depictedItemList);
depictsRecyclerView.smoothScrollToPosition(0);
}
@OnClick(R.id.depicts_next)

View file

@ -84,6 +84,35 @@ class DepictsPresenter @Inject constructor(
compositeDisposable.clear()
}
/**
* Selects the place depictions retrieved by the repository
*/
override fun selectPlaceDepictions() {
compositeDisposable.add(repository.placeDepictions
.subscribeOn(ioScheduler)
.observeOn(mainThreadScheduler)
.subscribe(::selectNewDepictions)
)
}
/**
* Selects each [DepictedItem] in a given list as if they were clicked by the user by calling
* [onDepictItemClicked] for each depiction and adding the depictions to [depictedItems]
*/
private fun selectNewDepictions(toSelect: List<DepictedItem>) {
toSelect.forEach {
it.isSelected = true
repository.onDepictItemClicked(it)
}
// Add the new selections to the list of depicted items so that the selections appear
// immediately (i.e. without any search term queries)
depictedItems.value?.toMutableList()
?.let { toSelect + it }
?.distinctBy(DepictedItem::id)
?.let { depictedItems.value = it }
}
override fun onPreviousButtonClicked() {
view.goToPreviousScreen()
}

View file

@ -6,5 +6,6 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
class UploadDepictsAdapter(onDepictsClicked: (DepictedItem) -> Unit) :
BaseDelegateAdapter<DepictedItem>(
uploadDepictsDelegate(onDepictsClicked),
areItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id }
areItemsTheSame = { oldItem, newItem -> oldItem.id == newItem.id },
areContentsTheSame = { itemA, itemB -> itemA.isSelected == itemB.isSelected}
)

View file

@ -1,8 +1,10 @@
package fr.free.nrw.commons.upload.mediaDetails;
import static android.app.Activity.RESULT_OK;
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@ -23,19 +25,19 @@ import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;
import com.github.chrisbanes.photoview.PhotoView;
import com.mapbox.mapboxsdk.camera.CameraPosition;
import fr.free.nrw.commons.LocationPicker.LocationPicker;
import fr.free.nrw.commons.R;
import fr.free.nrw.commons.Utils;
import fr.free.nrw.commons.filepicker.UploadableFile;
import fr.free.nrw.commons.kvstore.JsonKvStore;
import fr.free.nrw.commons.location.LatLng;
import fr.free.nrw.commons.nearby.Place;
import fr.free.nrw.commons.settings.Prefs;
import fr.free.nrw.commons.upload.ImageCoordinates;
import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
import fr.free.nrw.commons.upload.UploadBaseFragment;
import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.upload.UploadMediaDetail;
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
import fr.free.nrw.commons.upload.UploadItem;
import fr.free.nrw.commons.utils.DialogUtil;
import fr.free.nrw.commons.utils.ImageUtils;
import fr.free.nrw.commons.utils.ViewUtil;
@ -49,6 +51,7 @@ import timber.log.Timber;
public class UploadMediaDetailFragment extends UploadBaseFragment implements
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
private static final int REQUEST_CODE = 1211;
@BindView(R.id.tv_title)
TextView tvTitle;
@BindView(R.id.ib_map)
@ -94,7 +97,10 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
*/
private Place nearbyPlace;
private UploadItem uploadItem;
/**
* editableUploadItem : Storing the upload item before going to update the coordinates
*/
private UploadItem editableUploadItem;
private UploadMediaDetailFragmentCallback callback;
@ -378,10 +384,67 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
}
@Override
public void showExternalMap(UploadItem uploadItem) {
Utils.handleGeoCoordinates(getContext(),
new LatLng(uploadItem.getGpsCoords().getDecLatitude(),
uploadItem.getGpsCoords().getDecLongitude(), 0.0f));
public void showExternalMap(final UploadItem uploadItem) {
goToLocationPickerActivity(uploadItem);
}
/**
* Start Location picker activity. Show the location first then user can modify it by clicking
* modify location button.
* @param uploadItem current upload item
*/
private void goToLocationPickerActivity(final UploadItem uploadItem) {
editableUploadItem = uploadItem;
startActivityForResult(new LocationPicker.IntentBuilder()
.defaultLocation(new CameraPosition.Builder()
.target(new com.mapbox.mapboxsdk.geometry.LatLng(uploadItem.getGpsCoords()
.getDecLatitude(),
uploadItem.getGpsCoords().getDecLongitude()))
.zoom(16).build())
.activityKey("UploadActivity")
.build(getActivity()), REQUEST_CODE);
}
/**
* Get the coordinates and update the existing coordinates.
* @param requestCode code of request
* @param resultCode code of result
* @param data intent
*/
@Override
public void onActivityResult(final int requestCode, final int resultCode,
@Nullable final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE && resultCode == RESULT_OK) {
assert data != null;
final CameraPosition cameraPosition = LocationPicker.getCameraPosition(data);
if (cameraPosition != null) {
final String latitude = String.valueOf(cameraPosition.target.getLatitude());
final String longitude = String.valueOf(cameraPosition.target.getLongitude());
editLocation(latitude, longitude);
}
}
}
/**
* Update the old coordinates with new one
* @param latitude new latitude
* @param longitude new longitude
*/
public void editLocation(final String latitude, final String longitude){
editableUploadItem.getGpsCoords().setDecLatitude(Double.parseDouble(latitude));
editableUploadItem.getGpsCoords().setDecLongitude(Double.parseDouble(longitude));
editableUploadItem.getGpsCoords().setDecimalCoords(latitude+"|"+longitude);
editableUploadItem.getGpsCoords().setImageCoordsExists(true);
Toast.makeText(getContext(), "Location Updated", Toast.LENGTH_LONG).show();
}
@Override

View file

@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload.structure.depictions
import fr.free.nrw.commons.explore.depictions.DepictsClient
import fr.free.nrw.commons.nearby.Place
import io.reactivex.Flowable
import io.reactivex.Observable
import io.reactivex.Single
import io.reactivex.processors.BehaviorProcessor
import timber.log.Timber
@ -27,19 +28,29 @@ class DepictModel @Inject constructor(private val depictsClient: DepictsClient)
fun searchAllEntities(query: String): Flowable<List<DepictedItem>> {
return if (query.isBlank())
nearbyPlaces.switchMap { places: List<Place> ->
depictsClient.getEntities(places.toIds())
.map {
it.entities()
.values
.mapIndexed { index, entity -> DepictedItem(entity, places[index]) }
}
.onErrorResumeWithEmptyList()
.toFlowable()
getPlaceDepictions(places).toFlowable()
}
else
networkItems(query)
}
/**
* Provides [DepictedItem] instances via a [Single] for a given list of [Place], providing an
* empty list if no places are provided or if there is an error
*/
fun getPlaceDepictions(places: List<Place>): Single<List<DepictedItem>> =
places.toIds().let { ids ->
if (ids.isNotEmpty())
depictsClient.getEntities(ids)
.map{
it.entities()
.values
.mapIndexed { index, entity -> DepictedItem(entity, places[index])}
}
.onErrorResumeWithEmptyList()
else Single.just(emptyList())
}
private fun networkItems(query: String): Flowable<List<DepictedItem>> {
return depictsClient.searchForDepictions(query, SEARCH_DEPICTS_LIMIT, 0)
.onErrorResumeWithEmptyList()

View file

@ -6,6 +6,7 @@ import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.WorkerParameters
import com.google.gson.Gson
import com.mapbox.mapboxsdk.plugins.localization.BuildConfig
@ -151,6 +152,7 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
}
override suspend fun doWork(): Result {
var countUpload = 0
notificationManager = NotificationManagerCompat.from(appContext)
val processingUploads = getNotificationBuilder(
CommonsApplication.NOTIFICATION_CHANNEL_ID_ALL
@ -201,6 +203,8 @@ class UploadWorker(var appContext: Context, workerParams: WorkerParameters) :
contribution.transferred = 0
contribution.state = Contribution.STATE_IN_PROGRESS
contributionDao.saveSynchronous(contribution)
setProgressAsync(Data.Builder().putInt("progress", countUpload).build())
countUpload++
uploadContribution(contribution = contribution)
}
}.collect()

View file

@ -4,11 +4,12 @@ import android.content.Context;
import android.content.Intent;
public class ActivityUtils {
public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
Intent intent = new Intent(context, cls);
for (int flag: flags) {
intent.addFlags(flag);
public static <T> void startActivityWithFlags(Context context, Class<T> cls, int... flags) {
Intent intent = new Intent(context, cls);
for (int flag : flags) {
intent.addFlags(flag);
}
context.startActivity(intent);
}
context.startActivity(intent);
}
}

View file

@ -9,22 +9,22 @@ import javax.inject.Singleton;
@Singleton
public class ImageUtilsWrapper {
@Inject
public ImageUtilsWrapper() {
@Inject
public ImageUtilsWrapper() {
}
}
public Single<Integer> checkIfImageIsTooDark(String bitmapPath) {
return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath))
.subscribeOn(Schedulers.computation());
}
public Single<Integer> checkIfImageIsTooDark(String bitmapPath) {
return Single.fromCallable(() -> ImageUtils.checkIfImageIsTooDark(bitmapPath))
.subscribeOn(Schedulers.computation());
}
public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString,
LatLng latLng) {
return Single.fromCallable(
() -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng))
.subscribeOn(Schedulers.computation())
.map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT
: ImageUtils.IMAGE_OK);
}
public Single<Integer> checkImageGeolocationIsDifferent(String geolocationOfFileString,
LatLng latLng) {
return Single.fromCallable(
() -> ImageUtils.checkImageGeolocationIsDifferent(geolocationOfFileString, latLng))
.subscribeOn(Schedulers.computation())
.map(isDifferent -> isDifferent ? ImageUtils.IMAGE_GEOLOCATION_DIFFERENT
: ImageUtils.IMAGE_OK);
}
}

View file

@ -19,43 +19,44 @@ import timber.log.Timber;
@Singleton
public class WikiBaseClient {
private final WikiBaseInterface wikiBaseInterface;
private final CsrfTokenClient csrfTokenClient;
private final WikiBaseInterface wikiBaseInterface;
private final CsrfTokenClient csrfTokenClient;
@Inject
public WikiBaseClient(WikiBaseInterface wikiBaseInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
this.wikiBaseInterface = wikiBaseInterface;
this.csrfTokenClient = csrfTokenClient;
}
@Inject
public WikiBaseClient(WikiBaseInterface wikiBaseInterface,
@Named(NAMED_COMMONS_CSRF) CsrfTokenClient csrfTokenClient) {
this.wikiBaseInterface = wikiBaseInterface;
this.csrfTokenClient = csrfTokenClient;
}
public Observable<Boolean> postEditEntity(String fileEntityId, String data) {
return csrfToken()
.switchMap(editToken -> wikiBaseInterface.postEditEntity(fileEntityId, editToken, data)
.map(response -> (response.getSuccessVal() == 1)));
}
public Observable<Boolean> postEditEntity(String fileEntityId, String data) {
return csrfToken()
.switchMap(editToken -> wikiBaseInterface.postEditEntity(fileEntityId, editToken, data)
.map(response -> (response.getSuccessVal() == 1)));
}
public Observable<Long> getFileEntityId(UploadResult uploadResult) {
return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName())
.map(response -> (long) (response.query().pages().get(0).pageId()));
}
public Observable<Long> getFileEntityId(UploadResult uploadResult) {
return wikiBaseInterface.getFileEntityId(uploadResult.createCanonicalFileName())
.map(response -> (long) (response.query().pages().get(0).pageId()));
}
public Observable<MwPostResponse> addLabelstoWikidata(long fileEntityId,
String languageCode, String captionValue) {
return csrfToken()
.switchMap(editToken -> wikiBaseInterface
.addLabelstoWikidata(PAGE_ID_PREFIX + fileEntityId, editToken, languageCode, captionValue));
public Observable<MwPostResponse> addLabelstoWikidata(long fileEntityId,
String languageCode, String captionValue) {
return csrfToken()
.switchMap(editToken -> wikiBaseInterface
.addLabelstoWikidata(PAGE_ID_PREFIX + fileEntityId, editToken, languageCode,
captionValue));
}
}
private Observable<String> csrfToken() {
return Observable.fromCallable(() -> {
try {
return csrfTokenClient.getTokenBlocking();
} catch (Throwable throwable) {
Timber.e(throwable);
return "";
}
});
}
private Observable<String> csrfToken() {
return Observable.fromCallable(() -> {
try {
return csrfTokenClient.getTokenBlocking();
} catch (Throwable throwable) {
Timber.e(throwable);
return "";
}
});
}
}

View file

@ -15,32 +15,33 @@ import org.wikipedia.wikidata.Statement_partial;
public class WikidataClient {
private final WikidataInterface wikidataInterface;
private final Gson gson;
private final WikidataInterface wikidataInterface;
private final Gson gson;
@Inject
public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) {
this.wikidataInterface = wikidataInterface;
this.gson = gson;
}
@Inject
public WikidataClient(WikidataInterface wikidataInterface, final Gson gson) {
this.wikidataInterface = wikidataInterface;
this.gson = gson;
}
/**
* Create wikidata claim to add P18 value
*
* @return revisionID of the edit
*/
Observable<Long> setClaim(Statement_partial claim, String tags) {
return getCsrfToken()
.flatMap(csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken))
.map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
}
/**
* Create wikidata claim to add P18 value
*
* @return revisionID of the edit
*/
Observable<Long> setClaim(Statement_partial claim, String tags) {
return getCsrfToken()
.flatMap(
csrfToken -> wikidataInterface.postSetClaim(gson.toJson(claim), tags, csrfToken))
.map(mwPostResponse -> mwPostResponse.getPageinfo().getLastrevid());
}
/**
* Get csrf token for wikidata edit
*/
@NotNull
private Observable<String> getCsrfToken() {
return wikidataInterface.getCsrfToken()
.map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
}
/**
* Get csrf token for wikidata edit
*/
@NotNull
private Observable<String> getCsrfToken() {
return wikidataInterface.getCsrfToken()
.map(mwQueryResponse -> mwQueryResponse.query().csrfToken());
}
}

View file

@ -44,174 +44,172 @@ import timber.log.Timber;
@Singleton
public class WikidataEditService {
public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
public static final String COMMONS_APP_TAG = "wikimedia-commons-app";
private final Context context;
private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore;
private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient;
private final Gson gson;
private final Context context;
private final WikidataEditListener wikidataEditListener;
private final JsonKvStore directKvStore;
private final WikiBaseClient wikiBaseClient;
private final WikidataClient wikidataClient;
private final Gson gson;
@Inject
public WikidataEditService(final Context context,
final WikidataEditListener wikidataEditListener,
@Named("default_preferences") final JsonKvStore directKvStore,
final WikiBaseClient wikiBaseClient,
final WikidataClient wikidataClient, final Gson gson) {
this.context = context;
this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore;
this.wikiBaseClient = wikiBaseClient;
this.wikidataClient = wikidataClient;
this.gson = gson;
}
/**
* Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call to
* the wikibase API to set tag against the entity.
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addDepictsProperty(final String fileEntityId,
final WikidataItem depictedItem) {
final EditClaim data = editClaim(
ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
: depictedItem.getId()
);
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
.doOnNext(success -> {
if (success) {
Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
} else {
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
}
})
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString());
})
.subscribeOn(Schedulers.io());
}
private EditClaim editClaim(final String entityId) {
return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
}
/**
* Show a success toast when the edit is made successfully
*/
private void showSuccessToast(final String wikiItemName) {
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
final String successMessage = String
.format(Locale.getDefault(), successStringTemplate, wikiItemName);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
* csrfTokenClient
*
* @param fileEntityId
* @return
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.map(mwPostResponse -> mwPostResponse != null);
}
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
if (response != null) {
Timber.d("Caption successfully set, revision id = %s", response);
} else {
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
}
}
public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName, final
Map<String, String> captions) {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return null;
}
return addImageAndMediaLegends(wikidataPlace, fileName, captions);
}
public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
final Map<String, String> captions) {
final Snak_partial p18 = new Snak_partial("value", WikidataProperties.IMAGE.getPropertyName(),
new ValueString(fileName.replace("File:", "")));
final List<Snak_partial> snaks = new ArrayList<>();
for (final Map.Entry<String, String> entry : captions.entrySet()) {
snaks.add(new Snak_partial("value",
WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
@Inject
public WikidataEditService(final Context context,
final WikidataEditListener wikidataEditListener,
@Named("default_preferences") final JsonKvStore directKvStore,
final WikiBaseClient wikiBaseClient,
final WikidataClient wikidataClient, final Gson gson) {
this.context = context;
this.wikidataEditListener = wikidataEditListener;
this.directKvStore = directKvStore;
this.wikiBaseClient = wikiBaseClient;
this.wikidataClient = wikidataClient;
this.gson = gson;
}
final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id,
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
/**
* Edits the wikibase entity by adding DEPICTS property. Adding DEPICTS property requires call
* to the wikibase API to set tag against the entity.
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addDepictsProperty(final String fileEntityId,
final WikidataItem depictedItem) {
return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle();
}
public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
if (revisionId != null) {
if (wikidataEditListener != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
}
showSuccessToast(wikidataItem.getName());
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
public Observable addDepictionsAndCaptions(final UploadResult uploadResult, final Contribution contribution) {
return wikiBaseClient.getFileEntityId(uploadResult)
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.switchMap(fileEntityId -> {
if (fileEntityId != null) {
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
return Observable.concat(
depictionEdits(contribution, fileEntityId),
captionEdits(contribution, fileEntityId)
);
} else {
Timber.d("Error acquiring EntityId for image: %s", uploadResult);
return Observable.empty();
}
}
final EditClaim data = editClaim(
ConfigUtils.isBetaFlavour() ? "Q10" // Wikipedia:Sandbox (Q10)
: depictedItem.getId()
);
}
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet())
.concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue()));
}
private Observable<Boolean> depictionEdits(Contribution contribution, Long fileEntityId) {
final ArrayList<WikidataItem> depictedItems = new ArrayList<>(contribution.getDepictedItems());
final WikidataPlace wikidataPlace = contribution.getWikidataPlace();
if (wikidataPlace != null) {
depictedItems.add(wikidataPlace);
return wikiBaseClient.postEditEntity(PAGE_ID_PREFIX + fileEntityId, gson.toJson(data))
.doOnNext(success -> {
if (success) {
Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
} else {
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
}
})
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting DEPICTS property");
ViewUtil.showLongToast(context, throwable.toString());
})
.subscribeOn(Schedulers.io());
}
private EditClaim editClaim(final String entityId) {
return EditClaim.from(entityId, WikidataProperties.DEPICTS.getPropertyName());
}
/**
* Show a success toast when the edit is made successfully
*/
private void showSuccessToast(final String wikiItemName) {
final String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
final String successMessage = String
.format(Locale.getDefault(), successStringTemplate, wikiItemName);
ViewUtil.showLongToast(context, successMessage);
}
/**
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from
* csrfTokenClient
*
* @param fileEntityId
* @return
*/
@SuppressLint("CheckResult")
private Observable<Boolean> addCaption(final long fileEntityId, final String languageCode,
final String captionValue) {
return wikiBaseClient.addLabelstoWikidata(fileEntityId, languageCode, captionValue)
.doOnNext(mwPostResponse -> onAddCaptionResponse(fileEntityId, mwPostResponse))
.doOnError(throwable -> {
Timber.e(throwable, "Error occurred while setting Captions");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.map(mwPostResponse -> mwPostResponse != null);
}
private void onAddCaptionResponse(Long fileEntityId, MwPostResponse response) {
if (response != null) {
Timber.d("Caption successfully set, revision id = %s", response);
} else {
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
}
}
public Long createClaim(@Nullable final WikidataPlace wikidataPlace, final String fileName,
final Map<String, String> captions) {
if (!(directKvStore.getBoolean("Picture_Has_Correct_Location", true))) {
Timber
.d("Image location and nearby place location mismatched, so Wikidata item won't be edited");
return null;
}
return addImageAndMediaLegends(wikidataPlace, fileName, captions);
}
public Long addImageAndMediaLegends(final WikidataItem wikidataItem, final String fileName,
final Map<String, String> captions) {
final Snak_partial p18 = new Snak_partial("value",
WikidataProperties.IMAGE.getPropertyName(),
new ValueString(fileName.replace("File:", "")));
final List<Snak_partial> snaks = new ArrayList<>();
for (final Map.Entry<String, String> entry : captions.entrySet()) {
snaks.add(new Snak_partial("value",
WikidataProperties.MEDIA_LEGENDS.getPropertyName(), new DataValue.MonoLingualText(
new WikiBaseMonolingualTextValue(entry.getValue(), entry.getKey()))));
}
final String id = wikidataItem.getId() + "$" + UUID.randomUUID().toString();
final Statement_partial claim = new Statement_partial(p18, "statement", "normal", id,
Collections.singletonMap(WikidataProperties.MEDIA_LEGENDS.getPropertyName(), snaks),
Arrays.asList(WikidataProperties.MEDIA_LEGENDS.getPropertyName()));
return wikidataClient.setClaim(claim, COMMONS_APP_TAG).blockingSingle();
}
public void handleImageClaimResult(final WikidataItem wikidataItem, final Long revisionId) {
if (revisionId != null) {
if (wikidataEditListener != null) {
wikidataEditListener.onSuccessfulWikidataEdit();
}
showSuccessToast(wikidataItem.getName());
} else {
Timber.d("Unable to make wiki data edit for entity %s", wikidataItem);
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
}
}
public Observable addDepictionsAndCaptions(final UploadResult uploadResult,
final Contribution contribution) {
return wikiBaseClient.getFileEntityId(uploadResult)
.doOnError(throwable -> {
Timber
.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
})
.switchMap(fileEntityId -> {
if (fileEntityId != null) {
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
return Observable.concat(
depictionEdits(contribution, fileEntityId),
captionEdits(contribution, fileEntityId)
);
} else {
Timber.d("Error acquiring EntityId for image: %s", uploadResult);
return Observable.empty();
}
}
);
}
private Observable<Boolean> captionEdits(Contribution contribution, Long fileEntityId) {
return Observable.fromIterable(contribution.getMedia().getCaptions().entrySet())
.concatMap(entry -> addCaption(fileEntityId, entry.getKey(), entry.getValue()));
}
private Observable<Boolean> depictionEdits(Contribution contribution, Long fileEntityId) {
return Observable.fromIterable(contribution.getDepictedItems())
.concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
}
return Observable.fromIterable(depictedItems)
.concatMap(wikidataItem -> addDepictsProperty(fileEntityId.toString(), wikidataItem));
}
}

View file

@ -57,22 +57,21 @@
android:layout_marginTop="@dimen/activity_margin_vertical"
android:layout_marginBottom="@dimen/activity_margin_vertical"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
android:layout_width="match_parent" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/next_button"
android:layout_gravity="right"
style="?android:textAppearanceMedium"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="NEXT"
android:backgroundTint="#D6DCE0"
android:layout_marginBottom="@dimen/activity_margin_vertical" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/next_button"
android:layout_gravity="right"
style="?android:textAppearanceMedium"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
android:text="I am not sure"
android:backgroundTint="#D6DCE0"
android:layout_marginBottom="@dimen/activity_margin_vertical" />
</LinearLayout>
</ScrollView>

View file

@ -1,65 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_marginTop="@dimen/activity_margin_vertical"
android:layout_marginBottom="@dimen/activity_margin_vertical">
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin_vertical"
android:layout_marginBottom="@dimen/activity_margin_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<RadioButton
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:id="@+id/quiz_positive_answer"
android:textIsSelectable="false"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"/>
<Button
android:id="@+id/quiz_positive_answer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/yes"
android:textColor="@color/white"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="YES"
style="?android:textAppearanceMedium"
android:textColor="@color/secondaryTextColor" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_done_black_24dp"
android:layout_marginLeft="@dimen/activity_margin_horizontal" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/activity_margin_vertical"
android:orientation="horizontal">
<RadioButton
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:textIsSelectable="false"
android:id="@+id/quiz_negative_answer"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="NO"
style="?android:textAppearanceMedium"
android:textColor="@color/secondaryTextColor" />
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_clear_black_24dp"
android:layout_marginLeft="@dimen/activity_margin_horizontal" />
</LinearLayout>
</LinearLayout>
<Button
android:id="@+id/quiz_negative_answer"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/no"
android:textColor="@color/white"
android:layout_marginLeft="@dimen/activity_margin_horizontal"
android:layout_marginRight="@dimen/activity_margin_horizontal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -11,25 +10,71 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/select_location_location_picker"
android:tint="@color/white"
app:backgroundTint="@color/wikimedia_green"
app:elevation="3dp"
app:layout_anchorGravity="top|end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_check_black_24dp"
android:contentDescription="@string/select_location_location_picker" />
app:srcCompat="@drawable/ic_check_black_24dp" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/tv_attribution"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:text="@string/map_attribution"
android:textAlignment="center"
android:layout_margin="8dp"
android:textSize="10sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintBottom_toTopOf="@+id/map_bottom_layout"
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/map_bottom_layout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<Button
android:id="@+id/modify_location"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/modify_location"
android:textColor="@color/white"
android:visibility="gone"
android:layout_margin="5dp"
app:layout_constraintBottom_toBottomOf="@id/map_bottom_layout"
app:layout_constraintEnd_toStartOf="@+id/guideline3"
app:layout_constraintStart_toStartOf="@id/map_bottom_layout"
app:layout_constraintTop_toTopOf="@id/map_bottom_layout" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
<TextView
android:id="@+id/show_in_map"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/show_in_map_app"
android:textAlignment="center"
android:textColor="@color/primaryColor"
android:textSize="14sp"
android:visibility="gone"
android:layout_margin="5dp"
app:layout_constraintBottom_toBottomOf="@id/map_bottom_layout"
app:layout_constraintEnd_toEndOf="@id/map_bottom_layout"
app:layout_constraintStart_toStartOf="@+id/guideline3"
app:layout_constraintTop_toTopOf="@id/map_bottom_layout" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/mainBackground"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/caption_language_textview"
style="@style/CaptionTextLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:text="Caption"
android:textColor="?attr/caption_description_text_color"
android:textSize="16dp"
android:textStyle="bold" />
<TextView
android:id="@+id/caption_text"
style="@style/CaptionTextLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="textMultiLine"
android:textColor="?attr/caption_description_text_color"
android:textStyle="normal" />
</LinearLayout>
</LinearLayout>

View file

@ -23,6 +23,20 @@
layout="@layout/layout_edit_categories" />
</LinearLayout>
<LinearLayout
android:id="@+id/dummy_caption_description_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="bottom"
android:elevation="35dp"
android:orientation="vertical"
android:visibility="gone"
android:weightSum="10">
<include
layout="@layout/show_captions_descriptions" />
</LinearLayout>
<ImageView
android:id="@+id/mediaDetailImageFailed"
android:layout_height="wrap_content"
@ -163,6 +177,7 @@
android:layout_width="@dimen/widget_margin"
android:layout_height="match_parent"
style="@style/MediaDetailTextBody"
android:textIsSelectable="true"
tools:text="Captions of the media" />
</LinearLayout>
@ -187,9 +202,19 @@
android:padding="@dimen/small_gap"
android:textColor="?attr/mediaDetailsText"
android:textSize="@dimen/description_text_size"
android:textIsSelectable="true"
tools:text="Description of the media goes here. This can potentially be fairly long, and will need to wrap across multiple lines. We hope it looks nice though." />
</LinearLayout>
<TextView
android:id="@+id/show_caption_description_textview"
style="@style/MediaDetailTextLabelGeneric"
android:layout_width="match_parent"
android:gravity="center"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/dimen_10"
android:text="@string/media_detail_in_all_languages" />
<View
android:background="?attr/mediaDetailSpacerColor"
android:layout_width="match_parent"

View file

@ -26,17 +26,6 @@
android:text="@string/menu_settings"
android:textSize="18sp" />
<TextView
android:id="@+id/more_tutorial"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp"
android:drawableStart="@drawable/ic_help_black_24dp"
android:drawablePadding="12dp"
android:padding="8dp"
android:text="@string/navigation_item_info"
android:textSize="18sp" />
<TextView
android:id="@+id/more_feedback"
android:layout_width="match_parent"

View file

@ -1,114 +1,119 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:id="@+id/category_edit_layout"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:orientation="vertical"
android:background="?attr/mainBackground"
android:elevation="30dp">
<ScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="@dimen/half_standard_height"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="Type categories"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<FrameLayout
android:id="@+id/category_search_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_container_search"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<SearchView
android:id="@+id/et_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/categories_search_text_hint"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/pb_categories"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone" />
</FrameLayout>
<TextView
android:id="@+id/existing_categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<TextView
android:id="@+id/no_results_found"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="No results found"
android:textSize="@dimen/description_text_size"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/category_edit_layout"
android:layout_height="wrap_content"
android:layout_margin="15dp"
android:orientation="vertical"
android:weightSum="6"
>
android:background="?attr/mainBackground"
android:elevation="30dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_categories"
<TextView
android:id="@+id/tv_subtitle"
android:layout_width="wrap_content"
android:layout_height="@dimen/half_standard_height"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="Type categories"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<FrameLayout
android:id="@+id/category_search_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4"
android:background="?attr/mainBackground"/>
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_gap"
>
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/til_container_search"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<SearchView
android:id="@+id/et_search"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:queryHint="@string/categories_search_text_hint"
android:iconifiedByDefault="false"
android:imeOptions="actionSearch"
android:inputType="text"
android:maxLines="1"/>
</com.google.android.material.textfield.TextInputLayout>
<ProgressBar
android:id="@+id/pb_categories"
style="?android:progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/tiny_gap"
android:layout_marginRight="@dimen/tiny_gap"
android:layout_gravity="center_vertical|end"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone" />
</FrameLayout>
<TextView
android:id="@+id/existing_categories"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:textSize="@dimen/subtitle_text"
android:visibility="visible" />
<TextView
android:id="@+id/no_results_found"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:gravity="center_vertical"
android:text="No results found"
android:textSize="@dimen/description_text_size"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="2">
<Button
android:id="@+id/cancel_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/opak_middle_grey"
/>
<Button
android:id="@+id/update_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/category_edit_button_text"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/button_blue"
/>
android:layout_height="match_parent"
android:orientation="vertical"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/rv_categories"
android:layout_width="match_parent"
android:layout_height="@dimen/dimen_200"
android:background="?attr/mainBackground"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/cancel_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/cancel"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/opak_middle_grey"
/>
<Button
android:id="@+id/update_categories_button"
android:layout_width="wrap_content"
android:layout_margin="@dimen/quarter_standard_height"
android:layout_height="wrap_content"
android:text="@string/category_edit_button_text"
android:padding="@dimen/small_gap"
android:textColor="@color/white"
android:background="@color/button_blue"
/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/dimen_20"
android:background="?attr/mainBackground"
android:elevation="30dp"
android:padding="@dimen/dimen_10">
<TextView
android:id="@+id/caption_label"
style="@style/MediaDetailTextLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Captions"
android:textColor="?attr/caption_description_text_color" />
<ListView
android:id="@+id/caption_listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/caption_label" />
<TextView
android:id="@+id/description_label"
style="@style/MediaDetailTextLabel"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/caption_listview"
android:layout_marginTop="@dimen/dimen_10"
android:text="Descriptions"
android:textColor="?attr/caption_description_text_color" />
<ProgressBar
android:id="@+id/pb_circular"
style="?android:attr/progressBarStyleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:background="?attr/mainBackground"
android:indeterminate="true"
android:indeterminateOnly="true"
android:visibility="gone" />
<WebView
android:id="@+id/description_webview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/description_label"
android:layout_marginTop="@dimen/dimen_10"
android:layout_marginBottom="@dimen/dimen_20"
android:background="?attr/mainBackground"
tools:ignore="WebViewLayout" />
</RelativeLayout>

View file

@ -5,6 +5,7 @@
* Azouz.anis
* ButterflyOfFire
* Claw eg
* Dr. Mohammed
* Kassem7899
* Meno25
* Mido
@ -506,6 +507,7 @@
<string name="delete_helper_ask_alert_set_positive_button_reason">لأنها</string>
<string name="category_edit_helper_show_edit_title">تحديث التصنيف</string>
<string name="category_edit_button_text">تحديث التصنيفات</string>
<string name="coordinates_edit_helper_show_edit_title_success">نجاح</string>
<string name="share_image_via">مشاركة الصور عبر</string>
<string name="no_achievements_yet">أنت لم تقدم أية مساهمات حتى الآن</string>
<string name="account_created">تم إنشاء الحساب!</string>
@ -544,4 +546,8 @@
<string name="confirm">تأكيد</string>
<string name="instructions_title">التعليمات</string>
<string name="limited_connection_mode" fuzzy="true">اتصال محدود</string>
<string name="choose_a_location">اختر موقعًا</string>
<string name="select_location_location_picker">حدد موقعًا</string>
<string name="modify_location">أضف الموقع</string>
<string name="image_location">مكان الصورة</string>
</resources>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Chinamoonroll
-->
<resources>
<string name="crash_dialog_title">Commons kantun usak</string>
<string name="crash_dialog_text">Mimih. Wénten sané iwang!</string>
<string name="crash_dialog_comment_prompt">Bangyang iraga nawang napi sané ragané margiang, lantas wedar saking rerepél majeng iraga. Pacang ngawantu iraga ngabecikin!</string>
<string name="crash_dialog_ok_toast">Matur suksma!</string>
</resources>

View file

@ -87,6 +87,8 @@
<string name="display_list_button">Lis</string>
<string name="contributions_subtitle_zero">(Durung kaunggah)</string>
<string name="categories_not_found">Nénten wénten kategori sané patut sareng %1$s</string>
<string name="no_child_classes">%1$s ten ngelah kelas turunan</string>
<string name="no_parent_classes">%1$s ten ngelah kelas rerama</string>
<string name="categories_activity_title">Kategori</string>
<string name="title_activity_settings">Setélan</string>
<string name="title_activity_signup">Daptar</string>
@ -94,21 +96,37 @@
<string name="title_activity_category_details">Kategori</string>
<string name="title_activity_review">Ulasan Peer</string>
<string name="menu_about">Indik</string>
<string name="about_improve">Kardi &lt;a href=\"%1$s\"&gt;isu GitHub&lt;/a&gt; anyar antuk parihindik miwah panikayan kakutu.</string>
<string name="about_privacy_policy">Parikrama paragaan</string>
<string name="about_credits">Krédit</string>
<string name="title_activity_about">Indik</string>
<string name="menu_feedback">Kirim umpan walik (liwat Rerepél)</string>
<string name="provider_categories">Kategori sané mangkin kaanggén</string>
<string name="waiting_first_sync">Ngantosang sinkronisasi kapertama…</string>
<string name="menu_retry_upload">Indayang malih</string>
<string name="menu_cancel_upload">Wangdé</string>
<string name="menu_download">Unduh</string>
<string name="preference_license">Lisénsi baku</string>
<string name="use_previous">Anggén murda miwah pidarta sadurungné</string>
<string name="preference_theme">Téma</string>
<string name="license_name_cc_by_four">Atribusi 4.0</string>
<string name="license_name_cc_by">Atribusi 3.0</string>
<string name="license_name_cc0">CC0</string>
<string name="license_name_cc_by_sa_3_0">CC BY-SA 3.0</string>
<string name="license_name_cc_by_3_0">CC BY 3.0</string>
<string name="license_name_cc_by_sa_4_0">CC BY-SA 4.0</string>
<string name="license_name_cc_by_4_0">CC BY 4.0</string>
<string name="tutorial_2_subtext_1">Objék palemahan (sekar, baburon, gunung)</string>
<string name="tutorial_2_subtext_2">Objék mawiguna (sepeda, stasiun sepur)</string>
<string name="tutorial_2_subtext_3">Jadma kasub (bupati ragané, atlét Olimpiade sané ragané tepukin)</string>
<string name="tutorial_3_text">Durus SAMPUNANG ngunggah:</string>
<string name="tutorial_3_subtext_1">Potrék sélfi utawi potrék timpal ragané</string>
<string name="tutorial_3_subtext_2">Gambar sané kaunduh saking internét</string>
<string name="tutorial_3_subtext_3">Tangkepan layar satunggil aplikasi</string>
<string name="tutorial_4_text">Conto unggahan:</string>
<string name="tutorial_4_subtext_1">Murda: Wangunan Opera Sydney</string>
<string name="tutorial_4_subtext_2">Pidarta: Wangunan Opera Sydney kacingak saking sebrang celuk</string>
<string name="tutorial_4_subtext_3">Kategori: Wangunan Opera Sydney saking kauh, pacingakan Wangunan Opera Sydney</string>
<string name="welcome_final_text">Napiké Ida ngartos?</string>
<string name="welcome_final_button_text">Inggih!</string>
<string name="welcome_help_button_text">Pidarta lianan</string>
@ -116,6 +134,10 @@
<string name="detail_panel_cats_loading">Ngamuat…</string>
<string name="detail_panel_cats_none">Nénten wénten kapilih</string>
<string name="detail_caption_empty">Tanpa sasirah</string>
<string name="detail_description_empty">Tanpa pidarta</string>
<string name="detail_discussion_empty">Tanpa pabligbagan</string>
<string name="detail_license_empty">Lisénsi nénten kauningin</string>
<string name="menu_refresh">Segerang</string>
<string name="ok">OK</string>
<string name="warning">Paingetan</string>
<string name="duplicate_image_found">Katemuin Gambar Kaduplikat</string>
@ -125,6 +147,9 @@
<string name="media_detail_caption">Sesirah</string>
<string name="media_detail_title">Murda</string>
<string name="media_detail_discussion">Pabligbagan</string>
<string name="media_detail_uploaded_date">Tanggal kaunggah</string>
<string name="media_detail_license">Lisénsi</string>
<string name="media_detail_coordinates">Koordinat</string>
<string name="_2fa_code">Kode O2F</string>
<string name="commons_logo">Logo Commons</string>
<string name="commons_website">Situs Commons</string>
@ -147,13 +172,38 @@
<string name="navigation_item_settings">Setélan</string>
<string name="navigation_item_feedback">Umpan walik</string>
<string name="navigation_item_logout">Medal log</string>
<string name="navigation_item_info">Panuntun</string>
<string name="navigation_item_review">Turéksa</string>
<string name="nearby_info_menu_commons_article">Kaca berkas Commons</string>
<string name="nearby_info_menu_wikipedia_article">Suratan Wikipédia</string>
<string name="upload_problem_image_dark">Gambar bes peteng.</string>
<string name="upload_problem_image_blurry">Gambar burem.</string>
<string name="upload_problem_image_duplicate">Gambar sampun wénten ring Commons</string>
<string name="upload_problem_different_geolocation">Gamble kaambil ring genah tiosan.</string>
<string name="upload_problem_fbmd">Durus wantah unggaj gambar sané ragané ambil ngaraga. Sampunang unggah gambar sané ragané temuin ring akun Facebook jadma liyanan.</string>
<string name="upload_problem_do_you_continue">Napiké ragané kantunjagi ngunggah gambar puniki?</string>
<string name="upload_connection_error_alert_title">Galat sambungan</string>
<string name="upload_connection_error_alert_detail">Prosés pangunggahan perlu aksés internét urip. Durus turéksa sambungan jaringan ragané.</string>
<string name="upload_problem_image">Pikobet katemuin ring gambar</string>
<string name="login_to_your_account">Manjing log nuju akun ragané</string>
<string name="send_log_file">Kirim berkas log</string>
<string name="skip_login">Liwatin</string>
<string name="navigation_item_login">Manjing log</string>
<string name="nearby_directions">Paarah</string>
<string name="nearby_wikidata">Wikidata</string>
<string name="nearby_wikipedia">Wikipédia</string>
<string name="nearby_commons">Commons</string>
<string name="about_rate_us">Rating iraga</string>
<string name="welcome_skip_button">Liwatin Tutorial</string>
<string name="about_translate">Pangalih basa</string>
<string name="about_translate_title">Basa</string>
<string name="about_translate_proceed">Kamargiang</string>
<string name="about_translate_cancel">Wangdé</string>
<string name="retry">Indayang malih</string>
<string name="no_images_found">Gambar ten katemu!</string>
<string name="image_uploaded_by">Kaunggah olih: %1$s</string>
<string name="appwidget_img">Gambar rahina mangkin</string>
<string name="app_widget_heading">Gambar rahina mangkin</string>
<string name="menu_search_button">Rereh</string>
<string name="search_commons">Rereh Commons</string>
<string name="title_activity_search">Rereh</string>
@ -161,16 +211,66 @@
<string name="provider_searches">Kuéri parerehan sané mangkin</string>
<string name="search_tab_title_media">Média</string>
<string name="search_tab_title_categories">Kategori</string>
<string name="explore_tab_title_mobile">Kaunggah saking sélulér</string>
<string name="menu_set_wallpaper">Dadosang wallpaper</string>
<string name="wallpaper_set_successfully">Wallpaper sampun kapasang!</string>
<string name="quiz_question_string">Napiké gambar puniki OK antuk kaunggah?</string>
<string name="question">Pitakén</string>
<string name="result">Asil</string>
<string name="continue_message">Lanturang</string>
<string name="correct">Cawisan sané becik</string>
<string name="wrong">Pisaur Iwang</string>
<string name="delete_recent_searches_dialog">Napiké Ida jagi ngaresikin lelintihan parerehan Ida?</string>
<string name="delete_search_dialog">Napiké ragané jagi ngusap parerehan puniki?</string>
<string name="search_history_deleted">Lelintihan parerehan kausap</string>
<string name="nominate_delete">Usulan Pangusapan</string>
<string name="delete">Usap</string>
<string name="Achievements">Panghargaan</string>
<string name="Profile">Profil</string>
<string name="statistics">Statistik</string>
<string name="statistics_thanks">Haturan Suksma Katampi</string>
<string name="statistics_featured">Gambar Pilihan</string>
<string name="level">Tingkat</string>
<string name="images_uploaded">Gambar Kaupload</string>
<string name="images_used_by_wiki">Gambar Kaanggén</string>
<string name="contributions_fragment">Pituut</string>
<string name="nearby_fragment">Nampek</string>
<string name="list_sheet">Lis</string>
<string name="step_count">Langkat %1$d saking %2$d: %3$s</string>
<string name="next">Salanturné</string>
<string name="previous">Sadurungné</string>
<string name="submit">Kumpulang</string>
<string name="title_page_bookmarks_pictures">Gambar</string>
<string name="title_page_bookmarks_locations">Genah</string>
<string name="nominate_for_deletion_done">Puput</string>
<string name="send_thank_toast">Kirim Suksma majeng %1$s</string>
<string name="review_thanks_yes_button_text">Gambar salanturnyané</string>
<string name="review_thanks_no_button_text">Nggih, ngujang ten</string>
<string name="menu_option_read">Cingak wacén</string>
<string name="menu_option_unread">Cingak durung kawacén</string>
<string name="please_wait">Jantos dumun…</string>
<string name="copied_successfully">Katurun</string>
<string name="exif_tag_name_software">Piranti lunak</string>
<string name="upload_cancelled">Pangunggahan Kawangdé</string>
<string name="review_is_uploaded_by">%1$s kaunggah olih: %2$s</string>
<string name="delete_helper_ask_reason_copyright_logo">Logo</string>
<string name="title_for_media">MÉDIA</string>
<string name="title_app_shortcut_setting">Pangaturan</string>
<string name="load_more">Muat luwih akéh</string>
<string name="instructions_title">Patunjuk</string>
<string name="favorites">Senengan</string>
<string name="leaderboard_yearly">Ngawarsa</string>
<string name="leaderboard_weekly">Ngawuku</string>
<string name="leaderboard_all_time">Makejang kala</string>
<string name="leaderboard_upload">Unggah</string>
<string name="leaderboard_nearby">Nampek</string>
<string name="leaderboard_used">Kaanggén</string>
<string name="leaderboard_my_rank_button_text">Paringkat Titiang</string>
<string name="mapbox_telemetry">Télémétri Mapbox</string>
<string name="statistics_quality">Kualitas Gambar</string>
<string name="resuming_upload">Ngalanturang unggahan...</string>
<string name="license_step_title">Lisénsi Média</string>
<string name="media_detail_in_all_languages">Ring samian basa</string>
<string name="choose_a_location">Pilihin genah</string>
<string name="select_location_location_picker">Pilih Genah</string>
</resources>

View file

@ -236,7 +236,10 @@
<string name="showcase_view_no_longer_exists">Това място вече не съществува.</string>
<string name="no_images_found">Не са открити изображения!</string>
<string name="menu_search_button">Търсене</string>
<string name="search_commons">Търсене в Общомедия</string>
<string name="title_activity_search">Търсене</string>
<string name="search_recent_header">Скорошни търсения:</string>
<string name="provider_searches">Скорошни заявки за търсене</string>
<string name="error_loading_categories">Грешка при зареждането на категориите.</string>
<string name="error_loading_depictions">Грешка при зареждането на описанията.</string>
<string name="search_tab_title_media">Мултимедия</string>

View file

@ -312,6 +312,7 @@
<string name="category_edit_helper_show_edit_title_success">সফল</string>
<string name="category_edit_helper_edit_message_else">বিষয়শ্রেণী হালনাগাদ করা সম্ভব হয়নি।</string>
<string name="category_edit_button_text">বিষয়শ্রেণীগুলি হালনাগাদ করুন</string>
<string name="coordinates_edit_helper_show_edit_title_success">সফল</string>
<string name="account_created">অ্যাকাউন্ট তৈরি করা হয়েছে!</string>
<string name="remove_bookmark">বুকমার্ক থেকে সরানো হয়েছে</string>
<string name="add_bookmark">বুকমার্কে যোগ করা হয়েছে</string>

View file

@ -322,6 +322,7 @@
<string name="category_edit_helper_show_edit_title_success">Succes</string>
<string name="category_edit_helper_edit_message_else">Kunne ikke tilføje kategorier.</string>
<string name="category_edit_button_text">Opdater kategorier</string>
<string name="coordinates_edit_helper_show_edit_title_success">Succes</string>
<string name="share_image_via">Del billede via</string>
<string name="account_created">Konto oprettet!</string>
<string name="some_error">Der var en fejl!</string>

View file

@ -116,7 +116,7 @@
<string name="display_list_button">Liste</string>
<string name="contributions_subtitle_zero">(Noch keine hochgeladenen Dateien)</string>
<string name="categories_not_found">Die Kategorie „%1$s“ wurde nicht gefunden</string>
<string name="depictions_not_found">Keine Wikidata Gegenstände gefunden für %1$s</string>
<string name="depictions_not_found">Kein Wikidata-Objekt für %1$s gefunden</string>
<string name="no_child_classes">%1$s hat keine child-Klasse</string>
<string name="no_parent_classes">%1$s hat keine parent-Klasse</string>
<string name="categories_skip_explanation">Füge Kategorien hinzu, um deine Bilder auf Wikimedia Commons auffindbarer zu machen.\nBeginne mit der Eingabe, um Kategorien hinzuzufügen.</string>
@ -306,7 +306,7 @@
<string name="error_loading_depictions">Beim Laden von Darstellungen ist ein Fehler aufgetreten.</string>
<string name="search_tab_title_media">Medien</string>
<string name="search_tab_title_categories">Kategorien</string>
<string name="search_tab_title_depictions">Einträge</string>
<string name="search_tab_title_depictions">Objekte</string>
<string name="explore_tab_title_featured">Vorgestellt</string>
<string name="explore_tab_title_mobile">Über mobil hochgeladen</string>
<string name="successful_wikidata_edit">Bild zu %1$s auf Wikidata hinzugefügt!</string>
@ -512,10 +512,15 @@
<string name="text_copy">Text in die Zwischenablage kopiert</string>
<string name="notification_mark_read">Benachrichtigung als gelesen markieren</string>
<string name="some_error">Ein Fehler ist aufgetreten!</string>
<string name="place_state">Zustand des Platzes:</string>
<string name="place_state_exists">Existiert</string>
<string name="place_state_needs_photo">Benötigt Foto</string>
<string name="place_type">Platztyp:</string>
<string name="nearby_search_hint">Brücke, Museum, Hotel etc.</string>
<string name="you_must_reset_your_passsword">Bei der Anmeldung ist etwas schief gelaufen, du musst dein Passwort zurücksetzen!</string>
<string name="title_for_media">MEDIA</string>
<string name="title_for_child_classes">CHILD CLASSES</string>
<string name="title_for_parent_classes">PARENT CLASSES</string>
<string name="upload_nearby_place_found_title">Ort in der Nähe gefunden</string>
<string name="upload_nearby_place_found_description">Ist dies ein Foto vom Ort %1$s?</string>
<string name="title_app_shortcut_bookmark">Lesezeichen</string>
@ -525,6 +530,7 @@
<string name="wallpaper_set_unsuccessfully">Etwas ist schief gelaufen. Konnte das Hintergrundbild nicht einstellen</string>
<string name="setting_wallpaper_dialog_title">Als Hintergrundbild festlegen</string>
<string name="setting_wallpaper_dialog_message">Hintergrundbild wird festgelegt. Bitte warten...</string>
<string name="theme_default_name">System folgen</string>
<string name="theme_dark_name">Dunkel</string>
<string name="theme_light_name">Hell</string>
<string name="cannot_open_location_settings">Die Standorteinstellungen konnten nicht geöffnet werden. Bitte schalte den Standort manuell ein</string>
@ -546,6 +552,7 @@
<string name="wikipedia_instructions_step_3">3. Suche einen geeigneten Abschnitt im Artikel für dein Bild</string>
<string name="wikipedia_instructions_step_4">4. Klicken Sie auf das Symbol „Bearbeiten“ (das einem Stift ähnelt) für diesen Abschnitt.</string>
<string name="wikipedia_instructions_step_5">5. Füge den wikitext an der entsprechenden Stelle ein.</string>
<string name="wikipedia_instructions_step_6">6. Bearbeite den Wikitext für eine geeignete Positionierung, falls erforderlich. Weitere Informationen findest du &lt;a href=\"https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image\"&gt;hier&lt;/a&gt;.</string>
<string name="wikipedia_instructions_step_7">7. Veröffentliche den Artikel</string>
<string name="copy_wikicode_to_clipboard">wikicode in die Zwischenablage kopieren</string>
<string name="pause">pausieren</string>
@ -592,11 +599,19 @@
<string name="license_step_title">Medienlizenz</string>
<string name="media_detail_step_title">Mediendetails</string>
<string name="menu_view_category_page">Kategorieseite anzeigen</string>
<string name="menu_view_item_page">Objektseite ansehen</string>
<string name="app_ui_language">Benutzeroberflächensprache</string>
<string name="remove">Entfernt eine Beschriftung und Beschreibung</string>
<string name="read_help_link">Mehr lesen</string>
<string name="media_detail_in_all_languages">In allen Sprachen</string>
<string name="choose_a_location">Wähle einen Ort</string>
<string name="pan_and_zoom_to_adjust">Schwenken und Zoomen zum Einstellen</string>
<string name="exit_location_picker">Beenden der Ortswahl</string>
<string name="select_location_location_picker">Ort auswählen</string>
<string name="location_picker_image_view">Ortauswahl Bildansicht</string>
<string name="show_in_map_app">In Karten-App anzeigen</string>
<string name="modify_location">Standort beabeiten</string>
<string name="location_picker_image_view">Die Bildansicht der Standortauswahl</string>
<string name="location_picker_image_view_shadow">Der Schatten der Bildansicht der Ortsauswahl</string>
<string name="image_location">Bildstandort</string>
<string name="check_whether_location_is_correct">Überprüfe, ob der Standort korrekt ist</string>
</resources>

View file

@ -4,6 +4,7 @@
* Domdomegg
* Evropi
* Geraki
* Giannaras99
* Giorgos456
* Glavkos
* KATRINE1992
@ -359,4 +360,5 @@
<string name="leaderboard_upload">Ανέβασμα</string>
<string name="leaderboard_nearby">Γειτονικά</string>
<string name="leaderboard_used">Σε χρήση</string>
<string name="media_detail_in_all_languages">Για όλες τις γλώσσες</string>
</resources>

View file

@ -2,6 +2,7 @@
<!-- Authors:
* 2axterix2
* Adjen
* Agent
* Astroemi
* Benfutbol10
* Carlosmg.dg
@ -12,6 +13,7 @@
* Hasley
* Ihojose
* JO777
* Jackiezelaya
* Jduranboger
* Jelou
* Johnny243
@ -252,6 +254,7 @@
<string name="upload_problem_different_geolocation">Esta imagen fue tomada en una ubicación diferente.</string>
<string name="upload_problem_fbmd">Por favor sube solo fotografías que tu mismo hayas tomado. No subas imágenes o fotografías que hayas encontrado en las cuentas de Facebook de otros.</string>
<string name="upload_problem_do_you_continue">¿Todavía quieres cargar esta imagen?</string>
<string name="upload_connection_error_alert_detail">El cargar el proceso requiere acceso de internet activo. Complacer comprobar vuestra conexión de red.</string>
<string name="upload_problem_image">Problemas encontrados en la imagen</string>
<string name="internet_downloaded">Carga únicamente fotografías que hayas creado tú. No cargues imágenes o fotografías que hayas descargado de Internet.</string>
<string name="use_external_storage">Guardar tomas en la aplicación</string>
@ -500,6 +503,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">No se pudieron añadir las categorías.</string>
<string name="category_edit_button_text">Actualizar categorías</string>
<string name="coordinates_edit_helper_make_edit_toast">Intentando actualizar coordenadas.</string>
<string name="coordinates_edit_helper_show_edit_title">Actualización de coordenadas</string>
<string name="coordinates_edit_helper_show_edit_title_success">Éxito</string>
<string name="coordinates_edit_helper_show_edit_message">Coordenadas %1$s añadidas.</string>
<string name="coordinates_edit_helper_edit_message_else">No se pudo añadir coordenadas.</string>
<string name="coordinates_picking_unsuccessful">No se pudo conseguir las coordenadas.</string>
<string name="share_image_via">Compartir imagen via</string>
<string name="no_achievements_yet">No ha realizado ninguna contribución aún</string>
<string name="account_created">Cuenta creada</string>
@ -533,4 +542,15 @@
<string name="limited_connection_enabled">Se activó el modo de conexión limitada.</string>
<string name="limited_connection_disabled">Se desactivó el modo de conexión limitada. Las cargas pendientes se reanudarán ahora.</string>
<string name="limited_connection_mode" fuzzy="true">Conexión limitada</string>
<string name="media_detail_in_all_languages">En más idiomas</string>
<string name="choose_a_location">Escoge una ubicación</string>
<string name="pan_and_zoom_to_adjust">Haz una panorámica y acércate para ajustar</string>
<string name="exit_location_picker">Salir del seleccionador de ubicación</string>
<string name="select_location_location_picker">Seleccionar ubicación</string>
<string name="show_in_map_app">Mostrar en la aplicación de mapa</string>
<string name="modify_location">Editar ubicación</string>
<string name="location_picker_image_view">La vista de imagen de la ubicación picker</string>
<string name="location_picker_image_view_shadow">La sombra de la vista de imagen de la ubicación picker</string>
<string name="image_location">Posición de imagen</string>
<string name="check_whether_location_is_correct">Compruebe si la ubicación es correcta</string>
</resources>

View file

@ -205,6 +205,10 @@
<string name="nearby_info_menu_wikidata_article">Wikidata itema</string>
<string name="nearby_info_menu_wikipedia_article">Wikipediako artikulua</string>
<string name="description_info">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.</string>
<string name="upload_problem_image_dark">Irudia ilunegia da.</string>
<string name="upload_problem_image_duplicate">Irudian Commonsen badago.</string>
<string name="upload_problem_different_geolocation">Irudi hau beste leku batean hartu da.</string>
<string name="upload_connection_error_alert_title">Konektatzeko Errorea</string>
<string name="upload_problem_image">arazoak aurkitu dira irudian</string>
<string name="use_external_storage">Irudiak aplikazioan gorde</string>
<string name="use_external_storage_summary">Aplikazioaren kamerarekin ateratako argazkiak zure gailuan gorde</string>
@ -239,6 +243,9 @@
<string name="about_translate_cancel">Utzi</string>
<string name="retry">Saiatu berriro</string>
<string name="showcase_view_whole_nearby_activity">Zugandik hurbil dauden leku hauek irudiak behar dituzte beren Wikipedia artikuluak hornitzeko.\n\nSakatu \'EREMU HONETAN BILATU\' mapa zentratzeko eta kokapen horren inguruan bilaketa hasteko.</string>
<string name="showcase_view_needs_photo">Leku honek argazkia behar du.</string>
<string name="showcase_view_has_photo">Leku honek badu argazkia.</string>
<string name="showcase_view_no_longer_exists">Lekua hau ia ez da existitzen.</string>
<string name="no_images_found">Ez da irudirik aurkitu!</string>
<string name="error_loading_images">Arazo bat egon da irudiak kargatzerakoan.</string>
<string name="image_uploaded_by">Nork igota: %1$s</string>
@ -302,6 +309,7 @@
<string name="contributions_fragment">Ekarpenak</string>
<string name="nearby_fragment">Gertukoak</string>
<string name="notifications">Jakinarazpenak</string>
<string name="read_notifications">Jakinarazpenak (irakurriak)</string>
<string name="list_sheet">Zerrenda</string>
<string name="storage_permission">Biltegiratze baimena</string>
<string name="next">Hurrengoa</string>
@ -311,10 +319,13 @@
<string name="title_page_bookmarks_locations">Kokapenak</string>
<string name="provider_bookmarks">Lastermarkak</string>
<string name="provider_bookmarks_location">Lastermarkak</string>
<string name="search_this_area">Leku honetan bilatu</string>
<string name="nominate_for_deletion_done">Egina</string>
<string name="review_thanks_yes_button_text">Hurrengo orria</string>
<string name="review_thanks_no_button_text">Bai, zergatik ez</string>
<string name="please_wait">Mesedez itxaron…</string>
<string name="copied_successfully">Kopiatua</string>
<string name="skip_image">Irudi hau jauzi</string>
<string name="exif_tag_name_author">Egilea</string>
<string name="exif_tag_name_copyright">Egile-eskubideak</string>
<string name="exif_tag_name_location">Kokapena</string>

View file

@ -32,11 +32,11 @@
<item quantity="one">%1$d پرونده در حال بارگذاری</item>
<item quantity="other">%1$d پرونده در حال بارگذاری</item>
</plurals>
<plurals name="contributions_subtitle" fuzzy="true">
<item quantity="zero">\@string/contributions_subtitle_zero</item>
<plurals name="contributions_subtitle">
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<string name="starting_uploads">آغاز بارگذاری</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">پردازش %1$d بارگذاری پرونده</item>
<item quantity="other">پردازش بارگذاری %1$d پرونده</item>
@ -93,10 +93,11 @@
<string name="menu_nearby">در نزدیکی</string>
<string name="provider_contributions">بارگذاری‌های من</string>
<string name="menu_share">به اشتراک‌گذاشتن</string>
<string name="menu_view_file_page">مشاهدهٔ صفحهٔ پرونده</string>
<string name="share_title_hint">شرح مختصر (الزامی)</string>
<string name="add_caption_toast">لطفاً شرح مختصری برای این پرونده بنویسید</string>
<string name="share_description_hint">توضیحات</string>
<string name="share_caption_hint" fuzzy="true">شرح مختصر (محدود به ۲۵۵ نویسه)</string>
<string name="share_caption_hint">شرح مختصر</string>
<string name="login_failed_network">قادر به ورود نیست - شکست شبکه‌ای</string>
<string name="login_failed_throttled">تلاش ناموفق بیش از حد. لطفاً چند دقیقهٔ دیگر دوباره تلاش کنید</string>
<string name="login_failed_blocked">پوزش، کاربر در ویکی‌انبار بسته شده‌است</string>
@ -111,7 +112,7 @@
<string name="menu_save_categories">ذخیره</string>
<string name="refresh_button">تازه کردن</string>
<string name="display_list_button">فهرست</string>
<string name="contributions_subtitle_zero" fuzzy="true">هنوز هیچ بارگذاری</string>
<string name="contributions_subtitle_zero">(هنوز هیچ بارگذاری)</string>
<string name="categories_not_found">رده‌ای منطبق با %1$s یافت نشد</string>
<string name="categories_skip_explanation">برای دسترسی آسان‌تر در ویکی‌انبار به تصویرهایتان رده بیافزائید.\n\nشروع به افزودن رده.</string>
<string name="categories_activity_title">رده‌ها</string>

View file

@ -7,6 +7,7 @@
* FinlandMan
* Joquliina
* Kyykaarme
* MITO
* Maantietäjä
* Nike
* Olli
@ -548,4 +549,5 @@
<string name="depicts_step_title">Esittää</string>
<string name="menu_view_category_page">Näytä luokkasivu</string>
<string name="read_help_link">Lue lisää</string>
<string name="media_detail_in_all_languages">Kaikilla kielillä</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Adehertogh
* Alno
* BaRaN6161 TURK
* Cyclicus
@ -507,6 +508,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Impossible dajouter des catégories.</string>
<string name="category_edit_button_text">Mettre à jour les catégories</string>
<string name="coordinates_edit_helper_make_edit_toast">Essai de mise à jour des coordonnées.</string>
<string name="coordinates_edit_helper_show_edit_title">Mise à jour des coordonnées</string>
<string name="coordinates_edit_helper_show_edit_title_success">Succès</string>
<string name="coordinates_edit_helper_show_edit_message">Les coordonnées %1$s sont ajoutées.</string>
<string name="coordinates_edit_helper_edit_message_else">Impossible dajouter des coordonnées.</string>
<string name="coordinates_picking_unsuccessful">Impossible dobtenir les coordonnées.</string>
<string name="share_image_via">Partager limage via</string>
<string name="no_achievements_yet">Vous navez encore fait aucune contribution</string>
<string name="account_created">Compte créé!</string>
@ -604,4 +611,15 @@
<string name="app_ui_language">Langue dinterface utilisateur de lapplication</string>
<string name="remove">Supprime une légende et une description</string>
<string name="read_help_link">En savoir plus</string>
<string name="media_detail_in_all_languages">Dans toutes les langues</string>
<string name="choose_a_location">Choisir un emplacement</string>
<string name="pan_and_zoom_to_adjust">Descendre et zoomer pour ajuster</string>
<string name="exit_location_picker">Sortir du sélecteur demplacement</string>
<string name="select_location_location_picker">Sélectionner un emplacement</string>
<string name="show_in_map_app">Afficher dans lapplication carte</string>
<string name="modify_location">Modifier lemplacement</string>
<string name="location_picker_image_view">Laffichage dimage du sélecteur demplacement</string>
<string name="location_picker_image_view_shadow">Lombre de laffichage dimage du sélecteur dévénement</string>
<string name="image_location">Emplacement de limage</string>
<string name="check_whether_location_is_correct">Vérifier si lemplacement est correct</string>
</resources>

View file

@ -26,13 +26,13 @@
<item quantity="other">%1$d feltöltés</item>
</plurals>
<string name="starting_uploads">Feltöltések indítása</string>
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">%1$d feltöltés kezdése</item>
<item quantity="other">%1$d feltöltés kezdése</item>
<plurals name="starting_multiple_uploads">
<item quantity="one">%d feltöltés feldolgozása</item>
<item quantity="other">%d feltöltés feldolgozás</item>
</plurals>
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">%1$d feltöltés</item>
<item quantity="other">%1$d feltöltés</item>
<plurals name="multiple_uploads_title">
<item quantity="one">%d feltöltés</item>
<item quantity="other">%d feltöltés</item>
</plurals>
<plurals name="share_license_summary">
<item quantity="one">Ez a kép %1$s licenc alatt lesz feltöltve</item>
@ -71,7 +71,7 @@
<string name="uploading_queued">Feltöltések késleltetése (korlátozott kapcsolat-mód bekapcsolva)</string>
<string name="upload_completed_notification_title">%1$s feltöltve.</string>
<string name="upload_completed_notification_text">Feltöltés megtekintése</string>
<string name="upload_progress_notification_title_start" fuzzy="true">Feltöltés indul: %1$s</string>
<string name="upload_progress_notification_title_start">Fájl feltöltése: %s</string>
<string name="upload_progress_notification_title_in_progress">%1$s feltöltése</string>
<string name="upload_progress_notification_title_finishing">%1$s feltöltése befejezve</string>
<string name="upload_failed_notification_title">%1$s feltöltése sikertelen</string>
@ -101,7 +101,7 @@
<string name="menu_save_categories">Mentés</string>
<string name="refresh_button">Frissítés</string>
<string name="display_list_button">Lista</string>
<string name="contributions_subtitle_zero" fuzzy="true">Még nincsenek feltöltések</string>
<string name="contributions_subtitle_zero">(Még nincsenek feltöltések)</string>
<string name="categories_not_found">Nincs a(z) „%1$s” keresési kifejezésnek megfelelő kategória</string>
<string name="categories_skip_explanation">Adj kategóriákat a képekhez, hogy könnyebben meg lehessen találni őket a Commonson.\nKezdd el beírni a kategória nevét, hogy hozzáadd.\nBökj erre az üzenetre (vagy a vissza gombra) a lépés kihagyásához</string>
<string name="categories_activity_title">Kategóriák</string>
@ -334,7 +334,7 @@
<string name="storage_permission">Tárolási engedély</string>
<string name="write_storage_permission_rationale_for_image_share">Szükséges az engedélyed a külső tárolóhoz való hozzáféréshez. Ez a képek feltöltéséhez kell.</string>
<string name="nearby_notification_dismiss_message">Nem lesznek láthatók a képet igénylő közeli helyek. Az értesítés visszakapcsolható a Beállításoknál.</string>
<string name="step_count" fuzzy="true">%1$d. lépés, összesen: %2$d</string>
<string name="step_count">%1$d. lépés, összesen: %2$d: %3$s</string>
<string name="next">Következő</string>
<string name="previous">Előző</string>
<string name="submit">Küldés</string>
@ -356,7 +356,7 @@
<string name="no_uploads">Üdvözlünk a Commons-ban. \n\nA hozzáadás gombra koppintva feltöltheted első képedet.</string>
<string name="no_categories_selected">Nincs kiválasztott kategória</string>
<string name="no_categories_selected_warning_desc">A kategória nélküli képek ritkán használhatóak. Biztos, hogy kategória kiválasztása nélkül akarsz továbblépni?</string>
<string name="upload_flow_all_images_in_set" fuzzy="true">(az összes képre)</string>
<string name="upload_flow_all_images_in_set">(az összes képre)</string>
<string name="search_this_area">Keresés ezen a helyen</string>
<string name="nearby_card_permission_title">Engedély kérése</string>
<string name="nearby_card_permission_explanation">A földrajzi hely ismerete szükséges a közeli hely meghatározásához, ami képet igényel.</string>

View file

@ -7,6 +7,7 @@
* Davio
* Gianfranco
* Lorelai87
* Lorem Ipsum
* Nemo bis
* S4b1nuz E.656
* Sarah Bernabei
@ -237,6 +238,7 @@
<string name="upload_problem_different_geolocation">Questa foto è stata scattata in una posizione diversa.</string>
<string name="upload_problem_fbmd">Carica solo le foto che hai scattato tu stesso. Non caricare immagini che hai trovato negli account Facebook di altre persone.</string>
<string name="upload_problem_do_you_continue">Vuoi caricare lo stesso questa immagine?</string>
<string name="upload_connection_error_alert_title">Errore di connessione</string>
<string name="upload_problem_image">Problemi trovati nell\'immagine</string>
<string name="internet_downloaded">Carica solo le foto che hai scattato da solo. Non caricare immagini scaricate da Internet.</string>
<string name="use_external_storage">Salva scatti in-app</string>
@ -448,6 +450,10 @@
<string name="category_edit_helper_show_edit_title">Aggiorna la categoria</string>
<string name="category_edit_helper_show_edit_title_success">Successo</string>
<string name="category_edit_button_text">Aggiorna le categorie</string>
<string name="coordinates_edit_helper_show_edit_title">Coordinate aggiornate</string>
<string name="coordinates_edit_helper_show_edit_title_success">Successo</string>
<string name="coordinates_edit_helper_show_edit_message">Le coordinate %1$s sono state aggiunte.</string>
<string name="coordinates_edit_helper_edit_message_else">Impossibile aggiungere le coordinate.</string>
<string name="share_image_via">Condividi immagine tramite</string>
<string name="no_achievements_yet">Non hai fatto alcun contributo ancora</string>
<string name="account_created">Utenza creata!</string>
@ -501,4 +507,8 @@
<string name="leaderboard_nearby">Nelle vicinanze</string>
<string name="leaderboard_used">Usa</string>
<string name="map_attribution">&amp;#169; &lt;a href=\"https://www.mapbox.com/about/maps/\"&gt;Mapbox&lt;/a&gt; &amp;#169; &lt;a href=\"https://www.openstreetmap.org/copyright\"&gt;OpenStreetMap&lt;/a&gt; &lt;a href=\"https://www.mapbox.com/map-feedback/\"&gt;Migliora questa mappa&lt;/a&gt;</string>
<string name="media_detail_in_all_languages">In tutte le lingue</string>
<string name="choose_a_location">Scegli una posizione</string>
<string name="select_location_location_picker">Seleziona una posizione</string>
<string name="modify_location">Modifica posizione</string>
</resources>

View file

@ -616,9 +616,15 @@
<string name="app_ui_language">שפת ממשק המשתמש של היישום</string>
<string name="remove">הסרת כיתוב ותיאור</string>
<string name="read_help_link">מידע נוסף</string>
<string name="media_detail_in_all_languages">בכל השפות</string>
<string name="choose_a_location">נא לבחור מיקום</string>
<string name="pan_and_zoom_to_adjust">לצדד וקרב כדי לכוונן</string>
<string name="exit_location_picker">יציאה מבורר המיקום</string>
<string name="select_location_location_picker">נא לבחור מיקום</string>
<string name="show_in_map_app">הצגה ביישומון המפות</string>
<string name="modify_location">עריכת מיקום</string>
<string name="location_picker_image_view">תצוגת תמונה של בורר המיקום</string>
<string name="location_picker_image_view_shadow">הצל של תצוגת תמונה של בורר המיקום</string>
<string name="image_location">מיקום תמונה</string>
<string name="check_whether_location_is_correct">לבדוק האם המיקום נכון</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* Apzp79
* CYAN
* Codenstory
* Dlsrks1021
@ -27,6 +28,7 @@
<item quantity="one">(%1$d)</item>
<item quantity="other">(%1$d)</item>
</plurals>
<string name="starting_uploads">파일 올리기</string>
<plurals name="starting_multiple_uploads">
<item quantity="one">%1$d장의 업로드를 처리하는 중입니다</item>
<item quantity="other">%1$d장의 업로드를 처리하는 중입니다</item>
@ -67,7 +69,7 @@
<string name="uploading_started">올리기를 시작했습니다!</string>
<string name="upload_completed_notification_title">%1$s 파일을 올렸습니다!</string>
<string name="upload_completed_notification_text">올린 것을 보려면 탭하세요</string>
<string name="upload_progress_notification_title_start" fuzzy="true">%1$s 파일 업로드를 시작하는 중</string>
<string name="upload_progress_notification_title_start">%1$s 파일 올리는 중</string>
<string name="upload_progress_notification_title_in_progress">%1$s 파일을 올리는 중</string>
<string name="upload_progress_notification_title_finishing">%1$s 파일 올리기를 끝내는 중</string>
<string name="upload_failed_notification_title">%1$s 파일 올리기 실패</string>
@ -227,7 +229,7 @@
<string name="nearby_info_menu_wikidata_article">위키데이터 항목</string>
<string name="nearby_info_menu_wikipedia_article">위키백과 문서</string>
<string name="description_info">미디어에 대해 가능한 많이 설명하십시오: 어디서 촬영한 것인가? 무엇을 보여주는가? 무슨 문맥을 가지는가? 물건이나 사람에 대해 설명하십시오. 풍경에서 시간을 알려주는 것처럼 쉽게 추측할 수 없는 정보를 제공합니다. 미디어가 평범하지 않다면 무엇이 이를 평범하지 않게 만들었는지 설명하십시오.</string>
<string name="caption_info" fuzzy="true">그림 설명을 작성해 주세요. (255자 제한)</string>
<string name="caption_info">그림의 간략한 설명을 작성해 주세요. 앞선 설명이 그림의 제목으로 됩니다. (최대 255자)</string>
<string name="upload_problem_exist">이 그림에 잠재적인 문제가 있습니다:</string>
<string name="upload_problem_image_dark">이미지가 너무 어둡습니다.</string>
<string name="upload_problem_image_blurry">이미지가 흐릿합니다.</string>
@ -235,10 +237,12 @@
<string name="upload_problem_different_geolocation">이 사진은 다른 위치에서 찍었습니다.</string>
<string name="upload_problem_fbmd">당신이 직접 찍은 사진만 업로드하세요. 다른 사람의 페이스북 계정에서 찾은 사진을 업로드하지마세요.</string>
<string name="upload_problem_do_you_continue">그래도 이 사진을 업로드하시겠습니까?</string>
<string name="upload_connection_error_alert_title">연결 오류</string>
<string name="upload_connection_error_alert_detail">파일을 올리기 위해서는 인터넷 연결이 필요합니다. 네트워크 연결을 확인해 주세요.</string>
<string name="upload_problem_image">이미지에 문제가 있습니다</string>
<string name="internet_downloaded">직접 찍은 사진만 업로드하세요. 인터넷에서 다운로드한 사진을 업로드하지 마세요.</string>
<string name="use_external_storage" fuzzy="true">외부 저장소 사용하기</string>
<string name="use_external_storage_summary" fuzzy="true">장치의 인앱 카메라로 찍은 사진 저장하기</string>
<string name="use_external_storage">인앱으로 찍은 사진 저장하기</string>
<string name="use_external_storage_summary">인앱 카메라로 찍은 사진을 기기에 저장하기</string>
<string name="login_to_your_account">자신의 계정으로 로그인</string>
<string name="send_log_file">로그 파일 보내기</string>
<string name="send_log_file_description">앱 문제의 디버깅을 지원하기 위해 이메일로 개발자에게 로그 파일을 보냅니다. 주의: 로그는 당신을 식별할 수 있는 정보를 잠재적으로 포함하고 있을 수 있습니다.</string>
@ -273,7 +277,7 @@
<string name="about_translate_proceed">진행</string>
<string name="about_translate_cancel">취소</string>
<string name="retry">다시 시도</string>
<string name="showcase_view_whole_nearby_activity" fuzzy="true">이들은 위키백과 글에 사진을 넣을 필요가 있는 당신 주위의 장소들입니다</string>
<string name="showcase_view_whole_nearby_activity">위키백과 글을 꾸며 줄 당신 주변의 사진 장소입니다.\n\n\'이 지역을 검색\'을 눌러 지도에서 장소 주변 검색을 시작하세요.</string>
<string name="showcase_view_needs_photo">이 장소에는 사진이 필요합니다.</string>
<string name="showcase_view_has_photo">이 장소의 사진이 이미 있습니다.</string>
<string name="showcase_view_no_longer_exists">이 장소는 더 이상 있지 않습니다.</string>
@ -351,7 +355,7 @@
<string name="notifications">알림</string>
<string name="read_notifications">알림 (읽음)</string>
<string name="display_nearby_notification">주변 알림 표시</string>
<string name="display_nearby_notification_summary" fuzzy="true">사진이 필요한 주변 장소를 보려면 여기를 탭하세요</string>
<string name="display_nearby_notification_summary">사진이 필요한 주변 장소의 인앱 알림 표시하기</string>
<string name="list_sheet">목록</string>
<string name="storage_permission">기억 장치 권한</string>
<string name="write_storage_permission_rationale_for_image_share">이미지를 업로드하려면 외부 기억 장치에 접근할 권한이 필요합니다.</string>
@ -360,7 +364,7 @@
<string name="next">다음</string>
<string name="previous">이전</string>
<string name="submit">제출</string>
<string name="upload_title_duplicate" fuzzy="true">%1$s 이름의 파일이 존재합니다. 계속하시겠습니까?</string>
<string name="upload_title_duplicate">%1$s의 파일 이름이 존재합니다. 계속하시겠습니까?\n\n주의: 자동적으로 파일 이름에 알맞는 접미사가 붙게 됩니다.</string>
<string name="map_application_missing">장치에서 호환되는 지도 애플리케이션을 찾지 못했습니다. 이 기능을 사용하려면 지도 애플리케이션을 설치해 주십시오.</string>
<string name="title_page_bookmarks_pictures">사진</string>
<string name="title_page_bookmarks_locations">위치</string>
@ -378,7 +382,7 @@
<string name="no_categories_selected">선택된 분류 없음</string>
<string name="no_categories_selected_warning_desc">분류가 없는 그림은 거의 유용하지 않습니다. 분류를 선택하지 않고 제출하시겠습니까?</string>
<string name="no_depictions_selected">선택된 서술 없음</string>
<string name="search_this_area">여기를 검색</string>
<string name="search_this_area">이 지역을 검색</string>
<string name="nearby_card_permission_title">권한 요청</string>
<string name="nearby_card_permission_explanation">사진이 필요한 주변 장소를 표시하기 위해 현재 위치를 사용하시겠습니까?</string>
<string name="unable_to_display_nearest_place">위치 권한 없이 사진이 필요한 주변 장소를 표시할 수 없습니다</string>
@ -401,12 +405,15 @@
<string name="nominate_for_deletion_done">완료</string>
<string name="send_thank_success_title">감사 표현 보내기: 성공</string>
<string name="send_thank_failure_title">감사 표현 보내기: 실패</string>
<string name="review_category">알맞게 분류됐습니까?</string>
<string name="review_thanks">기여자에게 감사를 표하시겠습니까?</string>
<string name="review_category_explanation">이 이미지는 %1$s 분류에 속해 있습니다.</string>
<string name="review_thanks_yes_button_text">다음 이미지</string>
<string name="review_thanks_no_button_text"></string>
<string name="skip_image_explanation">이 버튼을 클릭하면 위키미디어 공용으로부터 최근 업로드된 다른 이미지를 제공합니다</string>
<string name="review_image_explanation">이미지를 검토하고 위키미디어 공용의 품질을 개선할 수 있습니다.\n 4가지 검토 변수가 있습니다:\n - 이 이미지가 범위 내에 있는가? \n - 이 이미지가 저작권 규정을 준수하고 있는가? \n - 이 이미지가 올바르게 분류되어 있는가? \n - 모든 것이 문제 없다면 기여자에게 감사를 표할 수도 있습니다.</string>
<string name="no_image">이미지가 사용되지 않음</string>
<string name="no_image_uploaded">이미지가 올려지지 않음</string>
<string name="no_notification">읽지 않은 알림이 없습니다</string>
<string name="no_read_notification">읽은 알림이 없습니다</string>
<string name="menu_option_read">읽은 항목 보기</string>
@ -416,6 +423,7 @@
<string name="copy_image_caption_description">다음 미디어로 복사</string>
<string name="copied_successfully">복사했습니다</string>
<string name="welcome_do_upload_content_description">공용에 업로드할 좋은 이미지의 예</string>
<string name="welcome_dont_upload_content_description">올리지 말아야 할 이미지 예시</string>
<string name="skip_image">이 이미지 건너뛰기</string>
<string name="download_failed_we_cannot_download_the_file_without_storage_permission">다운로드를 실패했습니다!! 외장 스토리지 권한 없이 파일을 다운로드할 수 없습니다.</string>
<string name="manage_exif_tags">EXIF 태그 관리</string>
@ -432,7 +440,9 @@
<string name="no_depiction_found">서술이 발견되지 않았습니다</string>
<string name="upload_cancelled">업로드 취소됨</string>
<string name="default_description_language">기본 설명 언어</string>
<string name="delete_helper_show_deletion_title">삭제 신청</string>
<string name="delete_helper_show_deletion_title_success">성공</string>
<string name="delete_helper_show_deletion_message_if">%1$s을(를) 삭제 신청함</string>
<string name="delete_helper_show_deletion_title_failed">실패</string>
<string name="delete_helper_show_deletion_message_else">삭제를 요청하지 못했습니다.</string>
<string name="delete_helper_ask_spam_selfie">셀카</string>
@ -446,6 +456,9 @@
<string name="category_edit_helper_show_edit_title_success">성공</string>
<string name="category_edit_helper_edit_message_else">분류를 추가하지 못했습니다.</string>
<string name="category_edit_button_text">분류 업데이트</string>
<string name="coordinates_edit_helper_show_edit_title">좌표 업데이트</string>
<string name="coordinates_edit_helper_show_edit_title_success">성공</string>
<string name="coordinates_picking_unsuccessful">좌표를 가져올 수 없습니다.</string>
<string name="share_image_via">다음을 통해 이미지 공유</string>
<string name="no_achievements_yet">아직 기여가 없습니다</string>
<string name="account_created">계정을 만들었습니다!</string>
@ -469,12 +482,13 @@
<string name="wallpaper_set_unsuccessfully">무언가 잘못되었습니다. 배경화면을 설정하지 못했습니다</string>
<string name="setting_wallpaper_dialog_title">배경화면으로 설정</string>
<string name="setting_wallpaper_dialog_message">배경화면을 설정 중입니다. 기다려 주십시오...</string>
<string name="theme_dark_name">어두</string>
<string name="theme_light_name"></string>
<string name="theme_dark_name">어두</string>
<string name="theme_light_name"></string>
<string name="cannot_open_location_settings">위치 설정을 열지 못했습니다. 위치를 수동으로 켜주세요</string>
<string name="ask_to_turn_location_on">위치를 켭니까?</string>
<string name="load_more">더 불러오기</string>
<string name="nearby_no_results">발견된 장소가 없습니다. 검색 기준을 바꾸어 보십시오.</string>
<string name="todo_improve">제안된 개선안들:</string>
<string name="add_picture_to_wikipedia_article_title">위키백과에 이미지 추가</string>
<string name="add_picture_to_wikipedia_article_desc">이 그림을 %1$s 언어 위키백과 문서에 추가하시겠습니까?</string>
<string name="confirm">확인</string>
@ -507,4 +521,7 @@
<string name="app_ui_language">앱 사용자 인터페이스 언어</string>
<string name="remove">캡션과 설명을 제거합니다</string>
<string name="read_help_link">더 읽어보기</string>
<string name="media_detail_in_all_languages">모든 언어</string>
<string name="choose_a_location">장소 선택하기</string>
<string name="select_location_location_picker">장소 선택</string>
</resources>

View file

@ -2,6 +2,7 @@
<!-- Authors:
* Autokrator
* Laurentianus
* Lorem Ipsum
* UV
-->
<resources>
@ -62,4 +63,6 @@
<string name="detail_panel_cats_label">Categoriae</string>
<string name="detail_panel_cats_loading" fuzzy="true">Depromens…</string>
<string name="detail_panel_cats_none">Nulla selecta</string>
<string name="modify_location">Mutare locus</string>
<string name="image_location">Locus imaginis</string>
</resources>

View file

@ -301,6 +301,8 @@
<string name="category_edit_helper_show_edit_title_success">Succès</string>
<string name="category_edit_helper_edit_message_else">Kategorie konnten net derbäigesat ginn.</string>
<string name="category_edit_button_text">Kategorien aktualiséieren</string>
<string name="coordinates_edit_helper_show_edit_title">Aktualiséierung vun de Koordinaten</string>
<string name="coordinates_edit_helper_show_edit_title_success">Succès</string>
<string name="account_created">Benotzerkont ugeluecht!</string>
<string name="place_state_needs_photo">Brauch eng Foto</string>
<string name="upload_nearby_place_found_title">Plaz nobäi fonnt</string>
@ -349,4 +351,7 @@
<string name="limited_connection_mode">Limitéierte Verbindungsmodus</string>
<string name="app_ui_language">Sprooch vum Interface vum Benotzer vun der App</string>
<string name="read_help_link">Liest méi</string>
<string name="media_detail_in_all_languages">An alle Sproochen</string>
<string name="choose_a_location">Eng Plaz eraussichen</string>
<string name="select_location_location_picker">Plaz eraussichen</string>
</resources>

View file

@ -584,10 +584,15 @@
<string name="app_ui_language">Јазик на прилогот</string>
<string name="remove">Острани толкување и опис</string>
<string name="read_help_link">Прочитајте повеќе</string>
<string name="media_detail_in_all_languages">На сите јазици</string>
<string name="choose_a_location">Изберете местоположба</string>
<string name="pan_and_zoom_to_adjust">Доведете го местото на картата и приближете го за да прилагодите</string>
<string name="exit_location_picker">Излези од избирачот на местоположба</string>
<string name="select_location_location_picker">Изберете местоположба</string>
<string name="location_picker_image_view">Поглед на слика за избирање местоположба</string>
<string name="location_picker_image_view_shadow">Сенчест поглед на слика за избирање местоположба</string>
<string name="show_in_map_app">Прикажи во прилог за карти</string>
<string name="modify_location">Уреди местоположба</string>
<string name="location_picker_image_view">Сликовен поглед на избирачот на местоположба</string>
<string name="location_picker_image_view_shadow">Сенка на сликата за избирање местоположба</string>
<string name="image_location">Местоположба на сликата</string>
<string name="check_whether_location_is_correct">Проверете дали местоположбата е точна</string>
</resources>

View file

@ -582,10 +582,15 @@
<string name="app_ui_language">Lenga d\'antërfassa utent ëd l\'aplicassion</string>
<string name="remove">A gava na legenda e na descrission</string>
<string name="read_help_link">Lese ëd pi</string>
<string name="media_detail_in_all_languages">An tute le lenghe</string>
<string name="choose_a_location">Serne na locassion</string>
<string name="pan_and_zoom_to_adjust">Slarghé e strenze për buté bin</string>
<string name="exit_location_picker">Seurte dal selessionador ëd locassion</string>
<string name="select_location_location_picker">Selessioné na locassion</string>
<string name="location_picker_image_view">Vista dla plancia dël selessionador ëd locassion</string>
<string name="location_picker_image_view_shadow">Locassion ëd picker_image_view_shadow</string>
<string name="show_in_map_app">Smon-e andrinta a l\'aplicassion carta</string>
<string name="modify_location">Modifiché ël pòst</string>
<string name="location_picker_image_view">La vista dla plancia dël selessionador ëd locassion</string>
<string name="location_picker_image_view_shadow">L\'ombra dla vista dla plancia dël selessionator ëd pòst</string>
<string name="image_location">Pòst ëd la plancia</string>
<string name="check_whether_location_is_correct">Controlé si ël pòst a l\'é giust</string>
</resources>

View file

@ -495,6 +495,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Não foi possível adicionar categorias.</string>
<string name="category_edit_button_text">Atualizar categorias</string>
<string name="coordinates_edit_helper_make_edit_toast">Tentando atualizar as coordenadas.</string>
<string name="coordinates_edit_helper_show_edit_title">Atualização de coordenadas</string>
<string name="coordinates_edit_helper_show_edit_title_success">Sucesso</string>
<string name="coordinates_edit_helper_show_edit_message">Coordenadas %1$s adicionadas.</string>
<string name="coordinates_edit_helper_edit_message_else">Não foi possível adicionar coordenadas.</string>
<string name="coordinates_picking_unsuccessful">Incapaz de obter coordenadas.</string>
<string name="share_image_via">Compartilhar imagem via</string>
<string name="no_achievements_yet">Você ainda não fez nenhuma contribuição</string>
<string name="account_created">Conta criada!</string>
@ -592,4 +598,11 @@
<string name="app_ui_language">Idioma da interface do usuário do aplicativo</string>
<string name="remove">Remove uma legenda e descrição</string>
<string name="read_help_link">Leia mais</string>
<string name="media_detail_in_all_languages">Em todas os idiomas</string>
<string name="choose_a_location">Escolha uma localização</string>
<string name="select_location_location_picker">Selecionar localização</string>
<string name="show_in_map_app">Mostrar no aplicativo de mapa</string>
<string name="modify_location">Editar localização</string>
<string name="image_location">Localização da imagem</string>
<string name="check_whether_location_is_correct">Verifique se a localização está correta</string>
</resources>

View file

@ -170,4 +170,5 @@
<string name="more">{{Identical|More}}</string>
<string name="favorites">{{Identical|Favorite}}</string>
<string name="map_attribution">&lt;code&gt;&amp;amp;#169;&lt;/code&gt; is the copyright symbol (©).</string>
<string name="location_picker_image_view_shadow">A description of a visual element, location picker image shadow. Used for accesibility usually.</string>
</resources>

View file

@ -525,6 +525,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Не удалось добавить категории.</string>
<string name="category_edit_button_text">Обновить категории</string>
<string name="coordinates_edit_helper_make_edit_toast">Попытка обновить координаты.</string>
<string name="coordinates_edit_helper_show_edit_title">Обновление координат</string>
<string name="coordinates_edit_helper_show_edit_title_success">Успешно</string>
<string name="coordinates_edit_helper_show_edit_message">Координаты %1$s добавлены.</string>
<string name="coordinates_edit_helper_edit_message_else">Не удалось добавить координаты.</string>
<string name="coordinates_picking_unsuccessful">Не удалось получить координаты.</string>
<string name="share_image_via">Поделиться изображением с помощью</string>
<string name="no_achievements_yet">Пока что от вас нет вклада</string>
<string name="account_created">Учётная запись создана!</string>
@ -622,4 +628,11 @@
<string name="app_ui_language">Язык пользовательского интерфейса приложения</string>
<string name="remove">Удаляет подпись и описание</string>
<string name="read_help_link">Подробнее</string>
<string name="media_detail_in_all_languages">На всех языках</string>
<string name="choose_a_location">Выберите местоположение</string>
<string name="pan_and_zoom_to_adjust">Панорамируйте и масштабируйте для настройки</string>
<string name="exit_location_picker">Выйти из окна выбора местоположения</string>
<string name="select_location_location_picker">Выберите местоположение</string>
<string name="show_in_map_app">Показать в приложении карты</string>
<string name="check_whether_location_is_correct">Проверьте правильность местоположения</string>
</resources>

View file

@ -502,6 +502,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Nepodarilo sa pridať kategórie.</string>
<string name="category_edit_button_text">Aktualizovať kategórie</string>
<string name="coordinates_edit_helper_make_edit_toast">Pokus o aktualizáciu súradníc.</string>
<string name="coordinates_edit_helper_show_edit_title">Aktualizácia súradníc</string>
<string name="coordinates_edit_helper_show_edit_title_success">Úspech</string>
<string name="coordinates_edit_helper_show_edit_message">Súradnice %1$s sú pridané.</string>
<string name="coordinates_edit_helper_edit_message_else">Nepodarilo sa pridať súradnice.</string>
<string name="coordinates_picking_unsuccessful">Nepodarilo sa získať súradnice.</string>
<string name="share_image_via">Zdieľať obrázok cez</string>
<string name="no_achievements_yet">Nemáte zatiaľ žiadne príspevky</string>
<string name="account_created">Účet bol vytvorený!</string>
@ -599,4 +605,15 @@
<string name="app_ui_language">Jazyk používateľského rozhrania aplikácie</string>
<string name="remove">Odstráni titulky a popis</string>
<string name="read_help_link">Čítať viac</string>
<string name="media_detail_in_all_languages">Pre všetky jazyky</string>
<string name="choose_a_location">Vybrať polohu</string>
<string name="pan_and_zoom_to_adjust">Posúvať a zväčšovať pre nastavenie</string>
<string name="exit_location_picker">Odísť z výberu polohy</string>
<string name="select_location_location_picker">Vybrať lokalizáciu</string>
<string name="show_in_map_app">Zobraziť na mape v aplikácii</string>
<string name="modify_location">Upraviť polohu</string>
<string name="location_picker_image_view">Obrázok výberu polohy</string>
<string name="location_picker_image_view_shadow">Tieň obrázka pre výber polohy</string>
<string name="image_location">Poloha obrázka</string>
<string name="check_whether_location_is_correct">Skontrolovať či je poloha správna</string>
</resources>

View file

@ -486,6 +486,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Kunde inte lägga till kategorier.</string>
<string name="category_edit_button_text">Uppdatera kategorier</string>
<string name="coordinates_edit_helper_make_edit_toast">Försöker uppdatera koordinater.</string>
<string name="coordinates_edit_helper_show_edit_title">Koordinater uppdaterades</string>
<string name="coordinates_edit_helper_show_edit_title_success">Genomfördes</string>
<string name="coordinates_edit_helper_show_edit_message">Koordinaterna %1$s lades till.</string>
<string name="coordinates_edit_helper_edit_message_else">Kunde inte lägga till koordinater.</string>
<string name="coordinates_picking_unsuccessful">Kunde inte hämta koordinater.</string>
<string name="share_image_via">Dela bild via</string>
<string name="no_achievements_yet">Du har ännu inte gjort några bidrag</string>
<string name="account_created">Konto har skapats!</string>
@ -583,4 +589,15 @@
<string name="app_ui_language">Appens användargränssnittspråk</string>
<string name="remove">Ta bort en bildtext och beskrivning</string>
<string name="read_help_link">Läs mer</string>
<string name="media_detail_in_all_languages">På alla språk</string>
<string name="choose_a_location">Välj en plats</string>
<string name="pan_and_zoom_to_adjust">Panorera och zooma för att justera</string>
<string name="exit_location_picker">Avsluta platsväljaren</string>
<string name="select_location_location_picker">Välj plats</string>
<string name="show_in_map_app">Visa i kartappen</string>
<string name="modify_location">Redigera plats</string>
<string name="location_picker_image_view">Platsväljarens bildvisare</string>
<string name="location_picker_image_view_shadow">Skuggan för platsväljarens bildvisare</string>
<string name="image_location">Bildplats</string>
<string name="check_whether_location_is_correct">Kontrollera om platsen är korrekt</string>
</resources>

View file

@ -501,6 +501,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Kategoriler eklenemedi.</string>
<string name="category_edit_button_text">Kategorileri güncelle</string>
<string name="coordinates_edit_helper_make_edit_toast">Koordinatlar güncellenmeye çalışılıyor.</string>
<string name="coordinates_edit_helper_show_edit_title">Koordinat güncellemesi</string>
<string name="coordinates_edit_helper_show_edit_title_success">Başarılı</string>
<string name="coordinates_edit_helper_show_edit_message">%1$s koordinatları eklendi.</string>
<string name="coordinates_edit_helper_edit_message_else">Koordinatlar eklenemedi.</string>
<string name="coordinates_picking_unsuccessful">Koordinatlar alınamadı.</string>
<string name="share_image_via">Resmi şununla paylaş</string>
<string name="no_achievements_yet">Henüz bir katkı yapmadınız</string>
<string name="account_created">Hesap oluşturuldu!</string>
@ -598,4 +604,15 @@
<string name="app_ui_language">Uygulama kullanıcı arayüzü dili</string>
<string name="remove">Bir başlığı ve açıklamayı kaldırır</string>
<string name="read_help_link">Devamını oku</string>
<string name="media_detail_in_all_languages">Tüm dillerde</string>
<string name="choose_a_location">Bir konum seçin</string>
<string name="pan_and_zoom_to_adjust">Ayarlamak için kaydırma ve yakınlaştırma</string>
<string name="exit_location_picker">Konum seçiciden çık</string>
<string name="select_location_location_picker">Konum seçin</string>
<string name="show_in_map_app">Harita uygulamasında göster</string>
<string name="modify_location">Konumu düzenle</string>
<string name="location_picker_image_view">Konum seçicinin resim görünümü</string>
<string name="location_picker_image_view_shadow">Konum seçicinin resim görünümünün gölgesi</string>
<string name="image_location">Görüntü Konumu</string>
<string name="check_whether_location_is_correct">Konumun doğru olup olmadığını kontrol edin</string>
</resources>

View file

@ -4,6 +4,7 @@
* Alexander Yukal
* Andriykopanytsia
* Base
* DDPAT
* Movses
* Mykola Swarnyk
* Piramidion
@ -148,7 +149,7 @@
<string name="preference_license">Усталена ліцензія</string>
<string name="use_previous">Використати попередні назву й опис</string>
<string name="preference_theme">Тема</string>
<string name="license_name_cc_by_sa_four"> Attribution-ShareAlike 4.0</string>
<string name="license_name_cc_by_sa_four">Attribution-ShareAlike 4.0</string>
<string name="license_name_cc_by_four"> Attribution 4.0</string>
<string name="license_name_cc_by_sa"> Attribution-ShareAlike 3.0</string>
<string name="license_name_cc_by">CC Attribution 3.0</string>
@ -193,7 +194,7 @@
<string name="ok">Гаразд</string>
<string name="warning">Попередження</string>
<string name="duplicate_image_found">Виявлено дублікат зображення</string>
<string name="upload">Вивантажити</string>
<string name="upload">Завантажити</string>
<string name="yes">Так</string>
<string name="no">Ні</string>
<string name="media_detail_caption">Підпис</string>
@ -207,7 +208,7 @@
<string name="media_detail_coordinates">Координати</string>
<string name="media_detail_coordinates_empty">Не передбачено</string>
<string name="become_a_tester_title">Станьте бета-тестером</string>
<string name="become_a_tester_description">Підпишіться на наш бета-канал на Google Play і отримайте ранній доступ до нових функцій та виправлень багів</string>
<string name="become_a_tester_description">Підпишіться на наш бета-канал на Google Play і отримайте ранній доступ до нових функцій та виправлень баґів</string>
<string name="_2fa_code">Код 2FA</string>
<string name="logout_verification">Ви справді хочете вийти із системи?</string>
<string name="commons_logo">Логотип Вікісховища</string>
@ -234,7 +235,7 @@
<string name="navigation_item_settings">Налаштування</string>
<string name="navigation_item_feedback">Зворотний зв\'язок</string>
<string name="navigation_item_logout">Вийти</string>
<string name="navigation_item_info">Посібник</string>
<string name="navigation_item_info">Керівництво</string>
<string name="navigation_item_notification">Сповіщення</string>
<string name="navigation_item_review">Перевірка</string>
<string name="no_description_found">опис не знайдено</string>
@ -250,6 +251,8 @@
<string name="upload_problem_different_geolocation">Зображення виконано в іншому місці.</string>
<string name="upload_problem_fbmd">Будь ласка, завантажуйте тільки ті зображення, які були зроблені вами. Не завантажуйте зображень, які ви знайшли у Фейсбуці.</string>
<string name="upload_problem_do_you_continue">Ви все одно хочете завантажити це зображення?</string>
<string name="upload_connection_error_alert_title">Помилка з\'єднання</string>
<string name="upload_connection_error_alert_detail">Процес вивантаження вимагає активного інтернет-з\'єднання. Будь ласка, перевірте своє підключення до мережі.</string>
<string name="upload_problem_image">Виявлено проблеми із зображенням</string>
<string name="internet_downloaded">Будь ласка, завантажуйте тільки ті зображення, які були зроблені вами. Не завантажуйте зображень, які ви знайшли в інтернеті.</string>
<string name="use_external_storage">Зберігати зроблені в програмці знімки</string>
@ -267,8 +270,8 @@
<string name="skip_login_title">Ви дійсно бажаєте пропустити автентифікацію?</string>
<string name="skip_login_message">Вам треба буде увійти в систему для завантаження зображень у майбутньому.</string>
<string name="login_alert_message">Увійдіть, щоб використати цю функцію</string>
<string name="copy_wikicode">Скопіювати вікітекст у буфер обміну</string>
<string name="wikicode_copied">Вікітекст скопійовано у буфер обміну</string>
<string name="copy_wikicode">Скопіювати вікі-текст у буфер обміну</string>
<string name="wikicode_copied">Вікі-текст скопійовано у буфер обміну</string>
<string name="nearby_location_not_available">Функція «Поблизу» може працювати некоректно, «Розташування» недоступне.</string>
<string name="location_permission_rationale_nearby">Потрібний дозвіл для показу списку місць поблизу</string>
<string name="nearby_directions">Напрямки</string>
@ -319,10 +322,10 @@
<string name="quiz_question_string">Чи можна завантажувати це зображення?</string>
<string name="question">Запитання</string>
<string name="result">Результат</string>
<string name="quiz_back_button">Якщо ви продовжите завантажувати зображення, які підлягають вилученню, ваш обліковий запис можуть забанити. Ви впевнені, що хочете закінчити тест?</string>
<string name="quiz_alert_message">Понад %1$s завантажених вами зображень було вилучено. Якщо ви продовжите завантажувати зображення, які підлягають вилучення, ваш обліковий запис можуть забанити. \n\nХочете переглянути посібник ще раз і тоді пройти тест, що допоможе дізнатися, які зображення можна, а які не можна завантажувати?</string>
<string name="selfie_answer">Селфі не мають значної енциклопедичної цінності. Будь ласка, не завантажуйте фото самих себе, якщо про вас нема статті у Вікіпедії.</string>
<string name="taj_mahal_answer">Зображення пам\'яток і пейзажів можна завантажувати у більшості країн. Зважте, будь ласка, що сучасні мистецькі інсталяції на вулиці часто захищені авторськими правами і їх НЕ можна завантажувати.</string>
<string name="quiz_back_button">Якщо ви продовжите завантажувати зображення, які підлягають вилученню, ваш обліковий запис можуть заблокувати. Ви впевнені, що хочете закінчити тест?</string>
<string name="quiz_alert_message">Понад %1$s завантажених вами зображень було вилучено. Якщо ви продовжите завантажувати зображення, які підлягають вилучення, ваш обліковий запис можуть заблокувати. \n\nХочете переглянути посібник ще раз і тоді пройти тест, що допоможе дізнатися, які зображення можна, а які не можна завантажувати?</string>
<string name="selfie_answer">Селфі не мають значної енциклопедичної цінності. Будь ласка, не завантажуйте фото самих себе, якщо про вас немає статті у Вікіпедії.</string>
<string name="taj_mahal_answer">Зображення пам\'яток і пейзажів можна завантажувати у більшості країн. Зауважте, будь ласка, що сучасні мистецькі інсталяції на вулиці часто захищені авторськими правами і їх НЕ можна завантажувати.</string>
<string name="screenshot_answer">Знімки екрану веб-сайтів вважаються похідними роботами і є таким же об\'єктом авторських прав, як і сам веб-сайт. Їх можна використовувати з дозволу авторів сайту. Без такого дозволу, будь-який твір, який ви створите на основі їхньої роботи, юридично вважається неліцензованою копією, яка належить оригінальному автору.</string>
<string name="blurry_image_answer">Одна з цілей Вікісховища — зібрати якісні зображення. Тому розмиті зображення завантажувати не треба. Завжди старайтеся зробити гарні знімки при хорошому освітленні.</string>
<string name="construction_event_answer">Зображення технологій чи культури дуже бажані для Вікісховища.</string>
@ -333,7 +336,7 @@
<string name="continue_message">Продовжити</string>
<string name="correct">Правильна відповідь</string>
<string name="wrong">Хибна відповідь</string>
<string name="quiz_screenshot_question">Чи можна завантажувати цей скріншот?</string>
<string name="quiz_screenshot_question">Чи можна завантажувати цей знімок?</string>
<string name="share_app_title">Поширити програму</string>
<string name="error_fetching_nearby_places">Помилка отримання місць поблизу.</string>
<string name="no_recent_searches">Історія пошуку порожня</string>
@ -358,7 +361,7 @@
<string name="images_uploaded_explanation">Кількість зображень, які ви завантажили у Вікісховище будь-яким методом</string>
<string name="images_reverted_explanation">Відсоток завантажених вами у Вікісховище зображень, що не були вилучені</string>
<string name="images_used_explanation">Кількість завантажених вами у Вікісховище зображень, що використані у статтях Вікімедіа</string>
<string name="error_occurred">Відбулася помилка!</string>
<string name="error_occurred">Сталася помилка!</string>
<string name="notifications_channel_name_all">Сповіщення Вікісховища</string>
<string name="preference_author_name_toggle">Використати альтернативне ім\'я автора</string>
<string name="preference_author_name_toggle_summary">При завантаженні фото використовувати альтернативне ім\'я для зазначення авторства замість власного імені користувача</string>
@ -409,7 +412,7 @@
<string name="achievements_fetch_failed_ultimate_achievement">Ви зробили так багато, що наша система підрахунку досягнень не може впоратись зі своїм завданням. Це — абсолютне досягнення.</string>
<string name="ends_on">Завершується:</string>
<string name="display_campaigns">Показати кампанії</string>
<string name="display_campaigns_explanation">Чинні кампанії</string>
<string name="display_campaigns_explanation">Чинні компанії</string>
<string name="nearby_campaign_dismiss_message">Ви більше не бачитимете кампаній. Однак Ви можете увімкнути це сповіщення повторно в своїх налаштуваннях, якщо забажаєте.</string>
<string name="this_function_needs_network_connection">Ця функція вимагає доступу до інтернету. Будь ласка, перевірте своє з\'єднання.</string>
<string name="error_processing_image">Сталася помилка при обробці зображення. Будь ласка, спробуйте ще раз!</string>
@ -454,7 +457,7 @@
<string name="error_occurred_in_picking_images">Сталася помилка при завантаженні зображень</string>
<string name="please_wait">Будь ласка, зачекайте…</string>
<string name="images_featured_explanation">Вибрані зображення — це зображення від вправних фотографів та ілюстраторів, які спільнота Вікісховища визначила як такі, що мають найкращу якість на сайті.</string>
<string name="images_via_nearby_explanation">Зображення, завантажені через «Поблизу», — це зображення, завантажені через дослідження місць на карті.</string>
<string name="images_via_nearby_explanation">Зображення, завантажені через «Поблизу», — це зображення, завантажені через дослідження місць на мапі.</string>
<string name="thanks_received_explanation">Ця функція дозволяє редакторам надіслати «дякую» користувачам, які роблять корисні редагування, — скориставшись невеличким посиланням на сторінці історії або порівняння версій.</string>
<string name="copy_image_caption_description">Копіювати до наступних медіафайлів</string>
<string name="copied_successfully">Скопійовано</string>
@ -504,8 +507,14 @@
</plurals>
<string name="category_edit_helper_edit_message_else">Неможливо додати категорії</string>
<string name="category_edit_button_text">Оновити категорії</string>
<string name="coordinates_edit_helper_make_edit_toast">Спроба оновити координати.</string>
<string name="coordinates_edit_helper_show_edit_title">Оновлення координат</string>
<string name="coordinates_edit_helper_show_edit_title_success">Успіх</string>
<string name="coordinates_edit_helper_show_edit_message">Координати %1$s додані.</string>
<string name="coordinates_edit_helper_edit_message_else">Не вдалося додати координати.</string>
<string name="coordinates_picking_unsuccessful">Не вдалося отримати координати.</string>
<string name="share_image_via">Поширити зображення через</string>
<string name="no_achievements_yet">Поки що від вас немає вкладу</string>
<string name="no_achievements_yet">Ви ще не зробили жодного внеску</string>
<string name="account_created">Обліковий запис створено!</string>
<string name="text_copy">Текст скопійовано до буферу обміну</string>
<string name="notification_mark_read">Сповіщення позначено прочитаним</string>
@ -545,12 +554,12 @@
<string name="add_picture_to_wikipedia_article_desc">Ви хочете додати це зображення у статтю Вікіпедії мовою %1$s?</string>
<string name="confirm">Підтвердити</string>
<string name="instructions_title">Інструкції</string>
<string name="wikipedia_instructions_step_1">1. Використовуйте такий вікітекст:</string>
<string name="wikipedia_instructions_step_1">1. Використовуйте такий вікі-текст:</string>
<string name="wikipedia_instructions_step_2">2. Натискання на «Підтвердити» відкриє статтю Вікіпедії</string>
<string name="wikipedia_instructions_step_3">3. Знайдіть розділ статті, до якого пасуватиме ваше зображення</string>
<string name="wikipedia_instructions_step_4">4. Натисніть на іконку «Редагувати» (у вигляді олівця) біля цього розділу.</string>
<string name="wikipedia_instructions_step_5">5. Вставте вікітекст у підхожому місці.</string>
<string name="wikipedia_instructions_step_6">6. Відредагуйте вікітекст за потреби, вказавши потрібне розміщення. Детальнішу інформацію знайдете &lt;a href=\"https://uk.wikipedia.org/wiki/Довідка:Розширений_синтаксисображень\"&gt;тут&lt;/a&gt;.</string>
<string name="wikipedia_instructions_step_5">5. Вставте вікі-текст у відповідне місце.</string>
<string name="wikipedia_instructions_step_6">6. Відредагуйте вікі-текст за потреби, вказавши потрібне розміщення. Детальну інформацію знайдете &lt;a href=\"https://uk.wikipedia.org/wiki/Довідка:Розширений_синтаксисображень\"&gt;тут&lt;/a&gt;.</string>
<string name="wikipedia_instructions_step_7">7. Опублікуйте статтю</string>
<string name="copy_wikicode_to_clipboard">Скопіювати вікікод у буфер обміну</string>
<string name="pause">пауза</string>
@ -579,16 +588,16 @@
<string name="leaderboard_my_rank_button_text">Мій ранг</string>
<string name="mapbox_telemetry">Телеметрія Mapbox</string>
<string name="telemetry_opt_out_summary">Надсилати анонімізоване розташування та дані щодо користування до Mapbox при використанні функції Поблизу</string>
<string name="map_attribution">&amp;#169; &lt;a href=\"https://www.mapbox.com/about/maps/\"&gt;Mapbox&lt;/a&gt; &amp;#169; &lt;a href=\"https://www.openstreetmap.org/copyright\"&gt;OpenStreetMap&lt;/a&gt; &lt;a href=\"https://www.mapbox.com/map-feedback/\"&gt;Покращити цю карту&lt;/a&gt;</string>
<string name="map_attribution">&amp;#169; &lt;a href=\"https://www.mapbox.com/about/maps/\"&gt;Mapbox&lt;/a&gt; &amp;#169; &lt;a href=\"https://www.openstreetmap.org/copyright\"&gt;OpenStreetMap&lt;/a&gt; &lt;a href=\"https://www.mapbox.com/map-feedback/\"&gt;Покращити цю мапу&lt;/a&gt;</string>
<string name="limited_connection_enabled">Увімкнено режим обмеженого з\'єднання!</string>
<string name="limited_connection_disabled">Вимкнено режим обмеженого з\'єднання. Завантаження в процесі очікування тепер відновляться.</string>
<string name="limited_connection_mode">Режим обмеженого з\'єднання</string>
<string name="statistics_quality">Якісні зображення</string>
<string name="quality_images_info">Якісні зображення — це діаграми чи фотографії, що відповідають певним стандартам якості (переважно технічним за природою) та цінні для проектів Вікімедіа</string>
<string name="quality_images_info">Якісні зображення — це діаграми чи фотографії, що відповідають певним стандартам якості (переважно технічним за природою) та цінні для проєктів Вікімедіа</string>
<string name="resuming_upload">Продовження завантаження…</string>
<string name="pausing_upload">Призупинення завантаження…</string>
<string name="limited_connection_explanation">Ви ввімкнули режим обмеженого з\'єднання. Усі завантаження призупинено та буде відновлено коли Ви вимкнете цей режим.</string>
<string name="limited_connection_is_on">Режим обмеженого з\'єднання ввімкнено.</string>
<string name="limited_connection_explanation">Ви увімкнули режим обмеженого з\'єднання. Усі завантаження призупинено та буде відновлено коли Ви вимкнете цей режим.</string>
<string name="limited_connection_is_on">Режим обмеженого з\'єднання увімкнено.</string>
<string name="media_details_tooltip">Будь ласка, напишіть короткий опис, що розповідає що представлено на зображенні. В описі розкажіть що робить це зображення цікавим, типовим або рідкісним, опишіть контекст — видимий чи ні. Старайтесь максимально використовувати точну термінологію.</string>
<string name="depicts_tooltip">Будь ласка, знайдіть та оберіть всі концепти, що це зображення показує. Будьте настільки точними, наскільки можливо. Якщо зображення зображує декілька речей, у межах здорового глузду оберіть їх всі. Не обирайте загальніші теґи, коли є доступні точніші.</string>
<string name="categories_tooltip">Будь ласка, оберіть відповідні категорії. На відміну від описів, назви категорій лише англійською.</string>
@ -601,4 +610,15 @@
<string name="app_ui_language">Мова інтерфейсу</string>
<string name="remove">Вилучає опис і підпис</string>
<string name="read_help_link">Читати більше</string>
<string name="media_detail_in_all_languages">Усіма мовами</string>
<string name="choose_a_location">Вибрати розташування</string>
<string name="pan_and_zoom_to_adjust">Панорамуйте і збільшуйте, щоб підлаштувати</string>
<string name="exit_location_picker">Вийти з вікна вибору розташування</string>
<string name="select_location_location_picker">Вибрати розташування</string>
<string name="show_in_map_app">Показати в додатку на мапі</string>
<string name="modify_location">Редагувати підпис</string>
<string name="location_picker_image_view">Вигляд вибору розташування як зображення</string>
<string name="location_picker_image_view_shadow">Вигляд вибору розташування як тіні зображення</string>
<string name="image_location">Підпис зображення</string>
<string name="check_whether_location_is_correct">Перевірте правильність розташування</string>
</resources>

View file

@ -32,7 +32,7 @@
<string name="preference_category_appearance">მიოჯინი</string>
<string name="preference_category_general">თარი</string>
<string name="preference_category_feedback">წიმოხრსხუ</string>
<string name="preference_category_privacy">კონფიდენციალურობ</string>
<string name="preference_category_privacy">კონფიდენციალურალ</string>
<string name="app_name">ვიკიოწკარუე</string>
<string name="menu_settings">პარამეტრეფი</string>
<string name="intent_share_upload_label">ვიკიოწკარუეშა ეხარგუა</string>
@ -69,6 +69,7 @@
<string name="menu_share">გობჟინაფა</string>
<string name="menu_view_file_page">ფაილიშ ხასჷლაშ ძირაფა</string>
<string name="share_title_hint">მუკნაჭარა (უციო)</string>
<string name="add_caption_toast">ქორთხინთ, ქემიოწურეთ თე ფაილიშ ეჭარუა</string>
<string name="share_description_hint">ეჭარუა</string>
<string name="share_caption_hint">მუკნაჭარა</string>
<string name="login_failed_network">მიშულაქ ვემიხუჯინუ - რშვილიშ ჩილათა</string>
@ -98,7 +99,7 @@
<string name="about_improve">&lt;a href=\"https://github.com/commons-app/apps-android-commons\"&gt;წყუ&lt;/a&gt; და &lt;a href=\"https://commons-app.gოthub.io/\"&gt;ვებ-ხასჷლა&lt;/a&gt; GitHub-ის. ჩილათაშ ოგინაფალო ვარ-და ზიტყვასქვილშო გაჭყით ახალი &lt;a href=\"%1$s\"&gt;მოთხირი GitHub-ის&lt;/a&gt;.</string>
<string name="about_privacy_policy">კონფიდენციალურალაშ პოლიტიკა</string>
<string name="about_credits" fuzzy="true">&lt;a href=\"https://github.com/commons-app/apps-android-commons/blob/master/CREDITS\"&gt;მარდეფი&lt;/a&gt;</string>
<string name="title_activity_about">პრგრამაშ გეშა</string>
<string name="title_activity_about">პრგრამაშ გეშა</string>
<string name="menu_feedback">წჷმიხონარეფიშ ჯღონუა (ელ.ფოსტათ)</string>
<string name="no_email_client">ელ-ფოსტაშ კლიენტი ვა რე გერინაფილი</string>
<string name="provider_categories">ასერდე გჷმორინაფილი კატეგორიეფი</string>

View file

@ -239,6 +239,8 @@
<string name="upload_problem_different_geolocation">此圖片是在不同的地點拍攝。</string>
<string name="upload_problem_fbmd">請僅上傳您自己拍攝的圖片。不要上傳您從別人臉書帳號裡所找到的圖片。</string>
<string name="upload_problem_do_you_continue">您仍要上傳此圖片嗎?</string>
<string name="upload_connection_error_alert_title">連接錯誤</string>
<string name="upload_connection_error_alert_detail">更新處理需要有效的網際網路存取。請檢查您的網路連線。</string>
<string name="upload_problem_image">在圖片裡發現問題</string>
<string name="internet_downloaded">請僅上傳您自己拍攝的圖片。不要上傳您從網路下載來的圖片。</string>
<string name="use_external_storage">儲存應用程式所提供的快照</string>
@ -491,6 +493,12 @@
</plurals>
<string name="category_edit_helper_edit_message_else">無法添加分類。</string>
<string name="category_edit_button_text">更新分類</string>
<string name="coordinates_edit_helper_make_edit_toast">嘗試更新座標。</string>
<string name="coordinates_edit_helper_show_edit_title">座標更新</string>
<string name="coordinates_edit_helper_show_edit_title_success">成功</string>
<string name="coordinates_edit_helper_show_edit_message">座標 %1$s 已添加。</string>
<string name="coordinates_edit_helper_edit_message_else">無法添加座標。</string>
<string name="coordinates_picking_unsuccessful">無法取得座標。</string>
<string name="share_image_via">分享圖片透過</string>
<string name="no_achievements_yet">您還沒有做出任何貢獻</string>
<string name="account_created">已建立帳號!</string>
@ -588,4 +596,15 @@
<string name="app_ui_language">應用程式使用者介面語言</string>
<string name="remove">移除說明和描述</string>
<string name="read_help_link">延伸閱讀</string>
<string name="media_detail_in_all_languages">在所有語言</string>
<string name="choose_a_location">挑選一個位置</string>
<string name="pan_and_zoom_to_adjust">平移和縮放來進行調整</string>
<string name="exit_location_picker">離開位置點選器</string>
<string name="select_location_location_picker">選擇位置</string>
<string name="show_in_map_app">顯示在地圖應用程式</string>
<string name="modify_location">編輯位置</string>
<string name="location_picker_image_view">位置點選器的圖片檢視</string>
<string name="location_picker_image_view_shadow">位置點選器的圖片檢視陰影</string>
<string name="image_location">圖片位置</string>
<string name="check_whether_location_is_correct">檢查位置是否正確</string>
</resources>

View file

@ -8,6 +8,7 @@
* Angrydog001
* D41D8CD98F
* Deathkon
* GuoPC
* Hydra
* Ken418
* Kuailong
@ -44,13 +45,13 @@
<item quantity="other">%1$d次上传</item>
</plurals>
<string name="starting_uploads">开始上传</string>
<plurals name="starting_multiple_uploads" fuzzy="true">
<item quantity="one">开始%1$d次上传</item>
<item quantity="other">开始%1$d次上传</item>
<plurals name="starting_multiple_uploads">
<item quantity="one">正在处理%d个上传</item>
<item quantity="other">正在处理%d个上传</item>
</plurals>
<plurals name="multiple_uploads_title" fuzzy="true">
<item quantity="one">%1$d次上传</item>
<item quantity="other">%1$d次上传</item>
<plurals name="multiple_uploads_title">
<item quantity="one">%d个上传</item>
<item quantity="other">%d个上传</item>
</plurals>
<plurals name="share_license_summary">
<item quantity="one">该图像的授权协议是 %1$s</item>
@ -120,6 +121,7 @@
<string name="provider_modifications">修改</string>
<string name="menu_upload_single">上传</string>
<string name="categories_search_text_hint">搜索分类</string>
<string name="depicts_search_text_hint">搜索您的媒体描述的项目(如山、泰姬陵等)</string>
<string name="menu_save_categories">保存</string>
<string name="refresh_button">刷新</string>
<string name="display_list_button">列表</string>
@ -203,6 +205,7 @@
<string name="no"></string>
<string name="media_detail_caption">说明</string>
<string name="media_detail_title">标题</string>
<string name="media_detail_depiction">描述</string>
<string name="media_detail_description">描述</string>
<string name="media_detail_discussion">讨论</string>
<string name="media_detail_author">作者</string>
@ -246,6 +249,7 @@
<string name="nearby_info_menu_wikidata_article">维基数据项</string>
<string name="nearby_info_menu_wikipedia_article">维基百科条目</string>
<string name="description_info">请尽可能详细地描述媒体:拍摄在何地?显示什么?例文是什么?请描述对象或个人。透露一些不易猜想到的信息,例如这幅风景画的具体日期时间。如果媒体显示了一些不寻常的事物,请说明为什么它显得不寻常。</string>
<string name="caption_info">请编写图像的简要说明。第一个标题将作为图片的题目。不能超过255个字符。</string>
<string name="upload_problem_exist">这个图片可能的问题:</string>
<string name="upload_problem_image_dark">图片太暗。</string>
<string name="upload_problem_image_blurry">图片模糊。</string>
@ -253,9 +257,11 @@
<string name="upload_problem_different_geolocation">这张图片是在不同的地点拍摄的。</string>
<string name="upload_problem_fbmd">请仅上传由您自己拍摄的图像。请勿上传您在他人的Facebook账户上所发现的图像。</string>
<string name="upload_problem_do_you_continue">仍然上传这张图片?</string>
<string name="upload_connection_error_alert_title">连接错误</string>
<string name="upload_connection_error_alert_detail">上传过程需要有效的互联网访问。请检查您的网络连接。</string>
<string name="upload_problem_image">在图片中发现的问题</string>
<string name="internet_downloaded">请仅上传由您自己拍摄的图像。请勿上传您从互联网下载的图像。</string>
<string name="use_external_storage" fuzzy="true">使用外部存储</string>
<string name="use_external_storage">保存应用程序内截图</string>
<string name="use_external_storage_summary">将您设备内部照相机应用拍摄的照片保存至您的设备存储中</string>
<string name="login_to_your_account">登录您的账户</string>
<string name="send_log_file">发送日志文件</string>
@ -272,7 +278,7 @@
<string name="login_alert_message">请登录后使用这个功能</string>
<string name="copy_wikicode">复制维基文本到剪贴板</string>
<string name="wikicode_copied">维基文本已经复制到剪贴板</string>
<string name="nearby_location_not_available" fuzzy="true">位置不可用。</string>
<string name="nearby_location_not_available">附近可能无法正常工作,位置不可用。</string>
<string name="location_permission_rationale_nearby">需要权限以显示附近地点列表</string>
<string name="nearby_directions">方向</string>
<string name="nearby_wikidata">维基数据</string>
@ -308,6 +314,7 @@
<string name="search_recent_header">最近搜索:</string>
<string name="provider_searches">最近的搜索查询</string>
<string name="error_loading_categories">载入分类时发生错误。</string>
<string name="error_loading_depictions">加载描述时出错。</string>
<string name="search_tab_title_media">媒体</string>
<string name="search_tab_title_categories">分类</string>
<string name="search_tab_title_depictions">项目</string>
@ -370,7 +377,7 @@
<string name="notifications">通知</string>
<string name="read_notifications">通知(已读)</string>
<string name="display_nearby_notification">显示附近通知</string>
<string name="display_nearby_notification_summary" fuzzy="true">点击此处查看最近需要图片的地方</string>
<string name="display_nearby_notification_summary">显示有关需要图片的最近地点的应用程序内通知</string>
<string name="list_sheet">列表</string>
<string name="storage_permission">存储权限</string>
<string name="write_storage_permission_rationale_for_image_share">我们需要您的许可才能访问设备的外部存储空间以上传图片。</string>
@ -398,6 +405,7 @@
<string name="no_categories_selected">未提交分类</string>
<string name="no_categories_selected_warning_desc">没有类别的图像很少可用。确实要继续而不选择类别吗?</string>
<string name="no_depictions_selected">没有选择描写</string>
<string name="no_depictions_selected_warning_desc">带有描述的图像更容易被发现并且更可能被使用。您确定不选择描述继续吗?</string>
<string name="upload_flow_all_images_in_set">(对于设置中的所有图像)</string>
<string name="search_this_area">搜索这个区域</string>
<string name="nearby_card_permission_title">需要许可</string>
@ -457,6 +465,8 @@
<string name="images_featured_explanation">特征图片是Wikimedia Commons社区选出的网站上的最高质量的图片中的一部分是来自于技术高超的摄影师和绘画师。</string>
<string name="images_via_nearby_explanation">通过附近位置上传的图片是指那些使用地图上发现位置功能上传的图片</string>
<string name="thanks_received_explanation">这些功能允许编辑人员给那些做出了有用编辑的用户发送感谢通知-感谢通知通过使用在历史页面或差分页面上的一个小的感谢链接实现的。</string>
<string name="copy_image_caption_description">复制到后续媒体</string>
<string name="copied_successfully">已复制</string>
<string name="welcome_do_upload_content_description">上传好图片到Commons的例子</string>
<string name="welcome_dont_upload_content_description">不能上传图片的例子</string>
<string name="skip_image">跳过该图片</string>
@ -493,7 +503,20 @@
<string name="delete_helper_ask_reason_copyright_logo">标志</string>
<string name="delete_helper_ask_alert_set_positive_button_reason">由于他是</string>
<string name="category_edit_helper_make_edit_toast">正在尝试更新分类。</string>
<string name="category_edit_helper_show_edit_title">分类更新</string>
<string name="category_edit_helper_show_edit_title_success">成功</string>
<plurals name="category_edit_helper_show_edit_message_if">
<item quantity="one">分类%1$s已添加。</item>
<item quantity="other">分类%1$s已添加。</item>
</plurals>
<string name="category_edit_helper_edit_message_else">无法添加分类。</string>
<string name="category_edit_button_text">更新分类</string>
<string name="coordinates_edit_helper_make_edit_toast">尝试更新坐标。</string>
<string name="coordinates_edit_helper_show_edit_title">坐标更新</string>
<string name="coordinates_edit_helper_show_edit_title_success">完成</string>
<string name="coordinates_edit_helper_show_edit_message">坐标 %1$s 已被添加。</string>
<string name="coordinates_edit_helper_edit_message_else">无法添加坐标。</string>
<string name="coordinates_picking_unsuccessful">无法获取坐标。</string>
<string name="share_image_via">分享图片透过</string>
<string name="no_achievements_yet">您目前还没有作出任何贡献</string>
<string name="account_created">账户已创建!</string>
@ -521,31 +544,81 @@
<string name="theme_default_name">跟随系统</string>
<string name="theme_dark_name">暗黑</string>
<string name="theme_light_name">明亮</string>
<string name="cannot_open_location_settings">无法打开位置设置。请手动打开位置</string>
<string name="recommend_high_accuracy_mode">选择高精确度模式以获得最佳结果。</string>
<string name="ask_to_turn_location_on">打开位置?</string>
<string name="nearby_needs_location">附近需要启用位置才能正常工作</string>
<string name="use_location_from_similar_image">您是在同一地点拍摄这两张图片的吗?您想要使用右侧图片的纬度/经度吗?</string>
<string name="load_more">加载更多</string>
<string name="nearby_no_results">未找到位置,请尝试更改您的搜索条件。</string>
<string name="todo_improve">建议的改进措施:</string>
<string name="missing_category">- 为此图像添加分类以提升可用性。</string>
<string name="missing_article">- 将此图像添加至相关的无图像的维基百科条目中。</string>
<string name="add_picture_to_wikipedia_article_title">将图片添加到维基百科</string>
<string name="add_picture_to_wikipedia_article_desc">您想将此图像添加至$1s维基百科条目中吗</string>
<string name="confirm">确认</string>
<string name="instructions_title">说明</string>
<string name="wikipedia_instructions_step_1">1. 使用下列维基文本:</string>
<string name="wikipedia_instructions_step_2">2. 点击确认将会打开维基百科条目</string>
<string name="wikipedia_instructions_step_3">3. 寻找条目中适合您的图像的章节</string>
<string name="wikipedia_instructions_step_4">4. 点击该章节的编辑图标(类似铅笔形状的图标)。</string>
<string name="wikipedia_instructions_step_5">5. 将维基文本粘贴到合适的位置。</string>
<string name="wikipedia_instructions_step_6">6. 如果必要,在合适的位置编辑维基文本。参阅&lt;a href=\"https://en.wikipedia.org/wiki/Wikipedia:Manual_of_Style/Images#How_to_place_an_image\"&gt;此处&lt;/a&gt;以获得更多信息。</string>
<string name="wikipedia_instructions_step_7">7. 发布条目</string>
<string name="copy_wikicode_to_clipboard">复制维基代码到剪贴板</string>
<string name="pause">暂停</string>
<string name="resume">继续</string>
<string name="paused">已暂停</string>
<string name="more">更多</string>
<string name="favorites">我的收藏</string>
<string name="achievements_tab_title">成就</string>
<string name="leaderboard_tab_title">排行榜</string>
<string name="rank_prefix">排名:</string>
<string name="count_prefix">数量:</string>
<string name="leaderboard_column_rank">排名</string>
<string name="leaderboard_column_user">用户</string>
<string name="leaderboard_column_count">计数</string>
<string name="setting_avatar_dialog_title">设为排行榜头像</string>
<string name="setting_avatar_dialog_message">正在设为头像,请稍候</string>
<string name="avatar_set_successfully">头像已设定</string>
<string name="avatar_set_unsuccessfully">设定新头像出错,请重试</string>
<string name="menu_set_avatar">设为头像</string>
<string name="leaderboard_yearly">每年</string>
<string name="leaderboard_weekly">每周</string>
<string name="leaderboard_all_time">所有时间</string>
<string name="leaderboard_upload">上传</string>
<string name="leaderboard_nearby">附近</string>
<string name="leaderboard_used">已使用</string>
<string name="leaderboard_my_rank_button_text">我的排名</string>
<string name="mapbox_telemetry">Mapbox Telemetry</string>
<string name="telemetry_opt_out_summary">使用附近功能时向Mapbox发送匿名位置和使用数据</string>
<string name="map_attribution">&amp;#169; &lt;a href=\"https://www.mapbox.com/about/maps/\"&gt;Mapbox&lt;/a&gt; &amp;#169; &lt;a href=\"https://www.openstreetmap.org/copyright\"&gt;OpenStreetMap&lt;/a&gt; &lt;a href=\"https://www.mapbox.com/map-feedback/\"&gt;改进此地图&lt;/a&gt;</string>
<string name="limited_connection_enabled">限制连接模式已启用!</string>
<string name="limited_connection_disabled">限制连接模式已禁用。挂起的上传将会立即恢复。</string>
<string name="limited_connection_mode">限制连接模式</string>
<string name="statistics_quality">品质图像</string>
<string name="quality_images_info">品质图像是符合一定质量标准(本质上大多是技术性的)的图表或照片,它们对维基媒体计划很有价值</string>
<string name="resuming_upload">正在恢复上传...</string>
<string name="pausing_upload">暂停上传...</string>
<string name="limited_connection_explanation">您已启用限制连接模式。所有的上传已暂停并将在您禁用此模式后立刻恢复。</string>
<string name="limited_connection_is_on">限制连接模式启用中。</string>
<string name="media_details_tooltip">请编写一个简短的标题来介绍您的图片展示了什么。请介绍该图片是如何有趣、典型或罕见,并说明背景以及是否可见。请尽可能使用准确的术语。</string>
<string name="depicts_tooltip">请寻找和选择此图像描绘的所有概念。越精确越好。如果图像描绘了多个项目,请在合理范围内全部选择。如果有更精确的标签可用,就不要选择通用标签。</string>
<string name="categories_tooltip">请选择合适的分类。与描述不同,分类仅使用英文。</string>
<string name="license_tooltip">维基共享资源让您的图片能够重复使用并且供任何人修改。您想放弃所有使用权吗?您希望被标示出吗?您想依据相同的授权条款来修改内容吗?</string>
<string name="depicts_step_title">描述</string>
<string name="license_step_title">媒体授权条款</string>
<string name="media_detail_step_title">媒体详情</string>
<string name="menu_view_category_page">查看分类页面</string>
<string name="menu_view_item_page">查看项目页面</string>
<string name="app_ui_language">应用程序用户界面语言</string>
<string name="remove">移除标题和描述</string>
<string name="read_help_link">阅读更多</string>
<string name="media_detail_in_all_languages">在所有语言中</string>
<string name="choose_a_location">选择一个地点</string>
<string name="pan_and_zoom_to_adjust">放大和做小来调整</string>
<string name="exit_location_picker">退出位置拾取器</string>
<string name="select_location_location_picker">选择地点</string>
<string name="location_picker_image_view">位置拾取器的图像视图</string>
<string name="location_picker_image_view_shadow">地点拾取器的照片观测视野的阴影。</string>
</resources>

View file

@ -50,6 +50,7 @@
<attr name="search_icon" format="reference"/>
<attr name="custom_selector_done" format="reference"/>
<attr name="custom_selector_back" format="reference"/>
<attr name="caption_description_text_color" format="reference" />
<declare-styleable name="Badge">
<attr name="boundary" format="color"/>

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