mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 12:23:58 +01:00
* [WIP] Fixes #2942. Set 'depicts' automatically for images uploaded via 'Nearby' * Feature/refractor uploads [WIP] (#2887) * Fix duplicate param information (#2515) * Bug fix issue #2476 (#2526) * Added wikidataEntityID in all db versions, handled db.execSql via method runQuery * Versioning and changelog for v2.10.2 (#2531) * Update changelog.md * Versioning for v2.10.2 * Update changelog.md * Bugfix/issue 2580 (#2584) * Corrected string placedholders in certain string files * Corrected string placedholders in certain string files[Bug fix #2580] * Bug Fix #2585 (#2647) * Bug Fix #2585 * Added null checks on view in SearchImageFragment when updating views from external sources * Disposed the disposables in SearchActivity and SearchImageFragment when no longer in active lifecycle * use FragmentUtils to verify fragment active state * Bug Fix issue #2648 (#2678) * Bug Fix issue #2648 * Handled external storage permission before file download * * Removed redudant check for permission in MediaDetailPagerFragment (Dexter already does that) * Removed duplicate code in PermissionUtil$checkPermissionsAndPerformAction, used the existing function with conditional extra parameters * string name typo correction * BugFix issue #2652 (#2706) * Addded null check on bookmark before operating on it * BugFix issue #2711 (#2712) * Added null checks in OkHttpJsonApiClient$searchImages MwQueryResponse * BugFix #2718 (#2719) * Handled null auth cookies * Fix #2791: NPE when nominating for deletion and leaving screen (#2792) * Bug Fix issue #2789 (#2790) * Handled Illegal State Exception for non existent appropriate view parents in ViewUtils$showShortSnackbar * BugFix #2720 (#2831) BugFix deprecated licenes #2720 * ui fixes, wip, upload * *Issue #2886, BugFix #2832[wip] * updated UploadActivity code * modified ui * Updated UploadPresenterTest * * updated interfaces names to follow names suffixed with Contract * added test cases * card view elevation * view pager disabled swipe * bug fix, duplicate image * used existing non-swipable view pager * Avoid image view resize with keyboard, added adjustPan and stateVisible as softinputMode for UploadActivity * retain UploadBaseFragment instances on orientation changes * * Added test cases for UploadMediaPresenter * Injected io and main thread schedulers * categories presenter test cased wip * Added CategoriesPresenter test * * Added the logic to show open map (with to be uploaded image's coordinates while uploading image) * codacy suggested changes * added java docs * Added travis_wait fot android-wait-for-emulator * ranamed interface onResponseCallback to Callback * * Added api to delete picture in UploadModel * cleanUp in UploadModel. once upload has been initiated * Removed unused methods from UploadModel and the corresponding test class * * Added tests for UploadPresenter * Travis suggested changes * Addded copy previous title and description * * Made the upload add descriptions visible when keyboard visible * add description request focus only when user manually requests it * Added JavaDocs, review suggested changes * Fix dagger injection * use DialogUtil to show info in descriptions * use activity context for DialogUtil * Minor changes * refactored title * ui for depicts * bug fix * basic architecture for depicts * adde architecture components for depicts * [WIP] ApacheHttpClientMediaWikiApi.wikidataEditEntity: JSON param creation uses object instead of string * resolved dagger errors * multilingual captions and next button error resolved * fixed next button issues in depicts fragment * captions and depicts * resolved previous button click issues * fixed bindview error and added multi-captions * replaced description and caption with uploadmediadetail * refactored few classes * modified ui of depicts * minor fixes * Bug fix, reduced the add description edit text clickable bound (#2973) * moved depicts before categories * replaced previous filename with captions * removed time from filename * added depicts suggestions * [WIP] Wikidata Sandbox (Q4115189) test * changes layout of layout_upload_depicts * changed layout of upload_depicts * code stuck at IO_SCHEDULER * labels and description for depicts activity * Bugfix/uploads (#3000) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Fix memory leak (#3001) * Bugfix/uploads (#3002) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Added tooltip in Title in UploadMediaFragment * BugFix recent categories * Updated test methods * Bugfix/uploads (#3011) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Added tooltip in Title in UploadMediaFragment * BugFix recent categories * Updated test methods * Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView * bugfix/uploads (#3012) * merged with master * BugFix IllegalStateException * setRetainState(true), not required with FragmentStatePagerAdapter * Increase the ViewPager's Offscreen Limit, we want all the fragments to be active * BugFix, clear selected categoris for previous upload session * Clear Selected Categories * Addded JavaDocs for CategoriesModel * Code Formatting in app/src/main/java/fr/free/nrw/commons/upload/UploadModel.java * Added class level JavaDoc UploadRemoteDataSource * Added class level JavaDoc for UploadRepository * Added JavaDocs for ThumbnailsAdapter * Added JavaDocs for MediaLicensePresenter, CategoriesPresenter * Removed null check on category query * Show default catgeories based on image title and gps location when category text empty * Allow search for empty category search * Attached image scale listener to upload media image * Bug fix, reduced the add description edit text clickable bound * Added tooltip in Title in UploadMediaFragment * BugFix recent categories * Updated test methods * Avoid memory leak, free the adpater in MediaLicenseFragment.onDestroyView * BugFix Illegal State Exception in ViewpPagerAdapter * Remove irrelevant comment * merge conflict with strings (#3016) * [WIP] Fixed duplicated subscriprion for 'addPropertyP180' * added documentation * fixed issue #3006 * resolved issue #3004 * fixed issue with categoryPresenterTest.kt * send captions as labels * fixed issue with the captions * optimised imports * added upload for captions * minor changes * resolved issue with uploading captions * resolved issue with api call * uploading captions to wikibase * added some tests and documentation * undo formatting changes * uploaded captions as labels to wikibase * minor changes * resolved error with spinner adpater * adding captions to local database * Fixed issue #3035 * fixed issue #3033 * fixed issue #3005 * fixed issue #3005 * added search for depicts * fixed issue with compile time * fixe issue with project build * fixed issue #3044 * merged uploading depicts into branch * uploading depicts * rebased branch * fixed crash due to depicts * modified depicts interface * Resolve merge conflicts * Fix issues with API calls * Use wikidata token * searching depictions from depicts activity * added some documentation and other changes * fixed crash on selecting depictions * sending wikidataentity id to upload depictions * added changes after review * Fixed issue with next button diabling in media detail activity * added tests for depictions * added all the unit tests and fixed few more issues * showing captions in media details * show captions in media details * added documentations and worked upon review comments * parsing response for depictions * displaying captions and depiction QID in media detail * added documentation * fetching labels from QIDs * captions working perfectly * added documentations and code cleaning * minor changes * minor changes * Showing items in explore * added search via depicts in explore * Added setOffscreenPageLimit in ViewPager * show captions in explore * show captions in home * showing depict images under items * added documentation and code refactoring * enabled pagination in depiction search * added some tests and media deatils in depiction detail activity * fixed bug with back button in media * fixed issue #3100 * fixed issue #3098 * fixed issue #3099 * fixed issue #3104 and #3098 * showing captions in place of title in home and explore:media * show captions in explore:depiction image list activity * showing depictions in media details * showing depictions in media details in production flavor * fixed issue #3108 and #3107 * fix isse #3108 * fixed issue #3110 and #3112 * fixed issue #3113 * added documentations * fixed issue #3076 and #3109 * added depiction search test * fixed issue #3113 * fixed issue #3111 * fixed issue #3106 * Showing items in explore * minor change * fixed issue #3118 and some other changes * added MVP in searchdepictionsfragment * added mvp architecture * added MVP architecture to DepictedImagesDetailsActivity * added documentation and some minor changes * added image to depicted item in search depictions * * Use callbacks from renderer to fetch thumbnails * adding fresco to load image in depictions * adding thumbnail image for depictions in upload and explore * pagination issues * fixed issue --(showing previous depiction thumbnail in explore) * Fixed the logic for pagination * hide progress on success of last page * adding sub-items and parent items to search in explore * minor changes for review comments * fixed issue #3119 * fixed issue #3130 * changes after review comments * showing child classes for depictions * Showing child items * showing parent classes for depicted items * adding localised search for parent and child items * clicking on any child class or parent class should call the corresponding class items * fixed issue of showing wrong thumbnail for P18 item * fixed issue #3132 * added test for DepictedImagesPresenter.java * added unit tests for depicted items parent and child classes * removed unused imports and code formatting * fixed issue in search test * deleting unnecessary .attach_pid9313 file * deleting unnecessary .attach_pid9655 file * added SearchDepictionsPresenterTest * changes after review comments * updates for review comments * added more documentations * removed unused code and classes and addressed spacing changes * changes after review * fixed build issues in the app * worked on some review comments * fixed issue:wrong thumbnail appears on wikidata item * minor change * worked on some review changes * worked on review comments * minor change * addressed remaining review comments * replaced hardcoded jpgs with pageIds to fetch captions * added documentation * removed hardcoded extensions and worked on review comments * review comments * [WIP] Added Depicts values for flavors * [WIP] Minor fix * [WIP] Minor fixes * [WIP] Fixed URL * [WIP] Fixed URLs and tokens * Fixed MediaClient: added check for null in continuation store * Fixed Media::from, changed return from null to new Media() * [WIP] Merged with master * Fix #3254 Displays a proper message in explore section when no result for caption * Updated Mockito to org.mockito:mockito-inline:2.13.0 * [WIP] Fixed tests after merging * [WIP] Fixed some JUnit tests * Fixed 'accessing from wrong thread' error * #3222 Delete manifest declaration of activity as fragment - stop casting MainActivity to CatgoryImagesCallback - fix tests * Remove unit test not associated with any class - make CategoryPresenterTest more idiomatic * fix compilation errors Co-authored-by: Vitaly V. Pinchuk <vetal.978@gmail.com> Co-authored-by: Ashish Kumar <ashishkumar468@gmail.com> Co-authored-by: vanshikaarora <vanshikaa937@gmail.com> Co-authored-by: Vivek Maskara <maskaravivek@gmail.com> Co-authored-by: Vanshika Arora <34261945+vanshikaarora@users.noreply.github.com> Co-authored-by: Somanshu and Himanshu <somanshS14@gmail.com>
This commit is contained in:
parent
99c6f5f105
commit
942cef5d5e
144 changed files with 7190 additions and 278 deletions
|
|
@ -42,6 +42,7 @@ dependencies {
|
|||
implementation 'com.dinuscxj:circleprogressbar:1.1.1'
|
||||
implementation 'com.karumi:dexter:5.0.0'
|
||||
implementation "com.jakewharton:butterknife:$BUTTERKNIFE_VERSION"
|
||||
|
||||
kapt "com.jakewharton:butterknife-compiler:$BUTTERKNIFE_VERSION"
|
||||
|
||||
// Logging
|
||||
|
|
@ -63,7 +64,7 @@ dependencies {
|
|||
|
||||
//Mocking
|
||||
testImplementation 'com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0'
|
||||
testImplementation 'org.mockito:mockito-inline:2.8.47'
|
||||
testImplementation 'org.mockito:mockito-inline:2.13.0'
|
||||
testImplementation 'org.mockito:mockito-core:2.23.0'
|
||||
testImplementation "org.powermock:powermock-module-junit4:2.0.0-beta.5"
|
||||
testImplementation "org.powermock:powermock-api-mockito2:2.0.0-beta.5"
|
||||
|
|
@ -200,6 +201,7 @@ android {
|
|||
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
|
||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns.json\""
|
||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.wikimedia.org/wikipedia/commons\""
|
||||
|
|
@ -215,12 +217,14 @@ android {
|
|||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\""
|
||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
|
||||
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.categories.contentprovider\""
|
||||
buildConfigField "String", "DEPICTION_AUTHORITY", "\"fr.free.nrw.commons.depicts.contentprovider\""
|
||||
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.explore.recentsearches.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.bookmarks.locations.contentprovider\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
|
||||
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
|
||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P180\""
|
||||
|
||||
dimension 'tier'
|
||||
}
|
||||
|
|
@ -232,6 +236,7 @@ android {
|
|||
buildConfigField "String", "WIKIMEDIA_API_POTD", "\"https://commons.wikimedia.org/w/api.php?action=featuredfeed&feed=potd&feedformat=rss&language=en\""
|
||||
buildConfigField "String", "WIKIMEDIA_API_HOST", "\"https://commons.wikimedia.beta.wmflabs.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIDATA_API_HOST", "\"https://www.wikidata.org/w/api.php\""
|
||||
buildConfigField "String", "WIKIDATA_URL", "\"https://www.wikidata.org\""
|
||||
buildConfigField "String", "WIKIMEDIA_FORGE_API_HOST", "\"https://tools.wmflabs.org/\""
|
||||
buildConfigField "String", "WIKIMEDIA_CAMPAIGNS_URL", "\"https://raw.githubusercontent.com/commons-app/campaigns/master/campaigns_beta_active.json\""
|
||||
buildConfigField "String", "IMAGE_URL_BASE", "\"https://upload.beta.wmflabs.org/wikipedia/commons\""
|
||||
|
|
@ -247,12 +252,14 @@ android {
|
|||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\""
|
||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
|
||||
buildConfigField "String", "CATEGORY_AUTHORITY", "\"fr.free.nrw.commons.beta.categories.contentprovider\""
|
||||
buildConfigField "String", "DEPICTION_AUTHORITY", "\"fr.free.nrw.commons.beta.depicts.contentprovider\""
|
||||
buildConfigField "String", "RECENT_SEARCH_AUTHORITY", "\"fr.free.nrw.commons.beta.explore.recentsearches.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.contentprovider\""
|
||||
buildConfigField "String", "BOOKMARK_LOCATIONS_AUTHORITY", "\"fr.free.nrw.commons.beta.bookmarks.locations.contentprovider\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"" + getBuildVersion().toString() + "\""
|
||||
buildConfigField "String", "TEST_USERNAME", "\"" + System.getenv("test_user_name") + "\""
|
||||
buildConfigField "String", "TEST_PASSWORD", "\"" + System.getenv("test_user_password") + "\""
|
||||
buildConfigField "String", "DEPICTS_PROPERTY", "\"P245962\""
|
||||
|
||||
dimension 'tier'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import org.junit.Rule
|
||||
import org.junit.runner.RunWith
|
||||
import android.net.Uri
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.core.AllOf
|
||||
import org.junit.Test
|
||||
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DepictionSearchTest {
|
||||
@get:Rule
|
||||
var activityRule = ActivityTestRule(UploadActivity::class.java)
|
||||
|
||||
@Test
|
||||
fun TestForCaptionsAndDepictions() {
|
||||
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
|
||||
|
||||
// Build a result to return from the Camera app
|
||||
|
||||
|
||||
// Stub out the File picker. When an intent is sent to the File picker, this tells
|
||||
// Espresso to respond with the ActivityResult we just created
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages)).perform(ViewActions.click());
|
||||
Espresso.onData(AllOf.allOf(Matchers.anything("spinner text"))).atPosition(1).perform(ViewActions.click());
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.btn_next))
|
||||
.perform(ViewActions.click())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +1,17 @@
|
|||
package fr.free.nrw.commons
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.test.espresso.Espresso
|
||||
import androidx.test.espresso.action.ViewActions
|
||||
import androidx.test.espresso.intent.Intents
|
||||
import androidx.test.espresso.intent.matcher.IntentMatchers
|
||||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import androidx.test.runner.AndroidJUnit4
|
||||
import fr.free.nrw.commons.upload.UploadActivity
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsFragment
|
||||
import org.hamcrest.Matchers
|
||||
import org.hamcrest.core.AllOf
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
|
@ -16,4 +25,25 @@ class UploadActivityTest {
|
|||
fun orientationChange() {
|
||||
UITestHelper.changeOrientation(activityRule)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun TestForCaptionsAndDepictions() {
|
||||
val imageUri = Uri.parse("file://mnt/sdcard/image.jpg")
|
||||
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in english"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages))
|
||||
.perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withId(R.id.spinner_description_languages)).perform(ViewActions.click());
|
||||
Espresso.onData(AllOf.allOf(Matchers.anything("spinner text"))).atPosition(1).perform(ViewActions.click());
|
||||
Espresso.onView(ViewMatchers.withId(R.id.caption_item_edit_text))
|
||||
.perform(ViewActions.typeText("caption in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.description_item_edit_text))
|
||||
.perform(ViewActions.typeText("description in some other language"))
|
||||
Espresso.onView(ViewMatchers.withId(R.id.btn_next))
|
||||
.perform(ViewActions.click())
|
||||
Intents.intended(IntentMatchers.hasComponent(DepictsFragment::class.java.name))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -119,6 +119,11 @@
|
|||
android:label="@string/title_activity_featured_images"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".depictions.WikidataItemDetailsActivity"
|
||||
android:label="@string/title_activity_featured_images"
|
||||
android:parentActivityName=".contributions.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".explore.categories.ExploreActivity"
|
||||
android:label="@string/title_activity_explore"
|
||||
|
|
@ -180,6 +185,13 @@
|
|||
android:label="@string/provider_categories"
|
||||
android:syncable="false" />
|
||||
|
||||
<provider
|
||||
android:authorities="${applicationId}.depicts.contentprovider"
|
||||
android:name=".upload.structure.depictions.DepictsContentProvider"
|
||||
android:exported="false"
|
||||
android:label="@string/provider_depictions"
|
||||
android:syncable="false"/>
|
||||
|
||||
<provider
|
||||
android:name=".explore.recentsearches.RecentSearchesContentProvider"
|
||||
android:authorities="${applicationId}.explore.recentsearches.contentprovider"
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ import fr.free.nrw.commons.logging.FileLoggingTree;
|
|||
import fr.free.nrw.commons.logging.LogUtils;
|
||||
import fr.free.nrw.commons.settings.Prefs;
|
||||
import fr.free.nrw.commons.upload.FileUtils;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictionDao;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.internal.functions.Functions;
|
||||
|
|
@ -311,6 +312,7 @@ public class CommonsApplication extends Application {
|
|||
SQLiteDatabase db = dbOpenHelper.getWritableDatabase();
|
||||
|
||||
CategoryDao.Table.onDelete(db);
|
||||
DepictionDao.Table.onDelete(db);
|
||||
dbOpenHelper.deleteTable(db,CONTRIBUTIONS_TABLE);//Delete the contributions table in the existing db on older versions
|
||||
appDatabase.getContributionDao().deleteAll();
|
||||
BookmarkPicturesDao.Table.onDelete(db);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,13 @@ public class Media implements Parcelable {
|
|||
public String thumbUrl;
|
||||
public String imageUrl;
|
||||
public String filename;
|
||||
public String thumbnailTitle;
|
||||
/**
|
||||
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
|
||||
* This is a replacement of the previously used titles for images (titles were not multilingual)
|
||||
* Also now captions replace the previous convention of using title for filename
|
||||
*/
|
||||
private String caption;
|
||||
public String description; // monolingual description on input...
|
||||
public String discussion;
|
||||
long dataLength;
|
||||
|
|
@ -59,9 +66,30 @@ public class Media implements Parcelable {
|
|||
public String license;
|
||||
public String licenseUrl;
|
||||
public String creator;
|
||||
/**
|
||||
* Wikibase Identifier associated with media files
|
||||
*/
|
||||
public String pageId;
|
||||
public ArrayList<String> categories; // as loaded at runtime?
|
||||
/**
|
||||
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
|
||||
* However unlike categories depictions is multi-lingual
|
||||
*/
|
||||
public ArrayList<Map<String, String>> depictionList;
|
||||
/**
|
||||
* The above hashmap is fetched from API and to diplay in Explore
|
||||
* However this list of depictions is for storing and retrieving depictions from local storage or cache
|
||||
*/
|
||||
public ArrayList<String> depictions;
|
||||
public boolean requestedDeletion;
|
||||
public HashMap<String, String> descriptions; // multilingual descriptions as loaded
|
||||
public Map<String, String> descriptions; // multilingual descriptions as loaded
|
||||
/**
|
||||
* This hasmap stores the list of multilingual captions, where
|
||||
* key of the HashMap is the language and value is the caption in the corresponding language
|
||||
* Ex: key = "en", value: "<caption in short in English>"
|
||||
* key = "de" , value: "<caption in german>"
|
||||
*/
|
||||
public HashMap<String, String> captions;
|
||||
public HashMap<String, String> tags = new HashMap<>();
|
||||
@Nullable public LatLng coordinates;
|
||||
|
||||
|
|
@ -70,7 +98,9 @@ public class Media implements Parcelable {
|
|||
*/
|
||||
protected Media() {
|
||||
this.categories = new ArrayList<>();
|
||||
this.depictions = new ArrayList<>();
|
||||
this.descriptions = new HashMap<>();
|
||||
this.captions = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -88,25 +118,28 @@ public class Media implements Parcelable {
|
|||
* @param localUri Media URI
|
||||
* @param imageUrl Media image URL
|
||||
* @param filename Media filename
|
||||
* @param captions Media captions
|
||||
* @param description Media description
|
||||
* @param dataLength Media date length
|
||||
* @param dateCreated Media creation date
|
||||
* @param dateUploaded Media date uploaded
|
||||
* @param creator Media creator
|
||||
*/
|
||||
public Media(Uri localUri, String imageUrl, String filename, String description,
|
||||
public Media(Uri localUri, String imageUrl, String filename, HashMap<String, String> captions, String description,
|
||||
long dataLength, Date dateCreated, Date dateUploaded, String creator) {
|
||||
this();
|
||||
this.localUri = localUri;
|
||||
this.thumbUrl = imageUrl;
|
||||
this.imageUrl = imageUrl;
|
||||
this.filename = filename;
|
||||
this.captions = captions;
|
||||
this.description = description;
|
||||
this.dataLength = dataLength;
|
||||
this.dateCreated = dateCreated;
|
||||
this.dateUploaded = dateUploaded;
|
||||
this.creator = creator;
|
||||
this.categories = new ArrayList<>();
|
||||
this.depictions = new ArrayList<>();
|
||||
this.descriptions = new HashMap<>();
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +149,7 @@ public class Media implements Parcelable {
|
|||
thumbUrl = in.readString();
|
||||
imageUrl = in.readString();
|
||||
filename = in.readString();
|
||||
caption = in.readString();
|
||||
description = in.readString();
|
||||
dataLength = in.readLong();
|
||||
dateCreated = (Date) in.readSerializable();
|
||||
|
|
@ -128,7 +162,11 @@ public class Media implements Parcelable {
|
|||
if (categories != null) {
|
||||
in.readStringList(categories);
|
||||
}
|
||||
if (depictions != null) {
|
||||
in.readStringList(depictions);
|
||||
}
|
||||
descriptions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
||||
captions = in.readHashMap(ClassLoader.getSystemClassLoader());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -143,12 +181,12 @@ public class Media implements Parcelable {
|
|||
public static Media from(MwQueryPage page) {
|
||||
ImageInfo imageInfo = page.imageInfo();
|
||||
if (imageInfo == null) {
|
||||
return null;
|
||||
return new Media(); // null is not allowed
|
||||
}
|
||||
ExtMetadata metadata = imageInfo.getMetadata();
|
||||
if (metadata == null) {
|
||||
Media media = new Media(null, imageInfo.getOriginalUrl(),
|
||||
page.title(), "", 0, null, null, null);
|
||||
page.title(), new HashMap<>() , "", 0, null, null, null);
|
||||
if (!StringUtils.isBlank(imageInfo.getThumbUrl())) {
|
||||
media.setThumbUrl(imageInfo.getThumbUrl());
|
||||
}
|
||||
|
|
@ -158,6 +196,7 @@ public class Media implements Parcelable {
|
|||
Media media = new Media(null,
|
||||
imageInfo.getOriginalUrl(),
|
||||
page.title(),
|
||||
new HashMap<>(),
|
||||
"",
|
||||
0,
|
||||
safeParseDate(metadata.dateTime()),
|
||||
|
|
@ -169,6 +208,8 @@ public class Media implements Parcelable {
|
|||
media.setThumbUrl(imageInfo.getThumbUrl());
|
||||
}
|
||||
|
||||
media.setPageId(String.valueOf(page.pageId()));
|
||||
|
||||
String language = Locale.getDefault().getLanguage();
|
||||
if (StringUtils.isBlank(language)) {
|
||||
language = "default";
|
||||
|
|
@ -203,6 +244,18 @@ public class Media implements Parcelable {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return pageId for the current media object*/
|
||||
public String getPageId() {
|
||||
return pageId;
|
||||
}
|
||||
|
||||
/**
|
||||
*sets pageId for the current media object
|
||||
*/
|
||||
private void setPageId(String pageId) {
|
||||
this.pageId = pageId;
|
||||
}
|
||||
public String getThumbUrl() {
|
||||
return thumbUrl;
|
||||
}
|
||||
|
|
@ -233,6 +286,21 @@ public class Media implements Parcelable {
|
|||
return filename != null ? getPageTitle().getDisplayTextWithoutNamespace().replaceFirst("[.][^.]+$", "") : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Caption(if available) as the thumbnail title of the image
|
||||
*/
|
||||
public void setThumbnailTitle(String title) {
|
||||
this.thumbnailTitle = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return title to be shown on image thumbnail
|
||||
* If caption is available for the image then it returns caption else filename
|
||||
*/
|
||||
public String getThumbnailTitle() {
|
||||
return thumbnailTitle != null? thumbnailTitle : getDisplayTitle();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets file page title
|
||||
* @return New media page title
|
||||
|
|
@ -299,6 +367,37 @@ public class Media implements Parcelable {
|
|||
return description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
|
||||
* This is a replacement of the previously used titles for images (titles were not multilingual)
|
||||
* Also now captions replace the previous convention of using title for filename
|
||||
*
|
||||
* @return caption
|
||||
*/
|
||||
public String getCaption() {
|
||||
return caption;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return depictions associated with the current media
|
||||
*/
|
||||
public ArrayList<Map<String, String>> getDepiction() {
|
||||
return depictionList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
|
||||
* This is a replacement of the previously used titles for images (titles were not multilingual)
|
||||
* Also now captions replace the previous convention of using title for filename
|
||||
*
|
||||
* key of the HashMap is the language and value is the caption in the corresponding language
|
||||
*
|
||||
* returns list of captions stored in hashmap
|
||||
*/
|
||||
public HashMap<String, String> getCaptions() {
|
||||
return captions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the file description.
|
||||
* @param description the new description of the file
|
||||
|
|
@ -452,6 +551,13 @@ public class Media implements Parcelable {
|
|||
return (ArrayList<String>) categories.clone(); // feels dirty
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array list of depictions associated with the current media
|
||||
*/
|
||||
public ArrayList<String> getDepictions() {
|
||||
return (ArrayList<String>) depictions.clone();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the categories the file falls under.
|
||||
* </p>
|
||||
|
|
@ -464,6 +570,11 @@ public class Media implements Parcelable {
|
|||
this.categories.addAll(categories);
|
||||
}
|
||||
|
||||
public void setDepictions(List<String> depictions) {
|
||||
this.depictions.clear();
|
||||
this.depictions.addAll(depictions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies (or sets) media descriptions
|
||||
* @param descriptions Media descriptions
|
||||
|
|
@ -523,6 +634,7 @@ public class Media implements Parcelable {
|
|||
parcel.writeString(thumbUrl);
|
||||
parcel.writeString(imageUrl);
|
||||
parcel.writeString(filename);
|
||||
parcel.writeString(caption);
|
||||
parcel.writeString(description);
|
||||
parcel.writeLong(dataLength);
|
||||
parcel.writeSerializable(dateCreated);
|
||||
|
|
@ -533,7 +645,9 @@ public class Media implements Parcelable {
|
|||
parcel.writeInt(height);
|
||||
parcel.writeString(license);
|
||||
parcel.writeStringList(categories);
|
||||
parcel.writeStringList(depictions);
|
||||
parcel.writeMap(descriptions);
|
||||
parcel.writeMap(captions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -559,4 +673,27 @@ public class Media implements Parcelable {
|
|||
public void setLicense(String license) {
|
||||
this.license = license;
|
||||
}
|
||||
|
||||
/**
|
||||
* Captions are a feature part of Structured data. They are meant to store short, multilingual descriptions about files
|
||||
* This is a replacement of the previously used titles for images (titles were not multilingual)
|
||||
* Also now captions replace the previous convention of using title for filename
|
||||
*
|
||||
* This function sets captions
|
||||
* @param caption
|
||||
*/
|
||||
public void setCaption(String caption) {
|
||||
this.caption = caption;
|
||||
}
|
||||
|
||||
public void setCaptions(HashMap<String, String> captions) {
|
||||
this.captions = captions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets depictions for the current media obtained fro Wikibase API
|
||||
*/
|
||||
public void setDepiction(ArrayList<Map<String, String>> depictions) {
|
||||
this.depictionList = depictions;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,14 @@ package fr.free.nrw.commons;
|
|||
|
||||
import androidx.core.text.HtmlCompat;
|
||||
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
|
|
@ -26,16 +34,20 @@ public class MediaDataExtractor {
|
|||
|
||||
/**
|
||||
* Simplified method to extract all details required to show media details.
|
||||
* It fetches media object, deletion status and talk page for the filename
|
||||
* It fetches media object, deletion status, talk page and captions for the filename
|
||||
* @param filename for which the details are to be fetched
|
||||
* @return full Media object with all details including deletion status and talk page
|
||||
*/
|
||||
public Single<Media> fetchMediaDetails(String filename) {
|
||||
public Single<Media> fetchMediaDetails(String filename, String pageId) {
|
||||
Single<Media> mediaSingle = getMediaFromFileName(filename);
|
||||
Single<Boolean> pageExistsSingle = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + filename);
|
||||
Single<String> discussionSingle = getDiscussion(filename);
|
||||
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, (media, deletionStatus, discussion) -> {
|
||||
Single<String> captionSingle = getCaption("M"+pageId);
|
||||
Single<JsonObject> depictionSingle = getDepictions(filename);
|
||||
return Single.zip(mediaSingle, pageExistsSingle, discussionSingle, captionSingle, depictionSingle, (media, deletionStatus, discussion, caption, depiction) -> {
|
||||
media.setDiscussion(discussion);
|
||||
media.setCaption(caption);
|
||||
media.setDepiction(formatDepictions(depiction));
|
||||
if (deletionStatus) {
|
||||
media.setRequestedDeletion();
|
||||
}
|
||||
|
|
@ -43,6 +55,62 @@ public class MediaDataExtractor {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains captions using filename
|
||||
* @param wikibaseIdentifier
|
||||
*
|
||||
* @return caption for the image in user's locale
|
||||
* Ex: "a nice painting" (english locale) and "No Caption" in case the caption is not available for the image
|
||||
*/
|
||||
private Single<String> getCaption(String wikibaseIdentifier) {
|
||||
return mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* From the Json Object extract depictions into an array list
|
||||
* @param mediaResponse
|
||||
* @return List containing map for depictions, the map has two keys,
|
||||
* first key is for the label and second is for the url of the item
|
||||
*/
|
||||
private ArrayList<Map<String, String>> formatDepictions(JsonObject mediaResponse) {
|
||||
try {
|
||||
JsonArray depictionArray = (JsonArray) mediaResponse.get("Depiction");
|
||||
ArrayList<Map<String, String>> depictedItemList = new ArrayList<>();
|
||||
try {
|
||||
for (int i = 0; i <depictionArray.size() ; i++) {
|
||||
JsonObject depictedItem = (JsonObject) depictionArray.get(i);
|
||||
Map <String, String> depictedObject = new HashMap<>();
|
||||
String label = depictedItem.get("label").toString();
|
||||
String id = depictedItem.get("id").toString();
|
||||
String transformedLabel = label.substring(3, label.length()-3);
|
||||
String transformedId = id.substring(1,id.length() - 1);
|
||||
depictedObject.put("label", transformedLabel); //remove the additional characters obtained in label and ID object to extract the relevant string (since the string also contains extra quites that are not required)
|
||||
depictedObject.put("id", transformedId);
|
||||
depictedItemList.add(depictedObject);
|
||||
}
|
||||
return depictedItemList;
|
||||
} catch (NullPointerException e) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
} catch (ClassCastException c) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch caption and depictions from the MediaWiki API
|
||||
* @param filename the filename we will return the caption for
|
||||
* @return a map containing caption and depictions (empty string in the map if no caption/depictions)
|
||||
*/
|
||||
private Single<JsonObject> getDepictions(String filename) {
|
||||
return mediaClient.getCaptionAndDepictions(filename)
|
||||
.map(mediaResponse -> {
|
||||
return mediaResponse;
|
||||
}).doOnError(throwable -> {
|
||||
Timber.e(throwable+ "error while fetching depictions");
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method can be used to fetch media for a given filename
|
||||
* @param filename Eg. File:Test.jpg
|
||||
|
|
|
|||
|
|
@ -44,6 +44,10 @@ import static android.view.View.VISIBLE;
|
|||
public class CategoryImagesListFragment extends DaggerFragment {
|
||||
|
||||
private static int TIMEOUT_SECONDS = 15;
|
||||
/**
|
||||
* counts the total number of items loaded from the API
|
||||
*/
|
||||
private int mediaSize = 0;
|
||||
|
||||
private GridViewAdapter gridAdapter;
|
||||
|
||||
|
|
@ -256,6 +260,35 @@ public class CategoryImagesListFragment extends DaggerFragment {
|
|||
progressBar.setVisibility(GONE);
|
||||
isLoading = false;
|
||||
statusTextView.setVisibility(GONE);
|
||||
for (Media m : collection) {
|
||||
replaceTitlesWithCaptions("M"+m.getPageId(), mediaSize++);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
|
||||
* else show filename
|
||||
*/
|
||||
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int i) {
|
||||
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(subscriber -> {
|
||||
handleLabelforImage(subscriber, i);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* If caption is available for the image, then modify grid adapter
|
||||
* to show captions
|
||||
*/
|
||||
private void handleLabelforImage(String s, int position) {
|
||||
if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
|
||||
gridAdapter.getItem(position).setThumbnailTitle(s);
|
||||
gridAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import android.view.ViewGroup;
|
|||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -55,7 +57,7 @@ public class GridViewAdapter extends ArrayAdapter {
|
|||
data = new ArrayList<>();
|
||||
return false;
|
||||
}
|
||||
if (data.size() <= 0) {
|
||||
if (data.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String fileName = data.get(0).getFilename();
|
||||
|
|
@ -86,12 +88,22 @@ public class GridViewAdapter extends ArrayAdapter {
|
|||
SimpleDraweeView imageView = convertView.findViewById(R.id.categoryImageView);
|
||||
TextView fileName = convertView.findViewById(R.id.categoryImageTitle);
|
||||
TextView author = convertView.findViewById(R.id.categoryImageAuthor);
|
||||
fileName.setText(item.getDisplayTitle());
|
||||
fileName.setText(item.getThumbnailTitle());
|
||||
setAuthorView(item, author);
|
||||
imageView.setImageURI(item.getThumbUrl());
|
||||
return convertView;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the Media item at the given position
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public Media getItem(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows author information if its present
|
||||
* @param item
|
||||
|
|
|
|||
|
|
@ -12,7 +12,9 @@ import androidx.room.PrimaryKey;
|
|||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
|
|
@ -71,11 +73,17 @@ public class Contribution extends Media {
|
|||
public Uri contentProviderUri;
|
||||
public String dateCreatedSource;
|
||||
|
||||
/**
|
||||
* Each depiction loaded in depictions activity is associated with a wikidata entity id,
|
||||
* this Id is in turn used to upload depictions to wikibase
|
||||
*/
|
||||
public ArrayList<String> depictionsEntityIds;
|
||||
|
||||
public Contribution(Uri contentUri, String filename, Uri localUri, String imageUrl, Date dateCreated,
|
||||
int state, long dataLength, Date dateUploaded, long transferred,
|
||||
String source, String description, String creator, boolean isMultiple,
|
||||
String source, HashMap<String, String> captions, String description, String creator, boolean isMultiple,
|
||||
int width, int height, String license) {
|
||||
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
this.contentUri = contentUri;
|
||||
this.state = state;
|
||||
this.transferred = transferred;
|
||||
|
|
@ -87,17 +95,18 @@ public class Contribution extends Media {
|
|||
this.dateCreatedSource = "";
|
||||
}
|
||||
|
||||
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
|
||||
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords) {
|
||||
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
public Contribution(Uri localUri, String imageUrl, String filename, HashMap<String, String> captions, String description, long dataLength,
|
||||
Date dateCreated, Date dateUploaded, String creator, String editSummary, ArrayList<String> depictionsEntityIds, String decimalCoords) {
|
||||
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
this.decimalCoords = decimalCoords;
|
||||
this.editSummary = editSummary;
|
||||
this.dateCreatedSource = "";
|
||||
this.depictionsEntityIds = depictionsEntityIds;
|
||||
}
|
||||
|
||||
public Contribution(Uri localUri, String imageUrl, String filename, String description, long dataLength,
|
||||
public Contribution(Uri localUri, String imageUrl, String filename, HashMap<String, String> captions, String description, long dataLength,
|
||||
Date dateCreated, Date dateUploaded, String creator, String editSummary, String decimalCoords, int state) {
|
||||
super(localUri, imageUrl, filename, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
super(localUri, imageUrl, filename, captions, description, dataLength, dateCreated, dateUploaded, creator);
|
||||
this.decimalCoords = decimalCoords;
|
||||
this.editSummary = editSummary;
|
||||
this.dateCreatedSource = "";
|
||||
|
|
@ -167,6 +176,13 @@ public class Contribution extends Media {
|
|||
this.dateUploaded = date;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets depiction entity ids for the given contribution
|
||||
*/
|
||||
public void setDepictions(ArrayList<String> depictionsEntityIds) {
|
||||
this.depictionsEntityIds = depictionsEntityIds;
|
||||
}
|
||||
|
||||
public String getPageContents(Context applicationContext) {
|
||||
StringBuilder buffer = new StringBuilder();
|
||||
buffer
|
||||
|
|
@ -275,4 +291,10 @@ public class Contribution extends Media {
|
|||
this.contentProviderUri = contentProviderUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array list of entityids for the depictions
|
||||
*/
|
||||
public ArrayList<String> getDepictionsEntityIds() {
|
||||
return depictionsEntityIds;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import com.facebook.drawee.view.SimpleDraweeView;
|
|||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
|
|
@ -23,6 +25,7 @@ import fr.free.nrw.commons.R;
|
|||
import fr.free.nrw.commons.contributions.ContributionsListAdapter.Callback;
|
||||
import fr.free.nrw.commons.contributions.model.DisplayableContribution;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.upload.FileUtils;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
|
|
@ -43,6 +46,9 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
@Inject
|
||||
MediaDataExtractor mediaDataExtractor;
|
||||
@Inject
|
||||
MediaClient mediaClient;
|
||||
|
||||
|
||||
@Inject
|
||||
@Named("thumbnail-cache")
|
||||
|
|
@ -51,6 +57,8 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
private DisplayableContribution contribution;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private int position;
|
||||
private static int TIMEOUT_SECONDS = 15;
|
||||
private static final String NO_CAPTION = "No caption";
|
||||
|
||||
ContributionViewHolder(View parent, Callback callback) {
|
||||
super(parent);
|
||||
|
|
@ -64,6 +72,7 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
this.position=position;
|
||||
this.contribution = contribution;
|
||||
fetchAndDisplayThumbnail(contribution);
|
||||
fetchAndDisplayCaption(contribution);
|
||||
titleView.setText(contribution.getDisplayTitle());
|
||||
|
||||
seqNumView.setText(String.valueOf(contribution.getPosition() + 1));
|
||||
|
|
@ -103,6 +112,30 @@ public class ContributionViewHolder extends RecyclerView.ViewHolder {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In contributions first we show the title for the image stored in cache,
|
||||
* then we fetch captions associated with the image and replace title on the thumbnail with caption
|
||||
*
|
||||
* @param contribution
|
||||
*/
|
||||
private void fetchAndDisplayCaption(DisplayableContribution contribution) {
|
||||
if ((contribution.getState() != Contribution.STATE_COMPLETED)) {
|
||||
titleView.setText(contribution.getDisplayTitle());
|
||||
} else {
|
||||
Timber.d("Fetching caption for %s", contribution.getFilename());
|
||||
String wikibaseMediaId = "M"+contribution.getPageId(); // Create Wikibase media id from the page id. Example media id: M80618155 for https://commons.wikimedia.org/wiki/File:Tantanmen.jpeg with has the pageid 80618155
|
||||
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseMediaId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(subscriber -> {
|
||||
if (!subscriber.trim().equals(NO_CAPTION)) {
|
||||
titleView.setText(subscriber);
|
||||
} else titleView.setText(contribution.getDisplayTitle());
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method fetches the thumbnail url from file name
|
||||
* If the thumbnail url is present in cache, then it is used otherwise API call is made to fetch the thumbnail
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import fr.free.nrw.commons.kvstore.JsonKvStore;
|
|||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.location.LocationServiceManager;
|
||||
import fr.free.nrw.commons.location.LocationUpdateListener;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment.MediaDetailProvider;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
|
|
@ -58,6 +59,8 @@ import io.reactivex.Observable;
|
|||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.contributions.Contribution.STATE_FAILED;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import androidx.lifecycle.Observer;
|
|||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -102,8 +103,8 @@ public class ContributionsPresenter implements UserActionListener {
|
|||
.filter(mwQueryLogEvent -> !mwQueryLogEvent.isDeleted()).doOnNext(mwQueryLogEvent -> Timber.d("Image %s passed filters", mwQueryLogEvent.title()))
|
||||
.map(image -> {
|
||||
Contribution contribution = new Contribution(null, null, image.title(),
|
||||
"", -1, image.date(), image.date(), user,
|
||||
"", "", STATE_COMPLETED);
|
||||
new HashMap<>(), "", -1, image.date(), image.date(), user,
|
||||
"", "", STATE_COMPLETED);
|
||||
return contribution;
|
||||
})
|
||||
.toList()
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ public class DisplayableContribution extends Contribution {
|
|||
contribution.getDateUploaded(),
|
||||
contribution.getTransferred(),
|
||||
contribution.getSource(),
|
||||
contribution.getCaptions(),
|
||||
contribution.getDescription(),
|
||||
contribution.getCreator(),
|
||||
contribution.getMultiple(),
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsDao;
|
|||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.category.CategoryDao;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictionDao;
|
||||
|
||||
public class DBOpenHelper extends SQLiteOpenHelper {
|
||||
|
||||
|
|
@ -28,6 +29,7 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
@Override
|
||||
public void onCreate(SQLiteDatabase sqLiteDatabase) {
|
||||
CategoryDao.Table.onCreate(sqLiteDatabase);
|
||||
DepictionDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkPicturesDao.Table.onCreate(sqLiteDatabase);
|
||||
BookmarkLocationsDao.Table.onCreate(sqLiteDatabase);
|
||||
RecentSearchesDao.Table.onCreate(sqLiteDatabase);
|
||||
|
|
@ -36,6 +38,7 @@ public class DBOpenHelper extends SQLiteOpenHelper {
|
|||
@Override
|
||||
public void onUpgrade(SQLiteDatabase sqLiteDatabase, int from, int to) {
|
||||
CategoryDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
DepictionDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkPicturesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
BookmarkLocationsDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
RecentSearchesDao.Table.onUpdate(sqLiteDatabase, from, to);
|
||||
|
|
|
|||
|
|
@ -1,21 +1,16 @@
|
|||
package fr.free.nrw.commons.db;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
|
||||
import org.wikipedia.json.GsonUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.di.ApplicationlessInjection;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Converters {
|
||||
|
||||
|
|
@ -59,7 +54,7 @@ public class Converters {
|
|||
}
|
||||
|
||||
@TypeConverter
|
||||
public static HashMap<String,String> stringToMap(String objectList) {
|
||||
public static HashMap<String,String> stringToHashMap(String objectList) {
|
||||
return objectList == null ? null : getGson().fromJson(objectList,new TypeToken<HashMap<String,String>>(){}.getType());
|
||||
}
|
||||
|
||||
|
|
@ -73,4 +68,24 @@ public class Converters {
|
|||
return objectList == null ? null : getGson().fromJson(objectList,LatLng.class);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String listOfMapToString(ArrayList<Map<String,String>> listOfMaps) {
|
||||
return listOfMaps == null ? null : getGson().toJson(listOfMaps);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static ArrayList<Map<String,String>> stringToListOfMap(String listOfMaps) {
|
||||
return listOfMaps == null ? null :getGson().fromJson(listOfMaps,new TypeToken<ArrayList<Map<String,String>>>(){}.getType());
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String mapToString(Map<String,String> map) {
|
||||
return map == null ? null : getGson().toJson(map);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Map<String,String> stringToMap(String map) {
|
||||
return map == null ? null :getGson().fromJson(map,new TypeToken<Map<String,String>>(){}.getType());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package fr.free.nrw.commons.depictions;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import fr.free.nrw.commons.depictions.Media.DepictedImagesContract;
|
||||
import fr.free.nrw.commons.depictions.Media.DepictedImagesPresenter;
|
||||
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListContract;
|
||||
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListPresenter;
|
||||
|
||||
/**
|
||||
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
|
||||
*/
|
||||
@Module
|
||||
public abstract class DepictionModule {
|
||||
|
||||
@Binds
|
||||
public abstract DepictedImagesContract.UserActionListener bindsDepictedImagesPresenter(
|
||||
DepictedImagesPresenter
|
||||
presenter
|
||||
);
|
||||
|
||||
@Binds
|
||||
public abstract SubDepictionListContract.UserActionListener bindsSubDepictionListPresenter(
|
||||
SubDepictionListPresenter
|
||||
presenter
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
package fr.free.nrw.commons.depictions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.drawee.view.SimpleDraweeView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
/**
|
||||
* Adapter for Items in DepictionDetailsActivity
|
||||
*/
|
||||
public class GridViewAdapter extends ArrayAdapter {
|
||||
|
||||
private List<Media> data;
|
||||
|
||||
public GridViewAdapter(Context context, int layoutResourceId, List<Media> data) {
|
||||
super(context, layoutResourceId, data);
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds more item to the list
|
||||
* Its triggered on scrolling down in the list
|
||||
* @param images
|
||||
*/
|
||||
public void addItems(List<Media> images) {
|
||||
if (data == null) {
|
||||
data = new ArrayList<>();
|
||||
}
|
||||
data.addAll(images);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the first item in the new list with old list and returns true if they are same
|
||||
* Its triggered on successful response of the fetch images API.
|
||||
* @param images
|
||||
*/
|
||||
public boolean containsAll(List<Media> images){
|
||||
if (images == null || images.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (data == null) {
|
||||
data = new ArrayList<>();
|
||||
return false;
|
||||
}
|
||||
if (data.size() <= 0) {
|
||||
return false;
|
||||
}
|
||||
String fileName = data.get(0).getFilename();
|
||||
String imageName = images.get(0).getFilename();
|
||||
return imageName.equals(fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return data == null || data.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the UI for the depicted image item
|
||||
* @param position
|
||||
* @param convertView
|
||||
* @param parent
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
|
||||
if (convertView == null) {
|
||||
convertView = LayoutInflater.from(getContext()).inflate(R.layout.layout_depict_image, null);
|
||||
}
|
||||
|
||||
Media item = data.get(position);
|
||||
SimpleDraweeView imageView = convertView.findViewById(R.id.depict_image_view);
|
||||
TextView fileName = convertView.findViewById(R.id.depict_image_title);
|
||||
TextView author = convertView.findViewById(R.id.depict_image_author);
|
||||
fileName.setText(item.getThumbnailTitle());
|
||||
setAuthorView(item, author);
|
||||
imageView.setImageURI(item.getThumbUrl());
|
||||
return convertView;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Media getItem(int position) {
|
||||
return data.get(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows author information if its present
|
||||
* @param item
|
||||
* @param author
|
||||
*/
|
||||
private void setAuthorView(Media item, TextView author) {
|
||||
if (!TextUtils.isEmpty(item.getCreator())) {
|
||||
String uploadedByTemplate = getContext().getString(R.string.image_uploaded_by);
|
||||
|
||||
String uploadedBy = String.format(Locale.getDefault(), uploadedByTemplate, item.getCreator());
|
||||
author.setText(uploadedBy);
|
||||
} else {
|
||||
author.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
package fr.free.nrw.commons.depictions.Media;
|
||||
|
||||
import android.widget.ListAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.Media;
|
||||
|
||||
/**
|
||||
* Contract with which DepictedImagesFragment and its presenter will talk to each other
|
||||
*/
|
||||
public interface DepictedImagesContract {
|
||||
|
||||
interface View {
|
||||
|
||||
/**
|
||||
* Handles the UI updates for no internet scenario
|
||||
*/
|
||||
void handleNoInternet();
|
||||
|
||||
/**
|
||||
* Handles the UI updates for a error scenario
|
||||
*/
|
||||
void initErrorView();
|
||||
|
||||
/**
|
||||
* Initializes the adapter with a list of Media objects
|
||||
*
|
||||
* @param mediaList List of new Media to be displayed
|
||||
*/
|
||||
void setAdapter(List<Media> mediaList);
|
||||
|
||||
/**
|
||||
* Seat caption to the image at the given position
|
||||
*/
|
||||
void handleLabelforImage(String s, int position);
|
||||
|
||||
/**
|
||||
* Display snackbar
|
||||
*/
|
||||
void showSnackBar();
|
||||
|
||||
/**
|
||||
* Inform the view that there are no more items to be loaded for this search query
|
||||
* or reset the isLastPage for the current query
|
||||
* @param isLastPage
|
||||
*/
|
||||
void setIsLastPage(boolean isLastPage);
|
||||
|
||||
/**
|
||||
* Set visibility of progressbar depending on the boolean value
|
||||
*/
|
||||
void progressBarVisible(Boolean value);
|
||||
|
||||
/**
|
||||
* It return an instance of gridView adapter which helps in extracting media details
|
||||
* used by the gridView
|
||||
*
|
||||
* @return GridView Adapter
|
||||
*/
|
||||
ListAdapter getAdapter();
|
||||
|
||||
/**
|
||||
* adds list to adapter
|
||||
*/
|
||||
void addItemsToAdapter(List<Media> media);
|
||||
|
||||
/**
|
||||
* Sets loading status depending on the boolean value
|
||||
*/
|
||||
void setLoadingStatus(Boolean value);
|
||||
|
||||
/**
|
||||
* Handles the success scenario
|
||||
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
|
||||
*
|
||||
* @param collection List of new Media to be displayed
|
||||
*/
|
||||
void handleSuccess(List<Media> collection);
|
||||
|
||||
}
|
||||
|
||||
interface UserActionListener extends BasePresenter<View> {
|
||||
|
||||
/**
|
||||
* Checks for internet connection and then initializes the grid view with first 10 images of that depiction
|
||||
*/
|
||||
void initList(String entityId);
|
||||
|
||||
/**
|
||||
* Fetches more images for the item and adds it to the grid view adapter
|
||||
*/
|
||||
void fetchMoreImages();
|
||||
|
||||
/**
|
||||
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
|
||||
* else show filename
|
||||
*/
|
||||
void replaceTitlesWithCaptions(String title, int position);
|
||||
|
||||
/**
|
||||
* add items to query list
|
||||
*/
|
||||
void addItemsToQueryList(List<Media> collection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,264 @@
|
|||
package fr.free.nrw.commons.depictions.Media;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.GridView;
|
||||
import android.widget.ListAdapter;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.depictions.GridViewAdapter;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
/**
|
||||
* Fragment for showing image list after selected an item from SearchActivity In Explore
|
||||
*/
|
||||
public class DepictedImagesFragment extends DaggerFragment implements DepictedImagesContract.View {
|
||||
|
||||
|
||||
@BindView(R.id.statusMessage)
|
||||
TextView statusTextView;
|
||||
@BindView(R.id.loadingImagesProgressBar)
|
||||
ProgressBar progressBar;
|
||||
@BindView(R.id.depicts_image_list)
|
||||
GridView gridView;
|
||||
@BindView(R.id.parentLayout)
|
||||
RelativeLayout parentLayout;
|
||||
@Inject
|
||||
DepictedImagesPresenter presenter;
|
||||
private GridViewAdapter gridAdapter;
|
||||
private String entityId = null;
|
||||
private boolean isLastPage;
|
||||
private boolean isLoading = true;
|
||||
private int mediaSize = 0;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.fragment_depict_image, container, false);
|
||||
ButterKnife.bind(this, v);
|
||||
presenter.onAttachView(this);
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
gridView.setOnItemClickListener((AdapterView.OnItemClickListener) getActivity());
|
||||
initViews();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the UI elements for the fragment
|
||||
* Setup the grid view to and scroll listener for it
|
||||
*/
|
||||
private void initViews() {
|
||||
String depictsName = getArguments().getString("wikidataItemName");
|
||||
entityId = getArguments().getString("entityId");
|
||||
if (getArguments() != null && depictsName != null) {
|
||||
initList();
|
||||
setScrollListener();
|
||||
}
|
||||
}
|
||||
|
||||
private void initList() {
|
||||
presenter.initList(entityId);
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
||||
handleNoInternet();
|
||||
} else presenter.initList(entityId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the UI updates for no internet scenario
|
||||
*/
|
||||
@Override
|
||||
public void handleNoInternet() {
|
||||
progressBar.setVisibility(GONE);
|
||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||
statusTextView.setVisibility(VISIBLE);
|
||||
statusTextView.setText(getString(R.string.no_internet));
|
||||
} else {
|
||||
ViewUtil.showShortSnackbar(parentLayout, R.string.no_internet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the UI updates for a error scenario
|
||||
*/
|
||||
@Override
|
||||
public void initErrorView() {
|
||||
progressBar.setVisibility(GONE);
|
||||
if (gridAdapter == null || gridAdapter.isEmpty()) {
|
||||
statusTextView.setVisibility(VISIBLE);
|
||||
statusTextView.setText(getString(R.string.no_images_found));
|
||||
} else {
|
||||
statusTextView.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the scroll listener for the grid view so that more images are fetched when the user scrolls down
|
||||
* Checks if the item has more images before loading
|
||||
* Also checks whether images are currently being fetched before triggering another request
|
||||
*/
|
||||
private void setScrollListener() {
|
||||
gridView.setOnScrollListener(new AbsListView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(AbsListView view, int scrollState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
|
||||
if (!isLastPage && !isLoading && (firstVisibleItem + visibleItemCount >= totalItemCount)) {
|
||||
isLoading = true;
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
||||
handleNoInternet();
|
||||
} else {
|
||||
presenter.fetchMoreImages();
|
||||
}
|
||||
}
|
||||
if (isLastPage) {
|
||||
progressBar.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Seat caption to the image at the given position
|
||||
*/
|
||||
@Override
|
||||
public void handleLabelforImage(String s, int position) {
|
||||
if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
|
||||
gridAdapter.getItem(position).setThumbnailTitle(s);
|
||||
gridAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display snackbar
|
||||
*/
|
||||
@Override
|
||||
public void showSnackBar() {
|
||||
ViewUtil.showShortSnackbar(parentLayout, R.string.error_loading_images);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set visibility of progressbar depending on the boolean value
|
||||
*/
|
||||
@Override
|
||||
public void progressBarVisible(Boolean value) {
|
||||
if (value) {
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
} else {
|
||||
progressBar.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* It return an instance of gridView adapter which helps in extracting media details
|
||||
* used by the gridView
|
||||
*
|
||||
* @return GridView Adapter
|
||||
*/
|
||||
@Override
|
||||
public ListAdapter getAdapter() {
|
||||
return gridAdapter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the adapter with a list of Media objects
|
||||
*
|
||||
* @param mediaList List of new Media to be displayed
|
||||
*/
|
||||
@Override
|
||||
public void setAdapter(List<Media> mediaList) {
|
||||
gridAdapter = new fr.free.nrw.commons.depictions.GridViewAdapter(getContext(), R.layout.layout_depict_image, mediaList);
|
||||
gridView.setAdapter(gridAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* adds list to adapter
|
||||
*/
|
||||
@Override
|
||||
public void addItemsToAdapter(List<Media> media) {
|
||||
gridAdapter.addAll(media);
|
||||
gridAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets loading status depending on the boolean value
|
||||
*/
|
||||
@Override
|
||||
public void setLoadingStatus(Boolean value) {
|
||||
if (!value) {
|
||||
statusTextView.setVisibility(GONE);
|
||||
}
|
||||
isLoading = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inform the view that there are no more items to be loaded for this search query
|
||||
* or reset the isLastPage for the current query
|
||||
* @param isLastPage
|
||||
*/
|
||||
@Override
|
||||
public void setIsLastPage(boolean isLastPage) {
|
||||
this.isLastPage=isLastPage;
|
||||
progressBar.setVisibility(GONE);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handles the success scenario
|
||||
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
|
||||
*
|
||||
* @param collection List of new Media to be displayed
|
||||
*/
|
||||
@Override
|
||||
public void handleSuccess(List<Media> collection) {
|
||||
presenter.addItemsToQueryList(collection);
|
||||
if (gridAdapter == null) {
|
||||
setAdapter(collection);
|
||||
} else {
|
||||
if (gridAdapter.containsAll(collection)) {
|
||||
return;
|
||||
}
|
||||
gridAdapter.addItems(collection);
|
||||
|
||||
try {
|
||||
((WikidataItemDetailsActivity) getContext()).viewPagerNotifyDataSetChanged();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
progressBar.setVisibility(GONE);
|
||||
isLoading = false;
|
||||
statusTextView.setVisibility(GONE);
|
||||
for (Media m : collection) {
|
||||
presenter.replaceTitlesWithCaptions("M"+m.getPageId(), mediaSize++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package fr.free.nrw.commons.depictions.Media;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
||||
|
||||
/**
|
||||
* Presenter for DepictedImagesFragment
|
||||
*/
|
||||
public class DepictedImagesPresenter implements DepictedImagesContract.UserActionListener {
|
||||
|
||||
private static final DepictedImagesContract.View DUMMY = (DepictedImagesContract.View) Proxy
|
||||
.newProxyInstance(
|
||||
DepictedImagesContract.View.class.getClassLoader(),
|
||||
new Class[]{DepictedImagesContract.View.class},
|
||||
(proxy, method, methodArgs) -> null);
|
||||
private static int TIMEOUT_SECONDS = 15;
|
||||
DepictsClient depictsClient;
|
||||
MediaClient mediaClient;
|
||||
@Named("default_preferences")
|
||||
JsonKvStore depictionKvStore;
|
||||
private final Scheduler ioScheduler;
|
||||
private final Scheduler mainThreadScheduler;
|
||||
private DepictedImagesContract.View view = DUMMY;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
/**
|
||||
* Wikibase enitityId for the depicted Item
|
||||
* Ex: Q9394
|
||||
*/
|
||||
private String entityId = null;
|
||||
private List<Media> queryList = new ArrayList<>();
|
||||
|
||||
@Inject
|
||||
public DepictedImagesPresenter(@Named("default_preferences") JsonKvStore depictionKvStore, DepictsClient depictsClient, MediaClient mediaClient, @Named(IO_THREAD) Scheduler ioScheduler,
|
||||
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
|
||||
this.depictionKvStore = depictionKvStore;
|
||||
this.depictsClient = depictsClient;
|
||||
this.ioScheduler = ioScheduler;
|
||||
this.mainThreadScheduler = mainThreadScheduler;
|
||||
this.mediaClient = mediaClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachView(DepictedImagesContract.View view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = DUMMY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for internet connection and then initializes the grid view with first 10 images of that depiction
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
@Override
|
||||
public void initList(String entityId) {
|
||||
view.setLoadingStatus(true);
|
||||
view.progressBarVisible(true);
|
||||
view.setIsLastPage(false);
|
||||
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, 25, 0)
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(this::handleSuccess, this::handleError));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches more images for the item and adds it to the grid view adapter
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
@Override
|
||||
public void fetchMoreImages() {
|
||||
view.progressBarVisible(true);
|
||||
compositeDisposable.add(depictsClient.fetchImagesForDepictedItem(entityId, 25, queryList.size())
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(this::handlePaginationSuccess, this::handleError));
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the success scenario
|
||||
* it initializes the recycler view by adding items to the adapter
|
||||
*/
|
||||
private void handlePaginationSuccess(List<Media> media) {
|
||||
queryList.addAll(media);
|
||||
view.progressBarVisible(false);
|
||||
view.addItemsToAdapter(media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs and handles API error scenario
|
||||
*
|
||||
* @param throwable
|
||||
*/
|
||||
public void handleError(Throwable throwable) {
|
||||
Timber.e(throwable, "Error occurred while loading images inside items");
|
||||
try {
|
||||
view.initErrorView();
|
||||
view.showSnackBar();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the success scenario
|
||||
* On first load, it initializes the grid view. On subsequent loads, it adds items to the adapter
|
||||
* @param collection List of new Media to be displayed
|
||||
*/
|
||||
public void handleSuccess(List<Media> collection) {
|
||||
if (collection == null || collection.isEmpty()) {
|
||||
if (queryList.isEmpty()) {
|
||||
view.initErrorView();
|
||||
} else {
|
||||
view.setIsLastPage(true);
|
||||
}
|
||||
} else {
|
||||
this.queryList.addAll(collection);
|
||||
view.handleSuccess(collection);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* fetch captions for the image using filename and replace title of on the image thumbnail(if captions are available)
|
||||
* else show filename
|
||||
*/
|
||||
@Override
|
||||
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int position) {
|
||||
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(subscriber -> {
|
||||
view.handleLabelforImage(subscriber, position);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* add items to query list
|
||||
*/
|
||||
@Override
|
||||
public void addItemsToQueryList(List<Media> collection) {
|
||||
queryList.addAll(collection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
||||
/**
|
||||
* The contract with which SubDepictionListFragment and its presenter would talk to each other
|
||||
*/
|
||||
public interface SubDepictionListContract {
|
||||
|
||||
interface View {
|
||||
|
||||
void onImageUrlFetched(String response, int position);
|
||||
|
||||
void onSuccess(List<DepictedItem> mediaList);
|
||||
|
||||
void initErrorView();
|
||||
|
||||
void showSnackbar();
|
||||
|
||||
void setIsLastPage(boolean b);
|
||||
|
||||
boolean isParentClass();
|
||||
}
|
||||
|
||||
interface UserActionListener extends BasePresenter<View> {
|
||||
|
||||
void saveQuery();
|
||||
|
||||
void fetchThumbnailForEntityId(String entityId, int position);
|
||||
|
||||
void initSubDepictionList(String qid, Boolean isParentClass) throws IOException;
|
||||
|
||||
String getQuery();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,191 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass;
|
||||
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import dagger.android.support.DaggerFragment;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsAdapterFactory;
|
||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsRenderer;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
/**
|
||||
* Fragment for parent classes and child classes of Depicted items in Explore
|
||||
*/
|
||||
public class SubDepictionListFragment extends DaggerFragment implements SubDepictionListContract.View {
|
||||
|
||||
@BindView(R.id.imagesListBox)
|
||||
RecyclerView depictionsRecyclerView;
|
||||
@BindView(R.id.imageSearchInProgress)
|
||||
ProgressBar progressBar;
|
||||
@BindView(R.id.imagesNotFound)
|
||||
TextView depictionNotFound;
|
||||
@BindView(R.id.bottomProgressBar)
|
||||
ProgressBar bottomProgressBar;
|
||||
/**
|
||||
* Keeps a record of whether current instance of the fragment if of SubClass or ParentClass
|
||||
*/
|
||||
private boolean isParentClass = false;
|
||||
private RVRendererAdapter<DepictedItem> depictionsAdapter;
|
||||
/**
|
||||
* Used by scroll state listener, when hasMoreImages is false scrolling does not fetches any more images
|
||||
*/
|
||||
private boolean hasMoreImages = true;
|
||||
RecyclerView.LayoutManager layoutManager;
|
||||
/**
|
||||
* Stores entityId for the depiction
|
||||
*/
|
||||
private String entityId;
|
||||
/**
|
||||
* Stores name of the depiction searched
|
||||
*/
|
||||
private String depictsName;
|
||||
|
||||
@Inject SubDepictionListPresenter presenter;
|
||||
|
||||
private final SearchDepictionsAdapterFactory adapterFactory = new SearchDepictionsAdapterFactory(new SearchDepictionsRenderer.DepictCallback() {
|
||||
@Override
|
||||
public void depictsClicked(DepictedItem item) {
|
||||
// Open SubDepiction Details page
|
||||
getActivity().finish();
|
||||
WikidataItemDetailsActivity.startYourself(getContext(), item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fetchThumbnailUrlForEntity(String entityId, int position) {
|
||||
presenter.fetchThumbnailForEntityId(entityId, position);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
}
|
||||
|
||||
private void initViews() {
|
||||
if (getArguments() != null) {
|
||||
depictsName = getArguments().getString("wikidataItemName");
|
||||
entityId = getArguments().getString("entityId");
|
||||
isParentClass = getArguments().getBoolean("isParentClass");
|
||||
if (entityId != null) {
|
||||
initList(entityId, isParentClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void initList(String qid, Boolean isParentClass) {
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
||||
handleNoInternet();
|
||||
} else {
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
try {
|
||||
presenter.initSubDepictionList(qid, isParentClass);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
View v = inflater.inflate(R.layout.fragment_browse_image, container, false);
|
||||
ButterKnife.bind(this, v);
|
||||
presenter.onAttachView(this);
|
||||
isParentClass = false;
|
||||
depictionNotFound.setVisibility(GONE);
|
||||
if (getActivity().getResources().getConfiguration().orientation
|
||||
== Configuration.ORIENTATION_PORTRAIT) {
|
||||
layoutManager = new LinearLayoutManager(getContext());
|
||||
} else {
|
||||
layoutManager = new GridLayoutManager(getContext(), 2);
|
||||
}
|
||||
initViews();
|
||||
depictionsRecyclerView.setLayoutManager(layoutManager);
|
||||
depictionsAdapter = adapterFactory.create();
|
||||
depictionsRecyclerView.setAdapter(depictionsAdapter);
|
||||
return v;
|
||||
}
|
||||
|
||||
private void handleNoInternet() {
|
||||
progressBar.setVisibility(GONE);
|
||||
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageUrlFetched(String response, int position) {
|
||||
depictionsAdapter.getItem(position).setImageUrl(response);
|
||||
depictionsAdapter.notifyItemChanged(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSuccess(List<DepictedItem> mediaList) {
|
||||
hasMoreImages = false;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
depictionNotFound.setVisibility(GONE);
|
||||
bottomProgressBar.setVisibility(GONE);
|
||||
int itemCount=layoutManager.getItemCount();
|
||||
depictionsAdapter.addAll(mediaList);
|
||||
depictionsRecyclerView.getRecycledViewPool().clear();
|
||||
if(itemCount!=0) {
|
||||
depictionsAdapter.notifyItemRangeInserted(itemCount, mediaList.size()-1);
|
||||
}else{
|
||||
depictionsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initErrorView() {
|
||||
hasMoreImages = false;
|
||||
progressBar.setVisibility(GONE);
|
||||
bottomProgressBar.setVisibility(GONE);
|
||||
depictionNotFound.setVisibility(VISIBLE);
|
||||
String no_depiction = getString(isParentClass? R.string.no_parent_classes: R.string.no_child_classes);
|
||||
depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, depictsName));
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSnackbar() {
|
||||
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setIsLastPage(boolean b) {
|
||||
hasMoreImages = !b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParentClass() {
|
||||
return isParentClass;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
||||
|
||||
/**
|
||||
* Presenter for parent classes and child classes of Depicted items in Explore
|
||||
*/
|
||||
public class SubDepictionListPresenter implements SubDepictionListContract.UserActionListener {
|
||||
|
||||
/**
|
||||
* This creates a dynamic proxy instance of the class,
|
||||
* proxy is to control access to the target object
|
||||
* here our target object is the view.
|
||||
* Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
|
||||
*/
|
||||
private static final SubDepictionListContract.View DUMMY = (SubDepictionListContract.View) Proxy
|
||||
.newProxyInstance(
|
||||
SubDepictionListContract.View.class.getClassLoader(),
|
||||
new Class[]{SubDepictionListContract.View.class},
|
||||
(proxy, method, methodArgs) -> null);
|
||||
|
||||
private final Scheduler ioScheduler;
|
||||
private final Scheduler mainThreadScheduler;
|
||||
private SubDepictionListContract.View view = DUMMY;
|
||||
RecentSearchesDao recentSearchesDao;
|
||||
/**
|
||||
* Value of the search query
|
||||
*/
|
||||
public String query;
|
||||
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
DepictsClient depictsClient;
|
||||
private static int TIMEOUT_SECONDS = 15;
|
||||
private List<DepictedItem> queryList = new ArrayList<>();
|
||||
OkHttpJsonApiClient okHttpJsonApiClient;
|
||||
/**
|
||||
* variable used to record the number of API calls already made for fetching Thumbnails
|
||||
*/
|
||||
private int size = 0;
|
||||
|
||||
@Inject
|
||||
public SubDepictionListPresenter(RecentSearchesDao recentSearchesDao, DepictsClient depictsClient, OkHttpJsonApiClient okHttpJsonApiClient, @Named(IO_THREAD) Scheduler ioScheduler,
|
||||
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
|
||||
this.recentSearchesDao = recentSearchesDao;
|
||||
this.ioScheduler = ioScheduler;
|
||||
this.mainThreadScheduler = mainThreadScheduler;
|
||||
this.depictsClient = depictsClient;
|
||||
this.okHttpJsonApiClient = okHttpJsonApiClient;
|
||||
}
|
||||
@Override
|
||||
public void onAttachView(SubDepictionListContract.View view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = DUMMY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store the current query in Recent searches
|
||||
*/
|
||||
@Override
|
||||
public void saveQuery() {
|
||||
RecentSearch recentSearch = recentSearchesDao.find(query);
|
||||
|
||||
// Newly searched query...
|
||||
if (recentSearch == null) {
|
||||
recentSearch = new RecentSearch(null, query, new Date());
|
||||
} else {
|
||||
recentSearch.setLastSearched(new Date());
|
||||
}
|
||||
recentSearchesDao.save(recentSearch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls Wikibase APIs to fetch Thumbnail image for a given wikidata item
|
||||
*/
|
||||
@Override
|
||||
public void fetchThumbnailForEntityId(String entityId, int position) {
|
||||
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(response -> {
|
||||
view.onImageUrlFetched(response,position);
|
||||
}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initSubDepictionList(String qid, Boolean isParentClass) throws IOException {
|
||||
size = 0;
|
||||
if (isParentClass) {
|
||||
compositeDisposable.add(okHttpJsonApiClient.getParentQIDs(qid)
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(this::handleSuccess, this::handleError));
|
||||
} else {
|
||||
compositeDisposable.add(okHttpJsonApiClient.getChildQIDs(qid)
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(this::handleSuccess, this::handleError));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the success scenario
|
||||
* it initializes the recycler view by adding items to the adapter
|
||||
*/
|
||||
public void handleSuccess(List<DepictedItem> mediaList) {
|
||||
if (mediaList == null || mediaList.isEmpty()) {
|
||||
if(queryList.isEmpty()){
|
||||
view.initErrorView();
|
||||
}else{
|
||||
view.setIsLastPage(true);
|
||||
}
|
||||
} else {
|
||||
this.queryList.addAll(mediaList);
|
||||
view.onSuccess(mediaList);
|
||||
for (DepictedItem m : mediaList) {
|
||||
fetchThumbnailForEntityId(m.getEntityId(), size++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs and handles API error scenario
|
||||
*/
|
||||
private void handleError(Throwable throwable) {
|
||||
Timber.e(throwable, "Error occurred while loading queried depictions");
|
||||
try {
|
||||
view.initErrorView();
|
||||
view.showSnackbar();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass.models;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing SparqlQueryResponse
|
||||
*/
|
||||
public class Binding {
|
||||
|
||||
@SerializedName("subclass")
|
||||
@Expose
|
||||
private Subclass subclass;
|
||||
@SerializedName("subclassLabel")
|
||||
@Expose
|
||||
private SubclassLabel subclassLabel;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public Binding() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param subclassLabel
|
||||
* @param subclass
|
||||
*/
|
||||
public Binding(Subclass subclass, SubclassLabel subclassLabel) {
|
||||
super();
|
||||
this.subclass = subclass;
|
||||
this.subclassLabel = subclassLabel;
|
||||
}
|
||||
|
||||
public Subclass getSubclass() {
|
||||
return subclass;
|
||||
}
|
||||
|
||||
public void setSubclass(Subclass subclass) {
|
||||
this.subclass = subclass;
|
||||
}
|
||||
|
||||
public SubclassLabel getSubclassLabel() {
|
||||
return subclassLabel;
|
||||
}
|
||||
|
||||
public void setSubclassLabel(SubclassLabel subclassLabel) {
|
||||
this.subclassLabel = subclassLabel;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass.models;
|
||||
import java.util.List;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing SparqlQueryResponse
|
||||
*/
|
||||
public class Head {
|
||||
|
||||
@SerializedName("vars")
|
||||
@Expose
|
||||
private List<String> vars = null;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public Head() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param vars
|
||||
*/
|
||||
public Head(List<String> vars) {
|
||||
super();
|
||||
this.vars = vars;
|
||||
}
|
||||
|
||||
public List<String> getVars() {
|
||||
return vars;
|
||||
}
|
||||
|
||||
public void setVars(List<String> vars) {
|
||||
this.vars = vars;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass.models;
|
||||
import java.util.List;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing SparqlQueryResponse
|
||||
*/
|
||||
public class Results {
|
||||
|
||||
@SerializedName("bindings")
|
||||
@Expose
|
||||
private List<Binding> bindings = null;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public Results() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param bindings
|
||||
*/
|
||||
public Results(List<Binding> bindings) {
|
||||
super();
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
public List<Binding> getBindings() {
|
||||
return bindings;
|
||||
}
|
||||
|
||||
public void setBindings(List<Binding> bindings) {
|
||||
this.bindings = bindings;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass.models;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* SparqlQueryResponse obtained while fetching parent classes and sub classes for depicted items in explore
|
||||
*/
|
||||
public class SparqlQueryResponse {
|
||||
|
||||
@SerializedName("head")
|
||||
@Expose
|
||||
private Head head;
|
||||
@SerializedName("results")
|
||||
@Expose
|
||||
private Results results;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public SparqlQueryResponse() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param results
|
||||
* @param head
|
||||
*/
|
||||
public SparqlQueryResponse(Head head, Results results) {
|
||||
super();
|
||||
this.head = head;
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
public Head getHead() {
|
||||
return head;
|
||||
}
|
||||
|
||||
public void setHead(Head head) {
|
||||
this.head = head;
|
||||
}
|
||||
|
||||
public Results getResults() {
|
||||
return results;
|
||||
}
|
||||
|
||||
public void setResults(Results results) {
|
||||
this.results = results;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass.models;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing SparqlQueryResponse
|
||||
*/
|
||||
public class Subclass {
|
||||
|
||||
@SerializedName("type")
|
||||
@Expose
|
||||
private String type;
|
||||
@SerializedName("value")
|
||||
@Expose
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public Subclass() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
* @param type
|
||||
*/
|
||||
public Subclass(String type, String value) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
package fr.free.nrw.commons.depictions.SubClass.models;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing SparqlQueryResponse
|
||||
*/
|
||||
public class SubclassLabel {
|
||||
|
||||
@SerializedName("type")
|
||||
@Expose
|
||||
private String type;
|
||||
@SerializedName("value")
|
||||
@Expose
|
||||
private String value;
|
||||
@SerializedName("xml:lang")
|
||||
@Expose
|
||||
private String xmlLang;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public SubclassLabel() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value
|
||||
* @param xmlLang
|
||||
* @param type
|
||||
*/
|
||||
public SubclassLabel(String type, String value, String xmlLang) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
this.xmlLang = xmlLang;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns type
|
||||
*/
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets value of the depiction
|
||||
*/
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setValue(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* get language in which the depiction was requested
|
||||
*/
|
||||
public String getXmlLang() {
|
||||
return xmlLang;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
package fr.free.nrw.commons.depictions;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment;
|
||||
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListFragment;
|
||||
import fr.free.nrw.commons.explore.ViewPagerAdapter;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
import fr.free.nrw.commons.theme.NavigationBaseActivity;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
||||
/**
|
||||
* Activity to show depiction media, parent classes and child classes of depicted items in Explore
|
||||
*/
|
||||
public class WikidataItemDetailsActivity extends NavigationBaseActivity implements MediaDetailPagerFragment.MediaDetailProvider, AdapterView.OnItemClickListener {
|
||||
private FragmentManager supportFragmentManager;
|
||||
private DepictedImagesFragment depictionImagesListFragment;
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
/**
|
||||
* Name of the depicted item
|
||||
* Ex: Rabbit
|
||||
*/
|
||||
private String wikidataItemName;
|
||||
@BindView(R.id.mediaContainer)
|
||||
FrameLayout mediaContainer;
|
||||
@BindView(R.id.tab_layout)
|
||||
TabLayout tabLayout;
|
||||
@BindView(R.id.viewPager)
|
||||
ViewPager viewPager;
|
||||
|
||||
ViewPagerAdapter viewPagerAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_wikidata_item_details);
|
||||
ButterKnife.bind(this);
|
||||
supportFragmentManager = getSupportFragmentManager();
|
||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||
viewPager.setAdapter(viewPagerAdapter);
|
||||
viewPager.setOffscreenPageLimit(2);
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
setTabs();
|
||||
setPageTitle();
|
||||
initDrawer();
|
||||
forceInitBackButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the passed wikidataItemName from the intents and displays it as the page title
|
||||
*/
|
||||
private void setPageTitle() {
|
||||
if (getIntent() != null && getIntent().getStringExtra("wikidataItemName") != null) {
|
||||
setTitle(getIntent().getStringExtra("wikidataItemName"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on success of API call for featured Images.
|
||||
* The viewpager will notified that number of items have changed.
|
||||
*/
|
||||
public void viewPagerNotifyDataSetChanged() {
|
||||
if (mediaDetails!=null){
|
||||
mediaDetails.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This activity contains 3 tabs and a viewpager. This method is used to set the titles of tab,
|
||||
* Set the fragments according to the tab selected in the viewPager.
|
||||
*/
|
||||
private void setTabs() {
|
||||
List<Fragment> fragmentList = new ArrayList<>();
|
||||
List<String> titleList = new ArrayList<>();
|
||||
depictionImagesListFragment = new DepictedImagesFragment();
|
||||
SubDepictionListFragment subDepictionListFragment = new SubDepictionListFragment();
|
||||
SubDepictionListFragment parentDepictionListFragment = new SubDepictionListFragment();
|
||||
wikidataItemName = getIntent().getStringExtra("wikidataItemName");
|
||||
String entityId = getIntent().getStringExtra("entityId");
|
||||
if (getIntent() != null && wikidataItemName != null) {
|
||||
Bundle arguments = new Bundle();
|
||||
arguments.putString("wikidataItemName", wikidataItemName);
|
||||
arguments.putString("entityId", entityId);
|
||||
arguments.putBoolean("isParentClass", false);
|
||||
depictionImagesListFragment.setArguments(arguments);
|
||||
subDepictionListFragment.setArguments(arguments);
|
||||
Bundle parentClassArguments = new Bundle();
|
||||
parentClassArguments.putString("wikidataItemName", wikidataItemName);
|
||||
parentClassArguments.putString("entityId", entityId);
|
||||
parentClassArguments.putBoolean("isParentClass", true);
|
||||
parentDepictionListFragment.setArguments(parentClassArguments);
|
||||
}
|
||||
fragmentList.add(depictionImagesListFragment);
|
||||
titleList.add(getResources().getString(R.string.title_for_media));
|
||||
fragmentList.add(subDepictionListFragment);
|
||||
titleList.add(getResources().getString(R.string.title_for_child_classes));
|
||||
fragmentList.add(parentDepictionListFragment);
|
||||
titleList.add(getResources().getString(R.string.title_for_parent_classes));
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPager.setOffscreenPageLimit(2);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows media detail fragment when user clicks on any image in the list
|
||||
*/
|
||||
@Override
|
||||
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
tabLayout.setVisibility(View.GONE);
|
||||
viewPager.setVisibility(View.GONE);
|
||||
mediaContainer.setVisibility(View.VISIBLE);
|
||||
if (mediaDetails == null || !mediaDetails.isVisible()) {
|
||||
// set isFeaturedImage true for featured images, to include author field on media detail
|
||||
mediaDetails = new MediaDetailPagerFragment(false, true);
|
||||
FragmentManager supportFragmentManager = getSupportFragmentManager();
|
||||
supportFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.mediaContainer, mediaDetails)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
supportFragmentManager.executePendingTransactions();
|
||||
}
|
||||
mediaDetails.showImage(position);
|
||||
forceInitBackButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (depictionImagesListFragment.getAdapter() == null) {
|
||||
// not yet ready to return data
|
||||
return null;
|
||||
} else {
|
||||
return (Media) depictionImagesListFragment.getAdapter().getItem(i);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called on backPressed of anyFragment in the activity.
|
||||
* If condition is called when mediaDetailFragment is opened.
|
||||
*/
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (supportFragmentManager.getBackStackEntryCount() == 1){
|
||||
// back to search so show search toolbar and hide navigation toolbar
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
viewPager.setVisibility(View.VISIBLE);
|
||||
mediaContainer.setVisibility(View.GONE);
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 (depictionImagesListFragment.getAdapter() == null) {
|
||||
return 0;
|
||||
}
|
||||
return depictionImagesListFragment.getAdapter().getCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumers should be simply using this method to use this activity.
|
||||
*
|
||||
* @param context A Context of the application package implementing this class.
|
||||
* @param depictedItem Name of the depicts for displaying its details
|
||||
*/
|
||||
public static void startYourself(Context context, DepictedItem depictedItem) {
|
||||
Intent intent = new Intent(context, WikidataItemDetailsActivity.class);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
intent.putExtra("wikidataItemName", depictedItem.getDepictsLabel());
|
||||
intent.putExtra("entityId", depictedItem.getEntityId());
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package fr.free.nrw.commons.depictions.models;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for object obtained while parsing depiction response
|
||||
*/
|
||||
public class Continue {
|
||||
|
||||
@SerializedName("sroffset")
|
||||
@Expose
|
||||
private Integer sroffset;
|
||||
@SerializedName("continue")
|
||||
@Expose
|
||||
private String _continue;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public Continue() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param sroffset
|
||||
* @param _continue
|
||||
*/
|
||||
public Continue(Integer sroffset, String _continue) {
|
||||
super();
|
||||
this.sroffset = sroffset;
|
||||
this._continue = _continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets sroffset from Continue object
|
||||
*/
|
||||
public Integer getSroffset() {
|
||||
return sroffset;
|
||||
}
|
||||
|
||||
public void setSroffset(Integer sroffset) {
|
||||
this.sroffset = sroffset;
|
||||
}
|
||||
|
||||
/**
|
||||
* gets continue string from Continue object
|
||||
*/
|
||||
public String getContinue() {
|
||||
return _continue;
|
||||
}
|
||||
|
||||
public void setContinue(String _continue) {
|
||||
this._continue = _continue;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
package fr.free.nrw.commons.depictions.models;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for list of depicted images obtained by fetching using depiction entity
|
||||
*/
|
||||
public class DepictionResponse {
|
||||
|
||||
@SerializedName("batchcomplete")
|
||||
@Expose
|
||||
private String batchcomplete;
|
||||
@SerializedName("continue")
|
||||
@Expose
|
||||
private Continue _continue;
|
||||
@SerializedName("query")
|
||||
@Expose
|
||||
private Query query;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public DepictionResponse() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param query
|
||||
* @param batchcomplete
|
||||
* @param _continue
|
||||
*/
|
||||
public DepictionResponse(String batchcomplete, Continue _continue, Query query) {
|
||||
super();
|
||||
this.batchcomplete = batchcomplete;
|
||||
this._continue = _continue;
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns batchcomplete string from DepictionResponse object
|
||||
*/
|
||||
public String getBatchcomplete() {
|
||||
return batchcomplete;
|
||||
}
|
||||
|
||||
public void setBatchcomplete(String batchcomplete) {
|
||||
this.batchcomplete = batchcomplete;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns continue object from DepictionResponse object
|
||||
*/
|
||||
public Continue getContinue() {
|
||||
return _continue;
|
||||
}
|
||||
|
||||
public void setContinue(Continue _continue) {
|
||||
this._continue = _continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns query object from DepictionResponse object
|
||||
*/
|
||||
public Query getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
public void setQuery(Query query) {
|
||||
this.query = query;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package fr.free.nrw.commons.depictions.models;
|
||||
import java.util.List;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for object obtained while parsing depiction response
|
||||
*
|
||||
* the getSearch() function is used to parse media
|
||||
*/
|
||||
public class Query {
|
||||
|
||||
@SerializedName("searchinfo")
|
||||
@Expose
|
||||
private Searchinfo searchinfo;
|
||||
@SerializedName("search")
|
||||
@Expose
|
||||
private List<Search> search = null;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public Query() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param search
|
||||
* @param searchinfo
|
||||
*/
|
||||
public Query(Searchinfo searchinfo, List<Search> search) {
|
||||
super();
|
||||
this.searchinfo = searchinfo;
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
/**
|
||||
* return searchInfo
|
||||
*/
|
||||
public Searchinfo getSearchinfo() {
|
||||
return searchinfo;
|
||||
}
|
||||
|
||||
public void setSearchinfo(Searchinfo searchinfo) {
|
||||
this.searchinfo = searchinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* the getSearch() function is used to parse media
|
||||
*/
|
||||
public List<Search> getSearch() {
|
||||
return search;
|
||||
}
|
||||
|
||||
public void setSearch(List<Search> search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package fr.free.nrw.commons.depictions.models;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for object obtained while parsing depiction response
|
||||
* this class contains all the details of for the media object
|
||||
*/
|
||||
|
||||
public class Search {
|
||||
|
||||
@SerializedName("ns")
|
||||
@Expose
|
||||
private Integer ns;
|
||||
@SerializedName("title")
|
||||
@Expose
|
||||
private String title;
|
||||
@SerializedName("pageid")
|
||||
@Expose
|
||||
private Integer pageid;
|
||||
@SerializedName("size")
|
||||
@Expose
|
||||
private Integer size;
|
||||
@SerializedName("wordcount")
|
||||
@Expose
|
||||
private Integer wordcount;
|
||||
@SerializedName("snippet")
|
||||
@Expose
|
||||
private String snippet;
|
||||
@SerializedName("timestamp")
|
||||
@Expose
|
||||
private String timestamp;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public Search() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param timestamp
|
||||
* @param title
|
||||
* @param ns
|
||||
* @param snippet
|
||||
* @param wordcount
|
||||
* @param size
|
||||
* @param pageid
|
||||
*/
|
||||
public Search(Integer ns, String title, Integer pageid, Integer size, Integer wordcount, String snippet, String timestamp) {
|
||||
super();
|
||||
this.ns = ns;
|
||||
this.title = title;
|
||||
this.pageid = pageid;
|
||||
this.size = size;
|
||||
this.wordcount = wordcount;
|
||||
this.snippet = snippet;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns ns int from Search object
|
||||
*/
|
||||
public Integer getNs() {
|
||||
return ns;
|
||||
}
|
||||
|
||||
public void setNs(Integer ns) {
|
||||
this.ns = ns;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns title string from Search object
|
||||
*/
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns pageid int from Search object
|
||||
*/
|
||||
public Integer getPageid() {
|
||||
return pageid;
|
||||
}
|
||||
|
||||
public void setPageid(Integer pageid) {
|
||||
this.pageid = pageid;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns size int from Search object
|
||||
*/
|
||||
public Integer getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(Integer size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns wordcount int from Search object
|
||||
*/
|
||||
public Integer getWordcount() {
|
||||
return wordcount;
|
||||
}
|
||||
|
||||
public void setWordcount(Integer wordcount) {
|
||||
this.wordcount = wordcount;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns snippet String from Search object
|
||||
*/
|
||||
public String getSnippet() {
|
||||
return snippet;
|
||||
}
|
||||
|
||||
public void setSnippet(String snippet) {
|
||||
this.snippet = snippet;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns ns int from Search object
|
||||
*/
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package fr.free.nrw.commons.depictions.models;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for object obtained while parsing query object
|
||||
*/
|
||||
|
||||
public class Searchinfo {
|
||||
|
||||
@SerializedName("totalhits")
|
||||
@Expose
|
||||
private Integer totalhits;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*
|
||||
*/
|
||||
public Searchinfo() {
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param totalhits
|
||||
*/
|
||||
public Searchinfo(Integer totalhits) {
|
||||
super();
|
||||
this.totalhits = totalhits;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns "totalhint" integer in SearchInfo object
|
||||
*/
|
||||
public Integer getTotalhits() {
|
||||
return totalhits;
|
||||
}
|
||||
|
||||
public void setTotalhits(Integer totalhits) {
|
||||
this.totalhits = totalhits;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import fr.free.nrw.commons.bookmarks.BookmarksActivity;
|
|||
import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
||||
import fr.free.nrw.commons.category.CategoryImagesActivity;
|
||||
import fr.free.nrw.commons.contributions.MainActivity;
|
||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.explore.SearchActivity;
|
||||
import fr.free.nrw.commons.explore.categories.ExploreActivity;
|
||||
import fr.free.nrw.commons.notification.NotificationActivity;
|
||||
|
|
@ -21,7 +22,7 @@ import fr.free.nrw.commons.upload.UploadActivity;
|
|||
/**
|
||||
* This Class handles the dependency injection (using dagger)
|
||||
* so, if a developer needs to add a new activity to the commons app
|
||||
* then that must be mentioned here to inject the dependencies
|
||||
* then that must be mentioned here to inject the dependencies
|
||||
*/
|
||||
@Module
|
||||
@SuppressWarnings({"WeakerAccess", "unused"})
|
||||
|
|
@ -60,6 +61,9 @@ public abstract class ActivityBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract CategoryDetailsActivity bindCategoryDetailsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract WikidataItemDetailsActivity bindDepictionDetailsActivity();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract ExploreActivity bindExploreActivity();
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import fr.free.nrw.commons.CommonsApplication;
|
|||
import fr.free.nrw.commons.auth.LoginActivity;
|
||||
import fr.free.nrw.commons.contributions.ContributionViewHolder;
|
||||
import fr.free.nrw.commons.contributions.ContributionsModule;
|
||||
import fr.free.nrw.commons.depictions.DepictionModule;
|
||||
import fr.free.nrw.commons.explore.SearchModule;
|
||||
import fr.free.nrw.commons.nearby.PlaceRenderer;
|
||||
import fr.free.nrw.commons.review.ReviewController;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
|
|
@ -33,7 +35,7 @@ import fr.free.nrw.commons.widget.PicOfDayAppWidget;
|
|||
ActivityBuilderModule.class,
|
||||
FragmentBuilderModule.class,
|
||||
ServiceBuilderModule.class,
|
||||
ContentProviderBuilderModule.class, UploadModule.class, ContributionsModule.class
|
||||
ContentProviderBuilderModule.class, UploadModule.class, ContributionsModule.class, SearchModule.class, DepictionModule.class
|
||||
})
|
||||
public interface CommonsApplicationComponent extends AndroidInjector<ApplicationlessInjection> {
|
||||
void inject(CommonsApplication application);
|
||||
|
|
|
|||
|
|
@ -110,6 +110,17 @@ public class CommonsApplicationModule {
|
|||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.CATEGORY_AUTHORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to provide instance of DepictsContentProviderClient
|
||||
* @param context context
|
||||
* @return DepictsContentProviderClient*/
|
||||
|
||||
@Provides
|
||||
@Named("depictions")
|
||||
public ContentProviderClient provideDepictsContentProviderClient(Context context) {
|
||||
return context.getContentResolver().acquireContentProviderClient(BuildConfig.DEPICTION_AUTHORITY);
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is used to provide instance of RecentSearchContentProviderClient
|
||||
* which provides content of Recent Searches from database
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import fr.free.nrw.commons.bookmarks.locations.BookmarkLocationsContentProvider;
|
|||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
||||
import fr.free.nrw.commons.category.CategoryContentProvider;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesContentProvider;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictsContentProvider;
|
||||
|
||||
/**
|
||||
* This Class Represents the Module for dependency injection (using dagger)
|
||||
|
|
@ -19,6 +20,9 @@ public abstract class ContentProviderBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract CategoryContentProvider bindCategoryContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DepictsContentProvider bindDepictsContentProvider();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract RecentSearchesContentProvider bindRecentSearchesContentProvider();
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,10 @@ import fr.free.nrw.commons.category.CategoryImagesListFragment;
|
|||
import fr.free.nrw.commons.category.SubCategoryListFragment;
|
||||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
||||
import fr.free.nrw.commons.contributions.ContributionsListFragment;
|
||||
import fr.free.nrw.commons.depictions.Media.DepictedImagesFragment;
|
||||
import fr.free.nrw.commons.depictions.SubClass.SubDepictionListFragment;
|
||||
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
|
||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment;
|
||||
import fr.free.nrw.commons.explore.images.SearchImageFragment;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailFragment;
|
||||
|
|
@ -17,6 +20,7 @@ import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment;
|
|||
import fr.free.nrw.commons.review.ReviewImageFragment;
|
||||
import fr.free.nrw.commons.settings.SettingsFragment;
|
||||
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
|
||||
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
|
||||
|
||||
|
|
@ -44,6 +48,12 @@ public abstract class FragmentBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract CategoryImagesListFragment bindFeaturedImagesListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DepictedImagesFragment bindDepictedImagesFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SubDepictionListFragment bindSubDepictionListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SubCategoryListFragment bindSubCategoryListFragment();
|
||||
|
||||
|
|
@ -53,6 +63,9 @@ public abstract class FragmentBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract SearchCategoryFragment bindSearchCategoryListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract SearchDepictionsFragment bindSearchDepictionListFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract RecentSearchesFragment bindRecentSearchesFragment();
|
||||
|
||||
|
|
@ -77,6 +90,9 @@ public abstract class FragmentBuilderModule {
|
|||
@ContributesAndroidInjector
|
||||
abstract UploadCategoriesFragment bindUploadCategoriesFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract DepictsFragment bindDepictsFragment();
|
||||
|
||||
@ContributesAndroidInjector
|
||||
abstract MediaLicenseFragment bindMediaLicenseFragment();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import fr.free.nrw.commons.actions.PageEditClient;
|
|||
import fr.free.nrw.commons.actions.PageEditInterface;
|
||||
import fr.free.nrw.commons.category.CategoryInterface;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.media.MediaDetailInterface;
|
||||
import fr.free.nrw.commons.media.MediaInterface;
|
||||
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient;
|
||||
import fr.free.nrw.commons.mwapi.UserInterface;
|
||||
|
|
@ -35,6 +36,9 @@ import fr.free.nrw.commons.review.ReviewInterface;
|
|||
import fr.free.nrw.commons.upload.UploadInterface;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.wikidata.WikidataInterface;
|
||||
import fr.free.nrw.commons.upload.WikiBaseInterface;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.CaptionInterface;
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
|
@ -53,6 +57,7 @@ public class NetworkingModule {
|
|||
|
||||
public static final String NAMED_COMMONS_WIKI_SITE = "commons-wikisite";
|
||||
private static final String NAMED_WIKI_DATA_WIKI_SITE = "wikidata-wikisite";
|
||||
private static final String NAMED_COMMONS_WIKI = "commonswiki";
|
||||
|
||||
public static final String NAMED_COMMONS_CSRF = "commons-csrf";
|
||||
|
||||
|
|
@ -142,6 +147,13 @@ public class NetworkingModule {
|
|||
return new WikiSite(BuildConfig.WIKIDATA_URL);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(NAMED_COMMONS_WIKI)
|
||||
public WikiSite provideCommonsWiki() {
|
||||
return new WikiSite(BuildConfig.COMMONS_URL);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gson objects are very heavy. The app should ideally be using just one instance of it instead of creating new instances everywhere.
|
||||
* @return returns a singleton Gson instance
|
||||
|
|
@ -172,6 +184,24 @@ public class NetworkingModule {
|
|||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, ReviewInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public CaptionInterface provideCaptionInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
||||
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, CaptionInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public DepictsInterface provideDepictsInterface(@Named(NAMED_WIKI_DATA_WIKI_SITE) WikiSite wikidataWikiSite) {
|
||||
return ServiceFactory.get(wikidataWikiSite, BuildConfig.WIKIDATA_URL, DepictsInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public WikiBaseInterface provideWikiBaseInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, WikiBaseInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public UploadInterface provideUploadInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
|
|
@ -207,6 +237,12 @@ public class NetworkingModule {
|
|||
return ServiceFactory.get(commonsWikiSite, BuildConfig.COMMONS_URL, MediaInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public MediaDetailInterface providesMediaDetailInterface(@Named(NAMED_COMMONS_WIKI) WikiSite commonsWikisite) {
|
||||
return ServiceFactory.get(commonsWikisite, BuildConfig.COMMONS_URL, MediaDetailInterface.class);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
public CategoryInterface provideCategoryInterface(@Named(NAMED_COMMONS_WIKI_SITE) WikiSite commonsWikiSite) {
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import fr.free.nrw.commons.Media;
|
|||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.explore.categories.SearchCategoryFragment;
|
||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragment;
|
||||
import fr.free.nrw.commons.explore.images.SearchImageFragment;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesFragment;
|
||||
import fr.free.nrw.commons.media.MediaDetailPagerFragment;
|
||||
|
|
@ -50,6 +51,7 @@ public class SearchActivity extends NavigationBaseActivity
|
|||
|
||||
private SearchImageFragment searchImageFragment;
|
||||
private SearchCategoryFragment searchCategoryFragment;
|
||||
private SearchDepictionsFragment searchDepictionsFragment;
|
||||
private RecentSearchesFragment recentSearchesFragment;
|
||||
private FragmentManager supportFragmentManager;
|
||||
private MediaDetailPagerFragment mediaDetails;
|
||||
|
|
@ -68,6 +70,7 @@ public class SearchActivity extends NavigationBaseActivity
|
|||
setSearchHistoryFragment();
|
||||
viewPagerAdapter = new ViewPagerAdapter(getSupportFragmentManager());
|
||||
viewPager.setAdapter(viewPagerAdapter);
|
||||
viewPager.setOffscreenPageLimit(2); // Because we want all the fragments to be alive
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
setTabs();
|
||||
searchView.setQueryHint(getString(R.string.search_commons));
|
||||
|
|
@ -93,11 +96,14 @@ public class SearchActivity extends NavigationBaseActivity
|
|||
List<Fragment> fragmentList = new ArrayList<>();
|
||||
List<String> titleList = new ArrayList<>();
|
||||
searchImageFragment = new SearchImageFragment();
|
||||
searchDepictionsFragment = new SearchDepictionsFragment();
|
||||
searchCategoryFragment= new SearchCategoryFragment();
|
||||
fragmentList.add(searchImageFragment);
|
||||
titleList.add(getResources().getString(R.string.search_tab_title_media).toUpperCase());
|
||||
fragmentList.add(searchCategoryFragment);
|
||||
titleList.add(getResources().getString(R.string.search_tab_title_categories).toUpperCase());
|
||||
fragmentList.add(searchDepictionsFragment);
|
||||
titleList.add(getResources().getString(R.string.search_tab_title_depictions).toUpperCase());
|
||||
|
||||
viewPagerAdapter.setTabData(fragmentList, titleList);
|
||||
viewPagerAdapter.notifyDataSetChanged();
|
||||
|
|
@ -112,6 +118,11 @@ public class SearchActivity extends NavigationBaseActivity
|
|||
viewPager.setVisibility(View.VISIBLE);
|
||||
tabLayout.setVisibility(View.VISIBLE);
|
||||
searchHistoryContainer.setVisibility(View.GONE);
|
||||
|
||||
if (FragmentUtils.isFragmentUIActive(searchDepictionsFragment)) {
|
||||
searchDepictionsFragment.updateDepictionList(query.toString());
|
||||
}
|
||||
|
||||
if (FragmentUtils.isFragmentUIActive(searchImageFragment)) {
|
||||
searchImageFragment.updateImageList(query.toString());
|
||||
}
|
||||
|
|
@ -119,6 +130,7 @@ public class SearchActivity extends NavigationBaseActivity
|
|||
if (FragmentUtils.isFragmentUIActive(searchCategoryFragment)) {
|
||||
searchCategoryFragment.updateCategoryList(query.toString());
|
||||
}
|
||||
|
||||
}else {
|
||||
//Open RecentSearchesFragment
|
||||
recentSearchesFragment.updateRecentSearches();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
package fr.free.nrw.commons.explore;
|
||||
|
||||
import dagger.Binds;
|
||||
import dagger.Module;
|
||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentContract;
|
||||
import fr.free.nrw.commons.explore.depictions.SearchDepictionsFragmentPresenter;
|
||||
|
||||
/**
|
||||
* The Dagger Module for explore:depictions related presenters and (some other objects maybe in future)
|
||||
*/
|
||||
@Module
|
||||
public abstract class SearchModule {
|
||||
|
||||
@Binds
|
||||
public abstract SearchDepictionsFragmentContract.UserActionListener bindsSearchDepictionsFragmentPresenter(
|
||||
SearchDepictionsFragmentPresenter
|
||||
presenter
|
||||
);
|
||||
}
|
||||
|
|
@ -217,7 +217,7 @@ public class SearchCategoryFragment extends CommonsDaggerSupportFragment {
|
|||
private void initErrorView() {
|
||||
progressBar.setVisibility(GONE);
|
||||
categoriesNotFoundView.setVisibility(VISIBLE);
|
||||
categoriesNotFoundView.setText(getString(R.string.categories_not_found));
|
||||
categoriesNotFoundView.setText(getString(R.string.categories_not_found,query));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -0,0 +1,186 @@
|
|||
package fr.free.nrw.commons.explore.depictions;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.text.ParseException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.depictions.models.Search;
|
||||
import fr.free.nrw.commons.media.MediaInterface;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
/**
|
||||
* Depicts Client to handle custom calls to Commons Wikibase APIs
|
||||
*/
|
||||
@Singleton
|
||||
public class DepictsClient {
|
||||
|
||||
private final DepictsInterface depictsInterface;
|
||||
private final MediaInterface mediaInterface;
|
||||
private static final String NO_DEPICTED_IMAGE = "No Image for Depiction";
|
||||
|
||||
@Inject
|
||||
public DepictsClient(DepictsInterface depictsInterface, MediaInterface mediaInterface) {
|
||||
this.depictsInterface = depictsInterface;
|
||||
this.mediaInterface = mediaInterface;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for depictions using the search item
|
||||
* @return list of depicted items
|
||||
*/
|
||||
public Observable<DepictedItem> searchForDepictions(String query, int limit, int offset) {
|
||||
return depictsInterface.searchForDepicts(
|
||||
query,
|
||||
String.valueOf(limit),
|
||||
Locale.getDefault().getLanguage(),
|
||||
Locale.getDefault().getLanguage(),
|
||||
String.valueOf(offset)
|
||||
)
|
||||
.flatMap(depictSearchResponse ->Observable.fromIterable(depictSearchResponse.getSearch()))
|
||||
.map(depictSearchItem -> new DepictedItem(depictSearchItem.getLabel(), depictSearchItem.getDescription(), "", false, depictSearchItem.getId()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL for image using image name
|
||||
* Ex: title = Guion Bluford
|
||||
* Url = https://upload.wikimedia.org/wikipedia/commons/thumb/0/04/Guion_Bluford.jpg/70px-Guion_Bluford.jpg
|
||||
*/
|
||||
private String getThumbnailUrl(String title) {
|
||||
String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
|
||||
title = title.replace(" ", "_");
|
||||
String MD5Hash = getMd5(title);
|
||||
/**
|
||||
* We use 70 pixels as the size of our Thumbnail (as it is the perfect fits our UI)
|
||||
*/
|
||||
return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/70px-" + title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ex: entityId = Q357458
|
||||
* value returned = Elgin Baylor Night program.jpeg
|
||||
*/
|
||||
public Single<String> getP18ForItem(String entityId) {
|
||||
return depictsInterface.getImageForEntity(entityId)
|
||||
.map(commonsFilename -> {
|
||||
String name;
|
||||
try {
|
||||
JsonObject claims = commonsFilename.getAsJsonObject("claims").getAsJsonObject();
|
||||
JsonObject p18 = claims.get("P18").getAsJsonArray().get(0).getAsJsonObject();
|
||||
JsonObject mainsnak = p18.get("mainsnak").getAsJsonObject();
|
||||
JsonObject datavalue = mainsnak.get("datavalue").getAsJsonObject();
|
||||
JsonPrimitive value = datavalue.get("value").getAsJsonPrimitive();
|
||||
name = value.toString();
|
||||
name = name.substring(1, name.length() - 1);
|
||||
} catch (Exception e) {
|
||||
name="";
|
||||
}
|
||||
if (!name.isEmpty()){
|
||||
return getThumbnailUrl(name);
|
||||
} else return NO_DEPICTED_IMAGE;
|
||||
})
|
||||
.singleOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list of images for a particular depict entity
|
||||
*/
|
||||
public Observable<List<Media>> fetchImagesForDepictedItem(String query, int limit, int sroffset) {
|
||||
return mediaInterface.fetchImagesForDepictedItem("haswbstatement:" + BuildConfig.DEPICTS_PROPERTY + "=" + query, String.valueOf(sroffset))
|
||||
.map(mwQueryResponse -> {
|
||||
List<Media> mediaList = new ArrayList<>();
|
||||
for (Search s: mwQueryResponse.getQuery().getSearch()) {
|
||||
Media media = new Media(null,
|
||||
getUrl(s.getTitle()),
|
||||
s.getTitle(),
|
||||
new HashMap<>(),
|
||||
"",
|
||||
0,
|
||||
safeParseDate(s.getTimestamp()),
|
||||
safeParseDate(s.getTimestamp()),
|
||||
""
|
||||
);
|
||||
mediaList.add(media);
|
||||
}
|
||||
return mediaList;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get url for the image from media of depictions
|
||||
* Ex: Tiger_Woods
|
||||
* Value: https://upload.wikimedia.org/wikipedia/commons/thumb/6/67/Tiger_Woods.jpg/70px-Tiger_Woods.jpg
|
||||
*/
|
||||
private String getUrl(String title) {
|
||||
String baseUrl = "https://upload.wikimedia.org/wikipedia/commons/thumb/";
|
||||
title = title.substring(title.indexOf(':')+1);
|
||||
title = title.replace(" ", "_");
|
||||
String MD5Hash = getMd5(title);
|
||||
return baseUrl + MD5Hash.charAt(0) + '/' + MD5Hash.charAt(0) + MD5Hash.charAt(1) + '/' + title + "/640px-" + title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates MD5 hash for the filename
|
||||
*/
|
||||
public String getMd5(String input)
|
||||
{
|
||||
try {
|
||||
|
||||
// Static getInstance method is called with hashing MD5
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
|
||||
// digest() method is called to calculate message digest
|
||||
// of an input digest() return array of byte
|
||||
byte[] messageDigest = md.digest(input.getBytes());
|
||||
|
||||
// Convert byte array into signum representation
|
||||
BigInteger no = new BigInteger(1, messageDigest);
|
||||
|
||||
// Convert message digest into hex value
|
||||
String hashtext = no.toString(16);
|
||||
while (hashtext.length() < 32) {
|
||||
hashtext = "0" + hashtext;
|
||||
}
|
||||
return hashtext;
|
||||
}
|
||||
|
||||
// For specifying wrong message digest algorithms
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the date string into the required format
|
||||
* @param dateStr
|
||||
* @return date in the required format
|
||||
*/
|
||||
@Nullable
|
||||
private static Date safeParseDate(String dateStr) {
|
||||
try {
|
||||
return CommonsDateUtil.getIso8601DateFormatShort().parse(dateStr);
|
||||
} catch (ParseException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package fr.free.nrw.commons.explore.depictions;
|
||||
|
||||
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
import com.pedrogomez.renderers.RendererBuilder;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
||||
/**
|
||||
* Adapter factory for Items in Explore
|
||||
*/
|
||||
|
||||
public class SearchDepictionsAdapterFactory {
|
||||
private final SearchDepictionsRenderer.DepictCallback listener;
|
||||
|
||||
public SearchDepictionsAdapterFactory(SearchDepictionsRenderer.DepictCallback listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public RVRendererAdapter<DepictedItem> create() {
|
||||
List<DepictedItem> searchImageItemList = new ArrayList<>();
|
||||
RendererBuilder<DepictedItem> builder = new RendererBuilder<DepictedItem>().bind(DepictedItem.class, new SearchDepictionsRenderer(listener));
|
||||
ListAdapteeCollection<DepictedItem> collection = new ListAdapteeCollection<>(
|
||||
searchImageItemList != null ? searchImageItemList : Collections.<DepictedItem>emptyList());
|
||||
return new RVRendererAdapter<>(builder, collection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
package fr.free.nrw.commons.explore.depictions;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
import static android.view.View.VISIBLE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import javax.inject.Inject;
|
||||
|
||||
/**
|
||||
* Display depictions in search fragment
|
||||
*/
|
||||
public class SearchDepictionsFragment extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.View {
|
||||
|
||||
@BindView(R.id.imagesListBox)
|
||||
RecyclerView depictionsRecyclerView;
|
||||
@BindView(R.id.imageSearchInProgress)
|
||||
ProgressBar progressBar;
|
||||
@BindView(R.id.imagesNotFound)
|
||||
TextView depictionNotFound;
|
||||
@BindView(R.id.bottomProgressBar)
|
||||
ProgressBar bottomProgressBar;
|
||||
RecyclerView.LayoutManager layoutManager;
|
||||
private boolean isLoading = true;
|
||||
private int PAGE_SIZE = 25;
|
||||
@Inject
|
||||
SearchDepictionsFragmentPresenter presenter;
|
||||
private final SearchDepictionsAdapterFactory adapterFactory = new SearchDepictionsAdapterFactory(new SearchDepictionsRenderer.DepictCallback() {
|
||||
@Override
|
||||
public void depictsClicked(DepictedItem item) {
|
||||
WikidataItemDetailsActivity.startYourself(getContext(), item);
|
||||
presenter.saveQuery();
|
||||
}
|
||||
|
||||
/**
|
||||
*fetch thumbnail image for all the depicted items (if available)
|
||||
*/
|
||||
@Override
|
||||
public void fetchThumbnailUrlForEntity(String entityId, int position) {
|
||||
presenter.fetchThumbnailForEntityId(entityId,position);
|
||||
}
|
||||
|
||||
});
|
||||
private RVRendererAdapter<DepictedItem> depictionsAdapter;
|
||||
private boolean isLastPage;
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
|
||||
ButterKnife.bind(this, rootView);
|
||||
if (getActivity().getResources().getConfiguration().orientation
|
||||
== Configuration.ORIENTATION_PORTRAIT) {
|
||||
layoutManager = new LinearLayoutManager(getContext());
|
||||
} else {
|
||||
layoutManager = new GridLayoutManager(getContext(), 2);
|
||||
}
|
||||
depictionsRecyclerView.setLayoutManager(layoutManager);
|
||||
depictionsAdapter = adapterFactory.create();
|
||||
depictionsRecyclerView.setAdapter(depictionsAdapter);
|
||||
depictionsRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
|
||||
super.onScrollStateChanged(recyclerView, newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
|
||||
int visibleItemCount = layoutManager.getChildCount();
|
||||
int totalItemCount = layoutManager.getItemCount();
|
||||
int firstVisibleItemPosition=0;
|
||||
if(layoutManager instanceof GridLayoutManager){
|
||||
firstVisibleItemPosition=((GridLayoutManager) layoutManager).findFirstVisibleItemPosition();
|
||||
} else {
|
||||
firstVisibleItemPosition=((LinearLayoutManager)layoutManager).findFirstVisibleItemPosition();
|
||||
}
|
||||
|
||||
/**
|
||||
* If the user isn't currently loading items and the last page hasn’t been reached,
|
||||
* then it checks against the current position in view to decide whether or not to load more items.
|
||||
*/
|
||||
if (!isLoading && !isLastPage) {
|
||||
if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount
|
||||
&& firstVisibleItemPosition >= 0
|
||||
&& totalItemCount >= PAGE_SIZE) {
|
||||
loadMoreItems(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return rootView;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch PAGE_SIZE number of items
|
||||
*/
|
||||
private void loadMoreItems(boolean reInitialise) {
|
||||
presenter.updateDepictionList(presenter.getQuery(),PAGE_SIZE, reInitialise);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
presenter.onAttachView(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user selects "Items" from Search Activity
|
||||
* to load the list of depictions from API
|
||||
*
|
||||
* @param query string searched in the Explore Activity
|
||||
*/
|
||||
public void updateDepictionList(String query) {
|
||||
presenter.initializeQuery(query);
|
||||
if (!NetworkUtils.isInternetConnectionEstablished(getContext())) {
|
||||
handleNoInternet();
|
||||
return;
|
||||
}
|
||||
loadMoreItems(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the UI updates for a error scenario
|
||||
*/
|
||||
@Override
|
||||
public void initErrorView() {
|
||||
progressBar.setVisibility(GONE);
|
||||
bottomProgressBar.setVisibility(GONE);
|
||||
depictionNotFound.setVisibility(VISIBLE);
|
||||
String no_depiction = getString(R.string.depictions_not_found);
|
||||
depictionNotFound.setText(String.format(Locale.getDefault(), no_depiction, presenter.getQuery()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
depictionsAdapter.clear();
|
||||
depictionsRecyclerView.cancelPendingInputEvents();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the UI updates for no internet scenario
|
||||
*/
|
||||
@Override
|
||||
public void handleNoInternet() {
|
||||
progressBar.setVisibility(GONE);
|
||||
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.no_internet);
|
||||
}
|
||||
|
||||
/**
|
||||
* If a non empty list is successfully returned from the api then modify the view
|
||||
* like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
|
||||
*/
|
||||
@Override
|
||||
public void onSuccess(List<DepictedItem> mediaList) {
|
||||
isLoading = false;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
depictionNotFound.setVisibility(GONE);
|
||||
bottomProgressBar.setVisibility(GONE);
|
||||
int itemCount = layoutManager.getItemCount();
|
||||
depictionsAdapter.addAll(mediaList);
|
||||
if(itemCount!=0) {
|
||||
depictionsAdapter.notifyItemRangeInserted(itemCount, mediaList.size()-1);
|
||||
}else{
|
||||
depictionsAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadingDepictions() {
|
||||
depictionNotFound.setVisibility(GONE);
|
||||
bottomProgressBar.setVisibility(View.VISIBLE);
|
||||
progressBar.setVisibility(GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearAdapter() {
|
||||
depictionsAdapter.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showSnackbar() {
|
||||
ViewUtil.showShortSnackbar(depictionsRecyclerView, R.string.error_loading_depictions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RVRendererAdapter<DepictedItem> getAdapter() {
|
||||
return depictionsAdapter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageUrlFetched(String response, int position) {
|
||||
depictionsAdapter.getItem(position).setImageUrl(response);
|
||||
depictionsAdapter.notifyItemChanged(position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Inform the view that there are no more items to be loaded for this search query
|
||||
* or reset the isLastPage for the current query
|
||||
* @param isLastPage
|
||||
*/
|
||||
@Override
|
||||
public void setIsLastPage(boolean isLastPage) {
|
||||
this.isLastPage=isLastPage;
|
||||
progressBar.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
package fr.free.nrw.commons.explore.depictions;
|
||||
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
||||
/**
|
||||
* The contract with with SearchDepictionsFragment and its presenter would talk to each other
|
||||
*/
|
||||
public interface SearchDepictionsFragmentContract {
|
||||
|
||||
interface View {
|
||||
/**
|
||||
* Handles the UI updates for a error scenario
|
||||
*/
|
||||
void initErrorView();
|
||||
|
||||
/**
|
||||
* Handles the UI updates for no internet scenario
|
||||
*/
|
||||
void handleNoInternet();
|
||||
|
||||
/**
|
||||
* If a non empty list is successfully returned from the api then modify the view
|
||||
* like hiding empty labels, hiding progressbar and notifying the apdapter that list of items has been fetched from the API
|
||||
*/
|
||||
void onSuccess(List<DepictedItem> mediaList);
|
||||
|
||||
/**
|
||||
* load depictions
|
||||
*/
|
||||
void loadingDepictions();
|
||||
|
||||
/**
|
||||
* clear adapter
|
||||
*/
|
||||
void clearAdapter();
|
||||
|
||||
/**
|
||||
* show snackbar
|
||||
*/
|
||||
void showSnackbar();
|
||||
|
||||
/**
|
||||
* @return adapter
|
||||
*/
|
||||
RVRendererAdapter<DepictedItem> getAdapter();
|
||||
|
||||
void onImageUrlFetched(String response, int position);
|
||||
|
||||
/**
|
||||
* Inform the view that there are no more items to be loaded for this search query
|
||||
* or reset the isLastPage for the current query
|
||||
* @param isLastPage
|
||||
*/
|
||||
void setIsLastPage(boolean isLastPage);
|
||||
}
|
||||
|
||||
interface UserActionListener extends BasePresenter<View> {
|
||||
|
||||
/**
|
||||
* Called when user selects "Items" from Search Activity
|
||||
* to load the list of depictions from API
|
||||
*
|
||||
* @param query string searched in the Explore Activity
|
||||
* @param reInitialise
|
||||
*/
|
||||
void updateDepictionList(String query, int pageSize, boolean reInitialise);
|
||||
|
||||
/**
|
||||
* This method saves Search Query in the Recent Searches Database.
|
||||
*/
|
||||
void saveQuery();
|
||||
|
||||
/**
|
||||
* Whenever a new query is initiated from the search activity clear the previous adapter
|
||||
* and add new value of the query
|
||||
*/
|
||||
void initializeQuery(String query);
|
||||
|
||||
/**
|
||||
* @return query
|
||||
*/
|
||||
String getQuery();
|
||||
|
||||
/**
|
||||
* After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
|
||||
*/
|
||||
void fetchThumbnailForEntityId(String entityId,int position);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package fr.free.nrw.commons.explore.depictions;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearch;
|
||||
import fr.free.nrw.commons.explore.recentsearches.RecentSearchesDao;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
||||
|
||||
/**
|
||||
* The presenter class for SearchDepictionsFragment
|
||||
*/
|
||||
public class SearchDepictionsFragmentPresenter extends CommonsDaggerSupportFragment implements SearchDepictionsFragmentContract.UserActionListener {
|
||||
|
||||
/**
|
||||
* This creates a dynamic proxy instance of the class,
|
||||
* proxy is to control access to the target object
|
||||
* here our target object is the view.
|
||||
* Thus we when onDettach method of fragment is called we replace the binding of view to our object with the proxy instance
|
||||
*/
|
||||
private static final SearchDepictionsFragmentContract.View DUMMY = (SearchDepictionsFragmentContract.View) Proxy
|
||||
.newProxyInstance(
|
||||
SearchDepictionsFragmentContract.View.class.getClassLoader(),
|
||||
new Class[]{SearchDepictionsFragmentContract.View.class},
|
||||
(proxy, method, methodArgs) -> null);
|
||||
private static int TIMEOUT_SECONDS = 15;
|
||||
protected CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
private final Scheduler ioScheduler;
|
||||
private final Scheduler mainThreadScheduler;
|
||||
|
||||
boolean isLoadingDepictions;
|
||||
String query;
|
||||
RecentSearchesDao recentSearchesDao;
|
||||
DepictsClient depictsClient;
|
||||
JsonKvStore basicKvStore;
|
||||
private SearchDepictionsFragmentContract.View view = DUMMY;
|
||||
private List<DepictedItem> queryList = new ArrayList<>();
|
||||
int offset=0;
|
||||
int size = 0;
|
||||
|
||||
@Inject
|
||||
public SearchDepictionsFragmentPresenter(@Named("default_preferences") JsonKvStore basicKvStore,
|
||||
RecentSearchesDao recentSearchesDao,
|
||||
DepictsClient depictsClient,
|
||||
@Named(IO_THREAD) Scheduler ioScheduler,
|
||||
@Named(MAIN_THREAD) Scheduler mainThreadScheduler) {
|
||||
this.basicKvStore = basicKvStore;
|
||||
this.recentSearchesDao = recentSearchesDao;
|
||||
this.depictsClient = depictsClient;
|
||||
this.ioScheduler = ioScheduler;
|
||||
this.mainThreadScheduler = mainThreadScheduler;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachView(SearchDepictionsFragmentContract.View view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = DUMMY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when user selects "Items" from Search Activity
|
||||
* to load the list of depictions from API
|
||||
*
|
||||
* @param query string searched in the Explore Activity
|
||||
* @param reInitialise
|
||||
*/
|
||||
@Override
|
||||
public void updateDepictionList(String query, int pageSize, boolean reInitialise) {
|
||||
this.query = query;
|
||||
view.loadingDepictions();
|
||||
if (reInitialise) {
|
||||
size = 0;
|
||||
}
|
||||
saveQuery();
|
||||
compositeDisposable.add(depictsClient.searchForDepictions(query, 25, offset)
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.doOnSubscribe(disposable -> saveQuery())
|
||||
.collect(ArrayList<DepictedItem>::new, ArrayList::add)
|
||||
.subscribe(this::handleSuccess, this::handleError));
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs and handles API error scenario
|
||||
*/
|
||||
private void handleError(Throwable throwable) {
|
||||
Timber.e(throwable, "Error occurred while loading queried depictions");
|
||||
try {
|
||||
view.initErrorView();
|
||||
view.showSnackbar();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This method saves Search Query in the Recent Searches Database.
|
||||
*/
|
||||
@Override
|
||||
public void saveQuery() {
|
||||
RecentSearch recentSearch = recentSearchesDao.find(query);
|
||||
|
||||
// Newly searched query...
|
||||
if (recentSearch == null) {
|
||||
recentSearch = new RecentSearch(null, query, new Date());
|
||||
} else {
|
||||
recentSearch.setLastSearched(new Date());
|
||||
}
|
||||
recentSearchesDao.save(recentSearch);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever a new query is initiated from the search activity clear the previous adapter
|
||||
* and add new value of the query
|
||||
*/
|
||||
@Override
|
||||
public void initializeQuery(String query) {
|
||||
this.query = query;
|
||||
this.queryList.clear();
|
||||
offset = 0;//Reset the offset on query change
|
||||
compositeDisposable.clear();
|
||||
view.setIsLastPage(false);
|
||||
view.clearAdapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the success scenario
|
||||
* it initializes the recycler view by adding items to the adapter
|
||||
*/
|
||||
public void handleSuccess(List<DepictedItem> mediaList) {
|
||||
if (mediaList == null || mediaList.isEmpty()) {
|
||||
if(queryList.isEmpty()){
|
||||
view.initErrorView();
|
||||
}else{
|
||||
view.setIsLastPage(true);
|
||||
}
|
||||
} else {
|
||||
this.queryList.addAll(mediaList);
|
||||
view.onSuccess(mediaList);
|
||||
offset=queryList.size();
|
||||
for (DepictedItem m : mediaList) {
|
||||
fetchThumbnailForEntityId(m.getEntityId(), size++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After all the depicted items are loaded fetch thumbnail image for all the depicted items (if available)
|
||||
*/
|
||||
@Override
|
||||
public void fetchThumbnailForEntityId(String entityId,int position) {
|
||||
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(response -> {
|
||||
view.onImageUrlFetched(response,position);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package fr.free.nrw.commons.explore.depictions;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
import com.facebook.common.executors.CallerThreadExecutor;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.datasource.DataSource;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
||||
import com.facebook.imagepipeline.image.CloseableImage;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
import com.pedrogomez.renderers.Renderer;
|
||||
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Renderer for DepictedItem
|
||||
*/
|
||||
public class SearchDepictionsRenderer extends Renderer<DepictedItem> {
|
||||
|
||||
@BindView(R.id.depicts_label)
|
||||
TextView tvDepictionLabel;
|
||||
|
||||
@BindView(R.id.description)
|
||||
TextView tvDepictionDesc;
|
||||
|
||||
@BindView(R.id.depicts_image)
|
||||
ImageView imageView;
|
||||
|
||||
private DepictCallback listener;
|
||||
|
||||
int size = 0;
|
||||
private final static String NO_IMAGE_FOR_DEPICTION = "No Image for Depiction";
|
||||
|
||||
public SearchDepictionsRenderer(DepictCallback listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpView(View rootView) {
|
||||
ButterKnife.bind(this, rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookListeners(View rootView) {
|
||||
rootView.setOnClickListener(v -> {
|
||||
DepictedItem item = getContent();
|
||||
if (listener != null) {
|
||||
listener.depictsClicked(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
|
||||
return inflater.inflate(R.layout.item_depictions, parent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render value to all the items in the search depictions list
|
||||
*/
|
||||
@Override
|
||||
public void render() {
|
||||
DepictedItem item = getContent();
|
||||
tvDepictionLabel.setText(item.getDepictsLabel());
|
||||
tvDepictionDesc.setText(item.getDescription());
|
||||
imageView.setImageDrawable(getContext().getResources().getDrawable(R.drawable.ic_wikidata_logo_24dp));
|
||||
|
||||
Timber.e("line86"+item.getImageUrl());
|
||||
if (!TextUtils.isEmpty(item.getImageUrl())) {
|
||||
if (!item.getImageUrl().equals(NO_IMAGE_FOR_DEPICTION) && !item.getImageUrl().equals(""))
|
||||
{
|
||||
ImageRequest imageRequest = ImageRequestBuilder
|
||||
.newBuilderWithSource(Uri.parse(item.getImageUrl()))
|
||||
.setAutoRotateEnabled(true)
|
||||
.build();
|
||||
|
||||
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
||||
final DataSource<CloseableReference<CloseableImage>>
|
||||
dataSource = imagePipeline.fetchDecodedImage(imageRequest, getContext());
|
||||
|
||||
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
||||
|
||||
@Override
|
||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||
if (dataSource.isFinished() && bitmap != null) {
|
||||
Timber.d("Bitmap loaded from url %s", item.getImageUrl());
|
||||
//imageView.setImageBitmap(Bitmap.createBitmap(bitmap));
|
||||
imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap)));
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailureImpl(DataSource dataSource) {
|
||||
Timber.d("Error getting bitmap from image url %s", item.getImageUrl());
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}, CallerThreadExecutor.getInstance());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public interface DepictCallback {
|
||||
void depictsClicked(DepictedItem item);
|
||||
|
||||
void fetchThumbnailUrlForEntity(String entityId,int position);
|
||||
}
|
||||
}
|
||||
|
|
@ -67,6 +67,11 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
|||
@Named("default_preferences")
|
||||
JsonKvStore defaultKvStore;
|
||||
|
||||
/**
|
||||
* A variable to store number of list items for whom API has been called to fetch captions
|
||||
*/
|
||||
private int mediaSize = 0;
|
||||
|
||||
private RVRendererAdapter<Media> imagesAdapter;
|
||||
private List<Media> queryList = new ArrayList<>();
|
||||
|
||||
|
|
@ -101,7 +106,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
|||
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
|
||||
View rootView = inflater.inflate(R.layout.fragment_browse_image, container, false);
|
||||
ButterKnife.bind(this, rootView);
|
||||
if (getActivity().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
|
||||
if (getContext().getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT){
|
||||
imagesRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
}
|
||||
else{
|
||||
|
|
@ -198,10 +203,36 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
|||
progressBar.setVisibility(GONE);
|
||||
imagesAdapter.addAll(mediaList);
|
||||
imagesAdapter.notifyDataSetChanged();
|
||||
((SearchActivity) getContext()).viewPagerNotifyDataSetChanged();
|
||||
((SearchActivity)getContext()).viewPagerNotifyDataSetChanged();
|
||||
for (Media m : mediaList) {
|
||||
replaceTitlesWithCaptions("M"+m.getPageId(), mediaSize++);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* In explore we first show title and simultaneously call the API to retrieve captions
|
||||
* When captions are retrieved they replace title
|
||||
*/
|
||||
|
||||
public void replaceTitlesWithCaptions(String wikibaseIdentifier, int i) {
|
||||
compositeDisposable.add(mediaClient.getCaptionByWikibaseIdentifier(wikibaseIdentifier)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(subscriber -> {
|
||||
handleLabelforImage(subscriber, i);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
private void handleLabelforImage(String s, int position) {
|
||||
if (!s.trim().equals(getString(R.string.detail_caption_empty))) {
|
||||
imagesAdapter.getItem(position).setThumbnailTitle(s);
|
||||
imagesAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs and handles API error scenario
|
||||
* @param throwable
|
||||
|
|
@ -221,7 +252,7 @@ public class SearchImageFragment extends CommonsDaggerSupportFragment {
|
|||
private void initErrorView() {
|
||||
progressBar.setVisibility(GONE);
|
||||
imagesNotFoundView.setVisibility(VISIBLE);
|
||||
imagesNotFoundView.setText(getString(R.string.images_not_found));
|
||||
imagesNotFoundView.setText(getString(R.string.images_not_found,query));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class SearchImagesRenderer extends Renderer<Media> {
|
|||
@Override
|
||||
public void render() {
|
||||
Media item = getContent();
|
||||
tvImageName.setText(item.getDisplayTitle());
|
||||
tvImageName.setText(item.getThumbnailTitle());
|
||||
browseImage.setImageURI(item.getThumbUrl());
|
||||
setAuthorView(item, categoryImageAuthor);
|
||||
}
|
||||
|
|
|
|||
43
app/src/main/java/fr/free/nrw/commons/media/Caption.java
Normal file
43
app/src/main/java/fr/free/nrw/commons/media/Caption.java
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
/**
|
||||
* Model class for parsing Captions when fetching captions using filename in MediaClient
|
||||
*/
|
||||
public class Caption {
|
||||
|
||||
/**
|
||||
* users language in which caption is written
|
||||
*/
|
||||
@SerializedName("language")
|
||||
private String language;
|
||||
@SerializedName("value")
|
||||
private String value;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public Caption() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param value
|
||||
* @param language
|
||||
*/
|
||||
public Caption(String language, String value) {
|
||||
super();
|
||||
this.language = language;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@SerializedName("language")
|
||||
public String getLanguage() {
|
||||
return language;
|
||||
}
|
||||
|
||||
@SerializedName("value")
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
/**
|
||||
* Represents the Wikibase item associated with a Wikimedia Commons file.
|
||||
* For instance the Wikibase item M63996 represents the Commons file "Paul Cézanne - The Pigeon Tower at Bellevue - 1936.19 - Cleveland Museum of Art.jpg"
|
||||
*/
|
||||
public class CommonsWikibaseItem {
|
||||
|
||||
@SerializedName("type")
|
||||
private String type;
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
@SerializedName("labels")
|
||||
private Map<String, Caption> labels;
|
||||
@SerializedName("statements")
|
||||
private Object statements = null;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public CommonsWikibaseItem() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param id
|
||||
* @param statements
|
||||
* @param labels
|
||||
* @param type
|
||||
*/
|
||||
public CommonsWikibaseItem(String type, String id, Map<String, Caption> labels, Object statements) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.id = id;
|
||||
this.labels = labels;
|
||||
this.statements = statements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ex: "mediainfo
|
||||
*/
|
||||
@SerializedName("type")
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Wikibase Id
|
||||
*/
|
||||
@SerializedName("id")
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return value of captions
|
||||
*/
|
||||
@SerializedName("labels")
|
||||
public Map<String, Caption> getLabels() {
|
||||
return labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contains the Depicts item
|
||||
*/
|
||||
@SerializedName("statements")
|
||||
public Object getStatements() {
|
||||
return statements;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,24 +1,37 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonPrimitive;
|
||||
import com.google.gson.internal.LinkedTreeMap;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.utils.CommonsDateUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
|
|
@ -28,13 +41,17 @@ import timber.log.Timber;
|
|||
public class MediaClient {
|
||||
|
||||
private final MediaInterface mediaInterface;
|
||||
private final MediaDetailInterface mediaDetailInterface;
|
||||
|
||||
//OkHttpJsonApiClient used JsonKvStore for this. I don't know why.
|
||||
private Map<String, Map<String, String>> continuationStore;
|
||||
private static final String NO_CAPTION = "No caption";
|
||||
private static final String NO_DEPICTION = "No depiction";
|
||||
|
||||
@Inject
|
||||
public MediaClient(MediaInterface mediaInterface) {
|
||||
public MediaClient(MediaInterface mediaInterface, MediaDetailInterface mediaDetailInterface) {
|
||||
this.mediaInterface = mediaInterface;
|
||||
this.mediaDetailInterface = mediaDetailInterface;
|
||||
this.continuationStore = new HashMap<>();
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +105,7 @@ public class MediaClient {
|
|||
*/
|
||||
public Single<List<Media>> getMediaListFromSearch(String keyword) {
|
||||
return responseToMediaList(
|
||||
continuationStore.containsKey("search_" + keyword) ?
|
||||
continuationStore.containsKey("search_" + keyword) && (continuationStore.get("search_" + keyword) != null) ?
|
||||
mediaInterface.getMediaListFromSearch(keyword, 10, continuationStore.get("search_" + keyword)) : //if true
|
||||
mediaInterface.getMediaListFromSearch(keyword, 10, Collections.emptyMap()), //if false
|
||||
"search_" + keyword);
|
||||
|
|
@ -108,7 +125,7 @@ public class MediaClient {
|
|||
.map(Media::from)
|
||||
.collect(ArrayList<Media>::new, List::add);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fetches Media object from the imageInfo API
|
||||
*
|
||||
|
|
@ -152,6 +169,7 @@ public class MediaClient {
|
|||
.single(Media.EMPTY);
|
||||
}
|
||||
|
||||
|
||||
@NonNull
|
||||
public Single<String> getPageHtml(String title){
|
||||
return mediaInterface.getPageHtml(title)
|
||||
|
|
@ -160,4 +178,143 @@ public class MediaClient {
|
|||
.map(MwParseResult::text)
|
||||
.first("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return caption for image using wikibaseIdentifier
|
||||
*/
|
||||
public Single<String> getCaptionByWikibaseIdentifier(String wikibaseIdentifier) {
|
||||
return mediaDetailInterface.getCaptionForImage(Locale.getDefault().getLanguage(), wikibaseIdentifier)
|
||||
.map(mediaDetailResponse -> {
|
||||
if (mediaDetailResponse != null && mediaDetailResponse.getSuccess() != null && mediaDetailResponse.getSuccess() == 1 && mediaDetailResponse.getEntities() != null) {
|
||||
Map<String, CommonsWikibaseItem> entities = mediaDetailResponse.getEntities();
|
||||
try {
|
||||
Map.Entry<String, CommonsWikibaseItem> entry = entities.entrySet().iterator().next();
|
||||
CommonsWikibaseItem commonsWikibaseItem = entry.getValue();
|
||||
Map<String, Caption> labels = commonsWikibaseItem.getLabels();
|
||||
Map.Entry<String, Caption> captionEntry = labels.entrySet().iterator().next();
|
||||
Caption caption = captionEntry.getValue();
|
||||
return caption.getValue();
|
||||
|
||||
} catch (Exception e) {
|
||||
return NO_CAPTION;
|
||||
}
|
||||
}
|
||||
return NO_CAPTION;
|
||||
|
||||
})
|
||||
.singleOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches Structured data from API
|
||||
*
|
||||
* @param filename
|
||||
* @return a map containing caption and depictions (empty string in the map if no caption/depictions)
|
||||
*/
|
||||
public Single<JsonObject> getCaptionAndDepictions(String filename) {
|
||||
return mediaDetailInterface.fetchStructuredDataByFilename(Locale.getDefault().getLanguage(), filename)
|
||||
.map(mediaDetailResponse -> {
|
||||
return fetchCaptionandDepictionsFromMediaDetailResponse(mediaDetailResponse);
|
||||
})
|
||||
.singleOrError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the mediaDetailResponse from API to extract captions and depictions
|
||||
* @param mediaDetailResponse Response obtained from API for Media Details
|
||||
* @return a map containing caption and depictions (empty string in the map if no caption/depictions)
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private JsonObject fetchCaptionandDepictionsFromMediaDetailResponse(MediaDetailResponse mediaDetailResponse) {
|
||||
JsonObject mediaDetails = new JsonObject();
|
||||
if (mediaDetailResponse != null && mediaDetailResponse.getSuccess() != null && mediaDetailResponse.getSuccess() == 1 && mediaDetailResponse.getEntities() != null) {
|
||||
Map<String, CommonsWikibaseItem> entities = mediaDetailResponse.getEntities();
|
||||
try {
|
||||
Map.Entry<String, CommonsWikibaseItem> entry = entities.entrySet().iterator().next();
|
||||
CommonsWikibaseItem commonsWikibaseItem = entry.getValue();
|
||||
try {
|
||||
Map<String, Caption> labels = commonsWikibaseItem.getLabels();
|
||||
Map.Entry<String, Caption> captionEntry = labels.entrySet().iterator().next();
|
||||
Caption caption = captionEntry.getValue();
|
||||
JsonElement jsonElement = new JsonPrimitive(caption.getValue());
|
||||
mediaDetails.add("Caption", jsonElement);
|
||||
} catch (Exception e) {
|
||||
JsonElement jsonElement = new JsonPrimitive(NO_CAPTION);
|
||||
mediaDetails.add("Caption", jsonElement);
|
||||
}
|
||||
|
||||
try {
|
||||
LinkedTreeMap statements = (LinkedTreeMap) commonsWikibaseItem.getStatements();
|
||||
ArrayList<LinkedTreeMap> depictsItemList = (ArrayList<LinkedTreeMap>) statements.get(BuildConfig.DEPICTS_PROPERTY);
|
||||
String depictions = null;
|
||||
JsonArray jsonArray = new JsonArray();
|
||||
for (int i = 0; i < depictsItemList.size(); i++) {
|
||||
LinkedTreeMap depictedItem = depictsItemList.get(i);
|
||||
LinkedTreeMap mainsnak = (LinkedTreeMap) depictedItem.get("mainsnak");
|
||||
Map<String, LinkedTreeMap> datavalue = (Map<String, LinkedTreeMap>) mainsnak.get("datavalue");
|
||||
LinkedTreeMap value = datavalue.get("value");
|
||||
String id = value.get("id").toString();
|
||||
JsonObject jsonObject = getLabelForDepiction(id, Locale.getDefault().getLanguage())
|
||||
.subscribeOn(Schedulers.newThread())
|
||||
.blockingGet();
|
||||
jsonArray.add(jsonObject);
|
||||
}
|
||||
mediaDetails.add("Depiction", jsonArray);
|
||||
} catch (Exception e) {
|
||||
JsonElement jsonElement = new JsonPrimitive(NO_DEPICTION);
|
||||
mediaDetails.add("Depiction", jsonElement);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
JsonElement jsonElement = new JsonPrimitive(NO_CAPTION);
|
||||
mediaDetails.add("Caption", jsonElement);
|
||||
jsonElement = null;
|
||||
jsonElement = new JsonPrimitive(NO_DEPICTION);
|
||||
mediaDetails.add("Depiction", jsonElement);
|
||||
}
|
||||
} else {
|
||||
JsonElement jsonElement = new JsonPrimitive(NO_CAPTION);
|
||||
mediaDetails.add("Caption", jsonElement);
|
||||
jsonElement = null;
|
||||
jsonElement = new JsonPrimitive(NO_DEPICTION);
|
||||
mediaDetails.add("Depiction", jsonElement);
|
||||
}
|
||||
|
||||
return mediaDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets labels for Depictions using Entity Id from MediaWikiAPI
|
||||
*
|
||||
* @param entityId EntityId (Ex: Q81566) of the depict entity
|
||||
* @return Json Object having label and Wikidata URL for the Depiction Entity
|
||||
*/
|
||||
public Single<JsonObject> getLabelForDepiction(String entityId, String language) {
|
||||
return mediaDetailInterface.getDepictions(entityId, language)
|
||||
.map(jsonResponse -> {
|
||||
try {
|
||||
if (jsonResponse.get("success").toString().equals("1")) {
|
||||
JsonObject entities = (JsonObject) jsonResponse.getAsJsonObject().get("entities");
|
||||
JsonObject responseObject = (JsonObject) entities.getAsJsonObject().get(entityId);
|
||||
JsonObject labels = responseObject.getAsJsonObject("labels");
|
||||
JsonObject languageObject = labels.getAsJsonObject(language);
|
||||
String label = String.valueOf(languageObject.get("value"));
|
||||
|
||||
|
||||
JsonElement labelJson = new JsonPrimitive(label);
|
||||
JsonElement idJson = new JsonPrimitive(entityId);
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.add("label", labelJson);
|
||||
jsonObject.add("id", idJson);
|
||||
return jsonObject;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Timber.e("Label not found");
|
||||
return new JsonObject();
|
||||
}return new JsonObject();
|
||||
})
|
||||
.singleOrError();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,14 +54,18 @@ import fr.free.nrw.commons.category.CategoryDetailsActivity;
|
|||
import fr.free.nrw.commons.contributions.ContributionsFragment;
|
||||
import fr.free.nrw.commons.delete.DeleteHelper;
|
||||
import fr.free.nrw.commons.delete.ReasonBuilder;
|
||||
import fr.free.nrw.commons.depictions.WikidataItemDetailsActivity;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.ui.widget.CompatTextView;
|
||||
import fr.free.nrw.commons.ui.widget.HtmlTextView;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.utils.ViewUtilWrapper;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.util.Map;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.view.View.GONE;
|
||||
|
|
@ -107,6 +111,12 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
SimpleDraweeView image;
|
||||
@BindView(R.id.mediaDetailTitle)
|
||||
TextView title;
|
||||
@BindView(R.id.caption_layout)
|
||||
LinearLayout captionLayout;
|
||||
@BindView(R.id.depicts_layout)
|
||||
LinearLayout depictsLayout;
|
||||
@BindView(R.id.media_detail_caption)
|
||||
TextView mediaCaption;
|
||||
@BindView(R.id.mediaDetailDesc)
|
||||
HtmlTextView desc;
|
||||
@BindView(R.id.mediaDetailAuthor)
|
||||
|
|
@ -125,6 +135,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
LinearLayout nominatedForDeletion;
|
||||
@BindView(R.id.mediaDetailCategoryContainer)
|
||||
LinearLayout categoryContainer;
|
||||
@BindView(R.id.media_detail_depiction_container)
|
||||
LinearLayout depictionContainer;
|
||||
@BindView(R.id.authorLinearLayout)
|
||||
LinearLayout authorLayout;
|
||||
@BindView(R.id.nominateDeletion)
|
||||
|
|
@ -133,8 +145,15 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
ScrollView scrollView;
|
||||
|
||||
private ArrayList<String> categoryNames;
|
||||
/**
|
||||
* Depicts is a feature part of Structured data. Multiple Depictions can be added for an image just like categories.
|
||||
* However unlike categories depictions is multi-lingual
|
||||
* Ex: key: en value: monument
|
||||
*/
|
||||
private ArrayList<Map<String, String>> depictions;
|
||||
private boolean categoriesLoaded = false;
|
||||
private boolean categoriesPresent = false;
|
||||
private boolean depictionLoaded = false;
|
||||
private ViewTreeObserver.OnGlobalLayoutListener layoutListener; // for layout stuff, only used once!
|
||||
private ViewTreeObserver.OnScrollChangedListener scrollListener;
|
||||
|
||||
|
|
@ -187,6 +206,8 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
categoryNames = new ArrayList<>();
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_loading));
|
||||
|
||||
depictions = new ArrayList<>();
|
||||
|
||||
final View view = inflater.inflate(R.layout.fragment_media_detail, container, false);
|
||||
|
||||
ButterKnife.bind(this,view);
|
||||
|
|
@ -234,7 +255,7 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
desc.setHtmlText(media.getDescription());
|
||||
license.setText(media.getLicense());
|
||||
|
||||
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename())
|
||||
Disposable disposable = mediaDataExtractor.fetchMediaDetails(media.getFilename(), media.getPageId())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::setTextFields);
|
||||
|
|
@ -299,18 +320,33 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
coordinates.setText(prettyCoordinates(media));
|
||||
uploadedDate.setText(prettyUploadedDate(media));
|
||||
mediaDiscussion.setText(prettyDiscussion(media));
|
||||
if (prettyCaption(media).equals(getContext().getString(R.string.detail_caption_empty))) {
|
||||
captionLayout.setVisibility(GONE);
|
||||
} else mediaCaption.setText(prettyCaption(media));
|
||||
|
||||
|
||||
categoryNames.clear();
|
||||
categoryNames.addAll(media.getCategories());
|
||||
|
||||
depictions.clear();
|
||||
depictions.addAll(media.getDepiction());
|
||||
|
||||
depictionLoaded = true;
|
||||
|
||||
categoriesLoaded = true;
|
||||
categoriesPresent = (categoryNames.size() > 0);
|
||||
if (!categoriesPresent) {
|
||||
// Stick in a filler element.
|
||||
categoryNames.add(getString(R.string.detail_panel_cats_none));
|
||||
}
|
||||
|
||||
rebuildCatList();
|
||||
|
||||
if(depictions != null && depictions.size() != 0) {
|
||||
rebuildDepictionList();
|
||||
}
|
||||
else depictsLayout.setVisibility(GONE);
|
||||
|
||||
if (media.getCreator() == null || media.getCreator().equals("")) {
|
||||
authorLayout.setVisibility(GONE);
|
||||
} else {
|
||||
|
|
@ -320,6 +356,19 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
checkDeletion(media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Populates media details fragment with depiction list
|
||||
*/
|
||||
private void rebuildDepictionList() {
|
||||
depictionContainer.removeAllViews();
|
||||
for (int i = 0; i<depictions.size(); i++) {
|
||||
String depictionName = depictions.get(i).get("label");
|
||||
String entityId = depictions.get(i).get("id");
|
||||
View depictLabel = buildDepictLabel(depictionName, entityId, depictionContainer);
|
||||
depictionContainer.addView(depictLabel);
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.mediaDetailLicense)
|
||||
public void onMediaDetailLicenceClicked(){
|
||||
String url = media.getLicenseUrl();
|
||||
|
|
@ -480,6 +529,26 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add view to depictions obtained also tapping on depictions should open the url
|
||||
*/
|
||||
private View buildDepictLabel(String depictionName, String entityId, LinearLayout depictionContainer) {
|
||||
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_depicts_item, depictionContainer, false);
|
||||
final CompatTextView textView = item.findViewById(R.id.media_detail_depicted_item_text);
|
||||
|
||||
textView.setText(depictionName);
|
||||
if (depictionLoaded) {
|
||||
item.setOnClickListener(view -> {
|
||||
DepictedItem depictedItem = new DepictedItem(depictionName, "", "", false, entityId);
|
||||
Intent intent = new Intent(getContext(), WikidataItemDetailsActivity.class);
|
||||
intent.putExtra("wikidataItemName", depictedItem.getDepictsLabel());
|
||||
intent.putExtra("entityId", depictedItem.getEntityId());
|
||||
getContext().startActivity(intent);
|
||||
});
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
private View buildCatLabel(final String catName, ViewGroup categoryContainer) {
|
||||
final View item = LayoutInflater.from(getContext()).inflate(R.layout.detail_category_item, categoryContainer, false);
|
||||
final CompatTextView textView = item.findViewById(R.id.mediaDetailCategoryItemText);
|
||||
|
|
@ -509,6 +578,21 @@ public class MediaDetailFragment extends CommonsDaggerSupportFragment {
|
|||
image.setAlpha(1.0f - scrollPercentage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns captions for media details
|
||||
*
|
||||
* @param media object of class media
|
||||
* @return caption as string
|
||||
*/
|
||||
private String prettyCaption(Media media) {
|
||||
String caption = media.getCaption().trim();
|
||||
if (caption.equals("")) {
|
||||
return getString(R.string.detail_caption_empty);
|
||||
} else {
|
||||
return caption;
|
||||
}
|
||||
}
|
||||
|
||||
private String prettyDescription(Media media) {
|
||||
// @todo use UI language when multilingual descs are available
|
||||
String desc = media.getDescription(locale.getLanguage()).trim();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
/**
|
||||
* Interface for interacting with Commons Structured Data related APIs
|
||||
*/
|
||||
public interface MediaDetailInterface {
|
||||
|
||||
/**
|
||||
* Fetches caption using file name
|
||||
*
|
||||
* @param filename name of the file to be used for fetching captions
|
||||
* Please note that languages=en does not have an impact on the languages returned. All captions are returned for all languages.
|
||||
*/
|
||||
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
|
||||
Observable<MediaDetailResponse> fetchStructuredDataByFilename(@Query("languages") String language, @Query("titles") String filename);
|
||||
|
||||
/**
|
||||
* Gets labels for Depictions using Entity Id from MediaWikiAPI
|
||||
*
|
||||
* @param entityId EntityId (Ex: Q81566) of the depict entity
|
||||
* @param language user's locale
|
||||
*/
|
||||
@GET("/w/api.php?format=json&action=wbgetentities&props=labels&languagefallback=1")
|
||||
Observable<JsonObject> getDepictions(@Query("ids") String entityId, @Query("languages") String language);
|
||||
|
||||
/**
|
||||
* Fetches caption using wikibaseIdentifier
|
||||
*
|
||||
* @param wikibaseIdentifier pageId for the media
|
||||
*/
|
||||
@GET("/w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1&sites=commonswiki")
|
||||
Observable<MediaDetailResponse> getCaptionForImage(@Query("languages") String language, @Query("ids") String wikibaseIdentifier);
|
||||
}
|
||||
|
|
@ -14,26 +14,20 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
import fr.free.nrw.commons.Media;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
import fr.free.nrw.commons.bookmarks.Bookmark;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesContentProvider;
|
||||
import fr.free.nrw.commons.bookmarks.pictures.BookmarkPicturesDao;
|
||||
import fr.free.nrw.commons.category.CategoryImagesCallback;
|
||||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
|
|
@ -41,6 +35,8 @@ import fr.free.nrw.commons.utils.ImageUtils;
|
|||
import fr.free.nrw.commons.utils.NetworkUtils;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
|
||||
|
|
@ -198,7 +194,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
*
|
||||
* @param m Media file to download
|
||||
*/
|
||||
private void downloadMedia(Media m) {
|
||||
public void downloadMedia(Media m) {
|
||||
String imageUrl = m.getImageUrl(), fileName = m.getFilename();
|
||||
|
||||
if (imageUrl == null
|
||||
|
|
@ -329,7 +325,7 @@ public class MediaDetailPagerFragment extends CommonsDaggerSupportFragment imple
|
|||
Timber.d("Returning as activity is destroyed!");
|
||||
return;
|
||||
}
|
||||
if (i+1 >= adapter.getCount())
|
||||
if (i+1 >= adapter.getCount() && getContext() instanceof CategoryImagesCallback)
|
||||
((CategoryImagesCallback) getContext()).requestMoreImages();
|
||||
|
||||
getActivity().invalidateOptionsMenu();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
package fr.free.nrw.commons.media;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Model class for object while fetching structured data
|
||||
*/
|
||||
public class MediaDetailResponse {
|
||||
|
||||
@SerializedName("entities")
|
||||
private Map<String, CommonsWikibaseItem> entities;
|
||||
@SerializedName("success")
|
||||
private Integer success;
|
||||
|
||||
/**
|
||||
* No args constructor for use in serialization
|
||||
*/
|
||||
public MediaDetailResponse() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param success
|
||||
* @param entities
|
||||
*/
|
||||
public MediaDetailResponse(Map<String, CommonsWikibaseItem> entities, Integer success) {
|
||||
super();
|
||||
this.entities = entities;
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public Map<String, CommonsWikibaseItem> getEntities() {
|
||||
return entities;
|
||||
}
|
||||
|
||||
|
||||
public Integer getSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(Integer success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
|||
|
||||
import java.util.Map;
|
||||
|
||||
import fr.free.nrw.commons.depictions.models.DepictionResponse;
|
||||
import io.reactivex.Observable;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
|
@ -83,4 +84,23 @@ public interface MediaInterface {
|
|||
|
||||
@GET("w/api.php?format=json&action=parse&prop=text")
|
||||
Observable<MwParseResponse> getPageHtml(@Query("page") String title);
|
||||
|
||||
/**
|
||||
* Fetches caption using file name
|
||||
*
|
||||
* @param filename name of the file to be used for fetching captions
|
||||
* */
|
||||
@GET("w/api.php?action=wbgetentities&props=labels&format=json&languagefallback=1")
|
||||
Observable<MwQueryResponse> fetchCaptionByFilename(@Query("language") String language, @Query("titles") String filename);
|
||||
|
||||
/**
|
||||
* Fetches list of images from a depiction entity
|
||||
*
|
||||
* @param query depictionEntityId
|
||||
* @param sroffset number od depictions already fetched, this is useful in implementing pagination
|
||||
*/
|
||||
|
||||
@GET("w/api.php?action=query&list=search&format=json&srnamespace=6")
|
||||
Observable<DepictionResponse> fetchImagesForDepictedItem(@Query("srsearch") String query, @Query("sroffset") String sroffset);
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,8 @@ import androidx.annotation.NonNull;
|
|||
import com.google.gson.Gson;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -19,11 +21,14 @@ import javax.inject.Singleton;
|
|||
import fr.free.nrw.commons.achievements.FeaturedImages;
|
||||
import fr.free.nrw.commons.achievements.FeedbackResponse;
|
||||
import fr.free.nrw.commons.campaigns.CampaignResponseDTO;
|
||||
import fr.free.nrw.commons.depictions.SubClass.models.Binding;
|
||||
import fr.free.nrw.commons.depictions.SubClass.models.SparqlQueryResponse;
|
||||
import fr.free.nrw.commons.location.LatLng;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.nearby.model.NearbyResponse;
|
||||
import fr.free.nrw.commons.nearby.model.NearbyResultItem;
|
||||
import fr.free.nrw.commons.upload.FileUtils;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse;
|
||||
import io.reactivex.Observable;
|
||||
|
|
@ -208,6 +213,94 @@ public class OkHttpJsonApiClient {
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item.
|
||||
* Example: bridge -> suspended bridge, aqueduct, etc
|
||||
*/
|
||||
public Observable<ArrayList<DepictedItem>> getChildQIDs(String qid) throws IOException {
|
||||
String queryString = FileUtils.readFromResource("/queries/subclasses_query.rq");
|
||||
String query = queryString.
|
||||
replace("${QID}", qid)
|
||||
.replace("${LANG}", "\""+Locale.getDefault().getLanguage()+"\"");
|
||||
Timber.e(query);
|
||||
HttpUrl.Builder urlBuilder = HttpUrl
|
||||
.parse(sparqlQueryUrl)
|
||||
.newBuilder()
|
||||
.addQueryParameter("query", query)
|
||||
.addQueryParameter("format", "json");
|
||||
Request request = new Request.Builder()
|
||||
.url(urlBuilder.build())
|
||||
.build();
|
||||
return Observable.fromCallable(() -> {
|
||||
Response response = okHttpClient.newCall(request).execute();
|
||||
String json = response.body().string();
|
||||
SparqlQueryResponse example = gson.fromJson(json, SparqlQueryResponse.class);
|
||||
List<Binding> bindings = example.getResults().getBindings();
|
||||
ArrayList<DepictedItem> subItems = new ArrayList<>();
|
||||
for (Binding binding : bindings) {
|
||||
if (binding.getSubclassLabel().getXmlLang() != null) {
|
||||
String label = binding.getSubclassLabel().getValue();
|
||||
String entityId = binding.getSubclass().getValue();
|
||||
entityId = entityId.substring(entityId.lastIndexOf("/") - 1);
|
||||
subItems.add(new DepictedItem(label, "", "", false,entityId ));
|
||||
Timber.e(label);
|
||||
}
|
||||
}
|
||||
return subItems;
|
||||
}).doOnError(throwable -> {
|
||||
Timber.e(throwable.toString());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the QIDs of all Wikidata items that are subclasses of the given Wikidata item.
|
||||
* Example: bridge -> suspended bridge, aqueduct, etc
|
||||
*/
|
||||
public Observable<ArrayList<DepictedItem>> getParentQIDs(String qid) throws IOException {
|
||||
String queryString = FileUtils.readFromResource("/queries/parentclasses_query.rq");
|
||||
String query = queryString.
|
||||
replace("${QID}", qid)
|
||||
.replace("${LANG}", "\""+Locale.getDefault().getLanguage()+"\"");
|
||||
Timber.e(query);
|
||||
HttpUrl.Builder urlBuilder = HttpUrl
|
||||
.parse(sparqlQueryUrl)
|
||||
.newBuilder()
|
||||
.addQueryParameter("query", query)
|
||||
.addQueryParameter("format", "json");
|
||||
Request request = new Request.Builder()
|
||||
.url(urlBuilder.build())
|
||||
.build();
|
||||
return Observable.fromCallable(() -> {
|
||||
Response response = okHttpClient.newCall(request).execute();
|
||||
try {
|
||||
String json = response.body().string();
|
||||
JSONObject jsonObject = new JSONObject(json);
|
||||
ArrayList<DepictedItem> subItems = new ArrayList<>();
|
||||
JSONObject results = (JSONObject) jsonObject.get("results");
|
||||
JSONArray bindings = (JSONArray) results.get("bindings");
|
||||
for (int i = 0; i < bindings.length(); i++) {
|
||||
Timber.e(bindings.get(i).getClass().toString());
|
||||
JSONObject object = (JSONObject) bindings.get(i);
|
||||
JSONObject parentClassLabel = (JSONObject) object.get("parentClassLabel");
|
||||
if (parentClassLabel.get("value") != null) {
|
||||
String labelString = parentClassLabel.getString("value");
|
||||
JSONObject parentClass = (JSONObject) object.get("parentClass");
|
||||
if (parentClass.get("value") != null) {
|
||||
String entityId = parentClass.getString("value");
|
||||
entityId = entityId.substring(entityId.lastIndexOf("/") + 1);
|
||||
subItems.add(new DepictedItem(labelString, "", "", false, entityId));
|
||||
}
|
||||
}
|
||||
}
|
||||
return subItems;
|
||||
} catch (Exception e) {
|
||||
return new ArrayList<DepictedItem>();
|
||||
}
|
||||
}).doOnError(throwable -> {
|
||||
Timber.e("line578"+throwable.toString());
|
||||
});
|
||||
}
|
||||
|
||||
public Single<CampaignResponseDTO> getCampaigns() {
|
||||
return Single.fromCallable(() -> {
|
||||
Request request = new Request.Builder().url(campaignsUrl)
|
||||
|
|
@ -223,25 +316,4 @@ public class OkHttpJsonApiClient {
|
|||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whenever imageInfo is fetched, these common properties can be specified for the API call
|
||||
* https://www.mediawiki.org/wiki/API:Imageinfo
|
||||
*
|
||||
* @param builder
|
||||
* @return
|
||||
*/
|
||||
private HttpUrl.Builder appendMediaProperties(HttpUrl.Builder builder) {
|
||||
builder.addQueryParameter("prop", "imageinfo")
|
||||
.addQueryParameter("iiprop", "url|extmetadata")
|
||||
.addQueryParameter("iiurlwidth", THUMB_SIZE)
|
||||
.addQueryParameter("iiextmetadatafilter", "DateTime|Categories|GPSLatitude|GPSLongitude|ImageDescription|DateTimeOriginal|Artist|LicenseShortName|LicenseUrl");
|
||||
|
||||
String language = Locale.getDefault().getLanguage();
|
||||
if (!StringUtils.isBlank(language)) {
|
||||
builder.addQueryParameter("iiextmetadatalanguage", language);
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ import fr.free.nrw.commons.upload.SimilarImageInterface;
|
|||
import fr.free.nrw.commons.upload.UploadController;
|
||||
import fr.free.nrw.commons.upload.UploadModel;
|
||||
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
|
|
@ -33,16 +36,17 @@ public class UploadRemoteDataSource {
|
|||
private UploadModel uploadModel;
|
||||
private UploadController uploadController;
|
||||
private CategoriesModel categoriesModel;
|
||||
private DepictModel depictModel;
|
||||
private NearbyPlaces nearbyPlaces;
|
||||
|
||||
@Inject
|
||||
public UploadRemoteDataSource(UploadModel uploadModel, UploadController uploadController,
|
||||
CategoriesModel categoriesModel,
|
||||
NearbyPlaces nearbyPlaces) {
|
||||
CategoriesModel categoriesModel, NearbyPlaces nearbyPlaces, DepictModel depictModel) {
|
||||
this.uploadModel = uploadModel;
|
||||
this.uploadController = uploadController;
|
||||
this.categoriesModel = categoriesModel;
|
||||
this.nearbyPlaces = nearbyPlaces;
|
||||
this.depictModel = depictModel;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -203,4 +207,39 @@ public class UploadRemoteDataSource {
|
|||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handles category selection/unselection
|
||||
* @param depictedItem
|
||||
*/
|
||||
|
||||
public void onDepictedItemClicked(DepictedItem depictedItem) {
|
||||
depictModel.onDepictItemClicked(depictedItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* returns the list of selected depictions
|
||||
* @return
|
||||
*/
|
||||
|
||||
public List<DepictedItem> getSelectedDepictions() {
|
||||
return depictModel.getSelectedDepictions();
|
||||
}
|
||||
|
||||
/**
|
||||
* get all depictions
|
||||
*/
|
||||
|
||||
public Observable<DepictedItem> searchAllEntities(String query, List<String> imageTitleList) {
|
||||
return depictModel.searchAllEntities(query, imageTitleList);
|
||||
}
|
||||
|
||||
public void setSelectedDepictions(List<String> selectedDepictions) {
|
||||
uploadModel.setSelectedDepictions(selectedDepictions);
|
||||
}
|
||||
|
||||
public List<String> depictionsEntityIdList() {
|
||||
return depictModel.depictionsEntityIdList();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ import fr.free.nrw.commons.filepicker.UploadableFile;
|
|||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.upload.SimilarImageInterface;
|
||||
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
|
||||
|
|
@ -112,6 +114,10 @@ public class UploadRepository {
|
|||
remoteDataSource.setSelectedCategories(categoryStringList);
|
||||
}
|
||||
|
||||
public void setSelectedDepictions(List<String> selectedDepictions) {
|
||||
remoteDataSource.setSelectedDepictions(selectedDepictions);
|
||||
}
|
||||
|
||||
/**
|
||||
* handles the category selection/deselection
|
||||
*
|
||||
|
|
@ -263,6 +269,36 @@ public class UploadRepository {
|
|||
localDataSource.setSelectedLicense(licenseName);
|
||||
}
|
||||
|
||||
public void onDepictItemClicked(DepictedItem depictedItem) {
|
||||
remoteDataSource.onDepictedItemClicked(depictedItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches and returns the selected depictions for the current upload
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
|
||||
public List<DepictedItem> getSelectedDepictions() {
|
||||
return remoteDataSource.getSelectedDepictions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Search all depictions from
|
||||
*
|
||||
* @param query
|
||||
* @param imageTitleList
|
||||
* @return
|
||||
*/
|
||||
|
||||
public Observable<DepictedItem> searchAllEntities(String query, List<String> imageTitleList) {
|
||||
return remoteDataSource.searchAllEntities(query, imageTitleList);
|
||||
}
|
||||
|
||||
public List<String> getDepictionsEntityIdList() {
|
||||
return remoteDataSource.depictionsEntityIdList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns nearest place matching the passed latitude and longitude
|
||||
* @param decLatitude
|
||||
|
|
@ -272,4 +308,5 @@ public class UploadRepository {
|
|||
public Place checkNearbyPlaces(double decLatitude, double decLongitude) {
|
||||
return remoteDataSource.getNearbyPlaces(decLatitude, decLongitude);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@ import android.content.Context;
|
|||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
|
|
@ -14,7 +16,7 @@ import fr.free.nrw.commons.utils.ImageUtilsWrapper;
|
|||
import io.reactivex.Single;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
||||
|
||||
|
|
@ -46,7 +48,7 @@ public class ImageProcessingService {
|
|||
* - checks duplicate image
|
||||
* - checks dark image
|
||||
* - checks geolocation for image
|
||||
* - check for valid title
|
||||
* - check for valid caption
|
||||
*/
|
||||
Single<Integer> validateImage(UploadModel.UploadItem uploadItem, boolean checkTitle) {
|
||||
int currentImageQuality = uploadItem.getImageQuality();
|
||||
|
|
@ -95,18 +97,18 @@ public class ImageProcessingService {
|
|||
|
||||
|
||||
/**
|
||||
* Checks item title
|
||||
* - empty title
|
||||
* - existing title
|
||||
* Checks item caption
|
||||
* - empty caption
|
||||
* - existing caption
|
||||
*
|
||||
* @param uploadItem
|
||||
* @return
|
||||
*/
|
||||
private Single<Integer> validateItemTitle(UploadModel.UploadItem uploadItem) {
|
||||
Timber.d("Checking for image title %s", uploadItem.getTitle());
|
||||
Title title = uploadItem.getTitle();
|
||||
if (title.isEmpty()) {
|
||||
return Single.just(EMPTY_TITLE);
|
||||
Timber.d("Checking for image title %s", uploadItem.getUploadMediaDetails());
|
||||
List<UploadMediaDetail> captions = uploadItem.getUploadMediaDetails();
|
||||
if (captions.isEmpty()) {
|
||||
return Single.just(EMPTY_CAPTION);
|
||||
}
|
||||
|
||||
return mediaClient.checkPageExistsUsingTitle("File:" + uploadItem.getFileName())
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import android.view.ViewGroup;
|
|||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
|
@ -122,9 +123,11 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
|
|||
|
||||
public class ViewHolder {
|
||||
|
||||
@Nullable
|
||||
@BindView(R.id.tv_language)
|
||||
TextView tvLanguage;
|
||||
|
||||
@Nullable
|
||||
@BindView(R.id.view)
|
||||
View view;
|
||||
|
||||
|
|
@ -136,8 +139,17 @@ public class SpinnerLanguagesAdapter extends ArrayAdapter {
|
|||
String languageCode = LangCodeUtils.fixLanguageCode(languageCodesList.get(position));
|
||||
final String languageName = StringUtils.capitalize(languageNamesList.get(position));
|
||||
|
||||
if(TextUtils.isEmpty(savedLanguageValue)){
|
||||
savedLanguageValue = Locale.getDefault().getLanguage();
|
||||
}
|
||||
|
||||
if (!isDropDownView) {
|
||||
view.setVisibility(View.GONE);
|
||||
if( !dropDownClicked && savedLanguageValue !=null){
|
||||
languageCode = LangCodeUtils.fixLanguageCode(savedLanguageValue);
|
||||
}
|
||||
if (view != null) {
|
||||
view.setVisibility(View.GONE);
|
||||
}
|
||||
if (languageCode.length() > 2)
|
||||
tvLanguage.setText(languageCode.substring(0, 2));
|
||||
else
|
||||
|
|
|
|||
|
|
@ -44,9 +44,11 @@ import fr.free.nrw.commons.mwapi.UserClient;
|
|||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.theme.BaseActivity;
|
||||
import fr.free.nrw.commons.upload.categories.UploadCategoriesFragment;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsFragment;
|
||||
import fr.free.nrw.commons.upload.license.MediaLicenseFragment;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailFragment.UploadMediaDetailFragmentCallback;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictModel;
|
||||
import fr.free.nrw.commons.utils.PermissionUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
|
|
@ -102,6 +104,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
|||
private UploadImageAdapter uploadImagesAdapter;
|
||||
private List<Fragment> fragments;
|
||||
private UploadCategoriesFragment uploadCategoriesFragment;
|
||||
private DepictsFragment depictsFragment;
|
||||
private MediaLicenseFragment mediaLicenseFragment;
|
||||
private ThumbnailsAdapter thumbnailsAdapter;
|
||||
|
||||
|
|
@ -288,6 +291,7 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
|||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
|
@ -357,12 +361,17 @@ public class UploadActivity extends BaseActivity implements UploadContract.View,
|
|||
}
|
||||
|
||||
uploadCategoriesFragment = new UploadCategoriesFragment();
|
||||
uploadCategoriesFragment.setMediaDetailList(presenter.getImageDetailList());
|
||||
uploadCategoriesFragment.setCallback(this);
|
||||
|
||||
depictsFragment = new DepictsFragment();
|
||||
depictsFragment.setMediaDetailList(presenter.getImageDetailList());
|
||||
depictsFragment.setCallback(this);
|
||||
|
||||
mediaLicenseFragment = new MediaLicenseFragment();
|
||||
mediaLicenseFragment.setCallback(this);
|
||||
|
||||
|
||||
fragments.add(depictsFragment);
|
||||
fragments.add(uploadCategoriesFragment);
|
||||
fragments.add(mediaLicenseFragment);
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ public interface UploadContract {
|
|||
|
||||
void handleSubmit();
|
||||
|
||||
List<UploadMediaDetail> getImageDetailList();
|
||||
|
||||
void deletePictureAtIndex(int index);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -131,6 +131,10 @@ public class UploadController {
|
|||
contribution.setDescription("");
|
||||
}
|
||||
|
||||
if (contribution.getCaption() == null) {
|
||||
contribution.setCaption("");
|
||||
}
|
||||
|
||||
String license = store.getString(Prefs.DEFAULT_LICENSE, Prefs.Licenses.CC_BY_SA_3);
|
||||
contribution.setLicense(license);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import com.pedrogomez.renderers.ListAdapteeCollection;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
import com.pedrogomez.renderers.RendererBuilder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.UploadDepictsCallback;
|
||||
|
||||
/**
|
||||
* Adapter Factory for DepictsClicked Listener
|
||||
*/
|
||||
|
||||
public class UploadDepictsAdapterFactory {
|
||||
private final UploadDepictsCallback listener;
|
||||
|
||||
public UploadDepictsAdapterFactory(UploadDepictsCallback listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
public RVRendererAdapter<DepictedItem> create(List<DepictedItem> itemList) {
|
||||
RendererBuilder<DepictedItem> builder = new RendererBuilder<DepictedItem>()
|
||||
.bind(DepictedItem.class, new UploadDepictsRenderer(listener));
|
||||
ListAdapteeCollection<DepictedItem> collection = new ListAdapteeCollection<>(
|
||||
itemList != null ? itemList : Collections.emptyList());
|
||||
return new RVRendererAdapter<>(builder, collection);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.facebook.common.executors.CallerThreadExecutor;
|
||||
import com.facebook.common.references.CloseableReference;
|
||||
import com.facebook.datasource.DataSource;
|
||||
import com.facebook.drawee.backends.pipeline.Fresco;
|
||||
import com.facebook.imagepipeline.core.ImagePipeline;
|
||||
import com.facebook.imagepipeline.datasource.BaseBitmapDataSubscriber;
|
||||
import com.facebook.imagepipeline.image.CloseableImage;
|
||||
import com.facebook.imagepipeline.request.ImageRequest;
|
||||
import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
||||
import com.pedrogomez.renderers.Renderer;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.UploadDepictsCallback;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Depicts Renderer for setting up inflating layout,
|
||||
* and setting views for the layout of each depicted Item
|
||||
*/
|
||||
public class UploadDepictsRenderer extends Renderer<DepictedItem> {
|
||||
private final UploadDepictsCallback listener;
|
||||
@BindView(R.id.depict_checkbox)
|
||||
CheckBox checkedView;
|
||||
@BindView(R.id.depicts_label)
|
||||
TextView depictsLabel;
|
||||
@BindView(R.id.description) TextView description;
|
||||
@BindView(R.id.depicted_image)
|
||||
ImageView imageView;
|
||||
private final static String NO_IMAGE_FOR_DEPICTION="No Image for Depiction";
|
||||
|
||||
public UploadDepictsRenderer(UploadDepictsCallback listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpView(View rootView) {
|
||||
ButterKnife.bind(this, rootView);
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup OnClicklisteners on the views
|
||||
*/
|
||||
@Override
|
||||
protected void hookListeners(View rootView) {
|
||||
rootView.setOnClickListener(v -> {
|
||||
DepictedItem item = getContent();
|
||||
item.setSelected(!item.isSelected());
|
||||
checkedView.setChecked(item.isSelected());
|
||||
if (listener != null) {
|
||||
listener.depictsClicked(item);
|
||||
}
|
||||
});
|
||||
checkedView.setOnClickListener(v -> {
|
||||
DepictedItem item = getContent();
|
||||
item.setSelected(!item.isSelected());
|
||||
checkedView.setChecked(item.isSelected());
|
||||
if (listener != null) {
|
||||
listener.depictsClicked(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
|
||||
return inflater.inflate(R.layout.layout_upload_depicts_item, parent, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* initialise views for every item in the adapter
|
||||
*/
|
||||
@Override
|
||||
public void render() {
|
||||
DepictedItem item = getContent();
|
||||
checkedView.setChecked(item.isSelected());
|
||||
depictsLabel.setText(item.getDepictsLabel());
|
||||
description.setText(item.getDescription());
|
||||
if (!TextUtils.isEmpty(item.getImageUrl())) {
|
||||
if (!item.getImageUrl().equals(NO_IMAGE_FOR_DEPICTION))
|
||||
setImageView(Uri.parse(item.getImageUrl()), imageView);
|
||||
}else{
|
||||
listener.fetchThumbnailUrlForEntity(item.getEntityId(),item.getPosition());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set thumbnail for the depicted item
|
||||
*/
|
||||
private void setImageView(Uri imageUrl, ImageView imageView) {
|
||||
ImageRequest imageRequest = ImageRequestBuilder
|
||||
.newBuilderWithSource(imageUrl)
|
||||
.setAutoRotateEnabled(true)
|
||||
.build();
|
||||
|
||||
ImagePipeline imagePipeline = Fresco.getImagePipeline();
|
||||
final DataSource<CloseableReference<CloseableImage>>
|
||||
dataSource = imagePipeline.fetchDecodedImage(imageRequest, getContext());
|
||||
|
||||
dataSource.subscribe(new BaseBitmapDataSubscriber() {
|
||||
|
||||
@Override
|
||||
public void onNewResultImpl(@Nullable Bitmap bitmap) {
|
||||
if (dataSource.isFinished() && bitmap != null) {
|
||||
Timber.d("Bitmap loaded from url %s", imageUrl.toString());
|
||||
imageView.post(() -> imageView.setImageBitmap(Bitmap.createBitmap(bitmap)));
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailureImpl(DataSource dataSource) {
|
||||
Timber.d("Error getting bitmap from image url %s", imageUrl.toString());
|
||||
if (dataSource != null) {
|
||||
dataSource.close();
|
||||
}
|
||||
}
|
||||
}, CallerThreadExecutor.getInstance());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Holds a description of an item being uploaded by {@link UploadActivity}
|
||||
*/
|
||||
public class UploadMediaDetail {
|
||||
|
||||
private String languageCode;
|
||||
private String descriptionText;
|
||||
public String captionText;
|
||||
private int selectedLanguageIndex = -1;
|
||||
private boolean isManuallyAdded=false;
|
||||
|
||||
/**
|
||||
* Formatting captions to the Wikibase format for sending labels
|
||||
* @param uploadMediaDetails list of media Details
|
||||
*/
|
||||
|
||||
public static HashMap<String, String> formatCaptions(List<UploadMediaDetail> uploadMediaDetails) {
|
||||
HashMap<String, String> caption = new HashMap<>();
|
||||
for (UploadMediaDetail uploadMediaDetail : uploadMediaDetails) {
|
||||
caption.put(uploadMediaDetail.getLanguageCode(),uploadMediaDetail.getCaptionText());
|
||||
}
|
||||
return caption;
|
||||
}
|
||||
|
||||
public String getCaptionText() {
|
||||
return captionText;
|
||||
}
|
||||
|
||||
public void setCaptionText(String captionText) {
|
||||
this.captionText = captionText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The language code ie. "en" or "fr"
|
||||
*/
|
||||
String getLanguageCode() {
|
||||
return languageCode;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param languageCode The language code ie. "en" or "fr"
|
||||
*/
|
||||
public void setLanguageCode(String languageCode) {
|
||||
this.languageCode = languageCode;
|
||||
}
|
||||
|
||||
String getDescriptionText() {
|
||||
return descriptionText;
|
||||
}
|
||||
|
||||
public void setDescriptionText(String descriptionText) {
|
||||
this.descriptionText = descriptionText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
|
||||
*/
|
||||
int getSelectedLanguageIndex() {
|
||||
return selectedLanguageIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param selectedLanguageIndex the index of the language selected in a spinner with {@link SpinnerLanguagesAdapter}
|
||||
*/
|
||||
void setSelectedLanguageIndex(int selectedLanguageIndex) {
|
||||
this.selectedLanguageIndex = selectedLanguageIndex;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns if the description was added manually (by the user, or we have added it programaticallly)
|
||||
* @return
|
||||
*/
|
||||
public boolean isManuallyAdded() {
|
||||
return isManuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets to true if the description was manually added by the user
|
||||
* @param manuallyAdded
|
||||
*/
|
||||
public void setManuallyAdded(boolean manuallyAdded) {
|
||||
isManuallyAdded = manuallyAdded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the list of descriptions into the format Commons requires for uploads.
|
||||
*
|
||||
* @param descriptions the list of descriptions, description is ignored if text is null.
|
||||
* @return a string with the pattern of {{en|1=descriptionText}}
|
||||
*/
|
||||
static String formatList(List<UploadMediaDetail> descriptions) {
|
||||
StringBuilder descListString = new StringBuilder();
|
||||
for (UploadMediaDetail description : descriptions) {
|
||||
if (!description.isEmpty()) {
|
||||
String individualDescription = String.format("{{%s|1=%s}}", description.getLanguageCode(),
|
||||
description.getDescriptionText());
|
||||
descListString.append(individualDescription);
|
||||
}
|
||||
}
|
||||
return descListString.toString();
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return descriptionText == null || descriptionText.isEmpty();
|
||||
}
|
||||
}
|
||||
|
|
@ -2,7 +2,9 @@ package fr.free.nrw.commons.upload;
|
|||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
|
|
@ -27,16 +29,17 @@ import fr.free.nrw.commons.utils.AbstractTextWatcher;
|
|||
import fr.free.nrw.commons.utils.BiMap;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapter.ViewHolder> {
|
||||
public class UploadMediaDetailAdapter extends RecyclerView.Adapter<UploadMediaDetailAdapter.ViewHolder> {
|
||||
|
||||
private List<Description> descriptions;
|
||||
private List<UploadMediaDetail> uploadMediaDetails;
|
||||
private Callback callback;
|
||||
private EventListener eventListener;
|
||||
|
||||
private BiMap<AdapterView, String> selectedLanguages;
|
||||
private String savedLanguageValue;
|
||||
|
||||
public DescriptionsAdapter(String savedLanguageValue) {
|
||||
descriptions = new ArrayList<>();
|
||||
public UploadMediaDetailAdapter() {
|
||||
uploadMediaDetails = new ArrayList<>();
|
||||
selectedLanguages = new BiMap<>();
|
||||
this.savedLanguageValue = savedLanguageValue;
|
||||
}
|
||||
|
|
@ -45,8 +48,12 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
this.callback = callback;
|
||||
}
|
||||
|
||||
public void setItems(List<Description> descriptions) {
|
||||
this.descriptions = descriptions;
|
||||
public void setEventListener(EventListener eventListener) {
|
||||
this.eventListener = eventListener;
|
||||
}
|
||||
|
||||
public void setItems(List<UploadMediaDetail> uploadMediaDetails) {
|
||||
this.uploadMediaDetails = uploadMediaDetails;
|
||||
selectedLanguages = new BiMap<>();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
|
@ -65,7 +72,7 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return descriptions.size();
|
||||
return uploadMediaDetails.size();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -73,13 +80,13 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
*
|
||||
* @return List of descriptions
|
||||
*/
|
||||
public List<Description> getDescriptions() {
|
||||
return descriptions;
|
||||
public List<UploadMediaDetail> getUploadMediaDetails() {
|
||||
return uploadMediaDetails;
|
||||
}
|
||||
|
||||
public void addDescription(Description description) {
|
||||
this.descriptions.add(description);
|
||||
notifyItemInserted(descriptions.size());
|
||||
public void addDescription(UploadMediaDetail uploadMediaDetail) {
|
||||
this.uploadMediaDetails.add(uploadMediaDetail);
|
||||
//notifyItemInserted(uploadMediaDetails.size());
|
||||
}
|
||||
|
||||
public class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
|
@ -91,6 +98,9 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
@BindView(R.id.description_item_edit_text)
|
||||
AppCompatEditText descItemEditText;
|
||||
|
||||
@BindView(R.id.caption_item_edit_text)
|
||||
AppCompatEditText captionItemEditText;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
ButterKnife.bind(this, itemView);
|
||||
|
|
@ -98,14 +108,54 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
}
|
||||
|
||||
public void init(int position) {
|
||||
Description description = descriptions.get(position);
|
||||
Timber.d("Description is " + description);
|
||||
if (!TextUtils.isEmpty(description.getDescriptionText())) {
|
||||
descItemEditText.setText(description.getDescriptionText());
|
||||
UploadMediaDetail uploadMediaDetail = uploadMediaDetails.get(position);
|
||||
Timber.d("UploadMediaDetail is " + uploadMediaDetail);
|
||||
if (!TextUtils.isEmpty(uploadMediaDetail.getCaptionText())) {
|
||||
captionItemEditText.setText(uploadMediaDetail.getCaptionText());
|
||||
} else {
|
||||
captionItemEditText.setText("");
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(uploadMediaDetail.getDescriptionText())) {
|
||||
descItemEditText.setText(uploadMediaDetail.getDescriptionText());
|
||||
} else {
|
||||
descItemEditText.setText("");
|
||||
}
|
||||
|
||||
captionItemEditText.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
if (s.length() != 0) {
|
||||
eventListener.onEvent(true);
|
||||
} else eventListener.onEvent(false);
|
||||
}
|
||||
});
|
||||
|
||||
if (position == 0) {
|
||||
captionItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
|
||||
null);
|
||||
captionItemEditText.setOnTouchListener((v, event) -> {
|
||||
//2 is for drawable right
|
||||
if (event.getAction() == MotionEvent.ACTION_UP && (event.getRawX() >= (captionItemEditText.getRight() - captionItemEditText.getCompoundDrawables()[2].getBounds().width()))) {
|
||||
if (getAdapterPosition() == 0) {
|
||||
callback.showAlert(R.string.media_detail_caption,
|
||||
R.string.caption_info);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, getInfoIcon(),
|
||||
null);
|
||||
descItemEditText.setOnTouchListener((v, event) -> {
|
||||
|
|
@ -122,18 +172,25 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
});
|
||||
|
||||
} else {
|
||||
captionItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
|
||||
descItemEditText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
|
||||
}
|
||||
|
||||
captionItemEditText.addTextChangedListener(new AbstractTextWatcher(
|
||||
captionText -> uploadMediaDetails.get(position)
|
||||
.setCaptionText(captionText)));
|
||||
initLanguageSpinner(position, uploadMediaDetail);
|
||||
|
||||
descItemEditText.addTextChangedListener(new AbstractTextWatcher(
|
||||
descriptionText -> descriptions.get(position).setDescriptionText(descriptionText)));
|
||||
initLanguageSpinner(position, description);
|
||||
descriptionText -> uploadMediaDetails.get(position)
|
||||
.setDescriptionText(descriptionText)));
|
||||
initLanguageSpinner(position, uploadMediaDetail);
|
||||
|
||||
//If the description was manually added by the user, it deserves focus, if not, let the user decide
|
||||
if (description.isManuallyAdded()) {
|
||||
descItemEditText.requestFocus();
|
||||
if (uploadMediaDetail.isManuallyAdded()) {
|
||||
captionItemEditText.requestFocus();
|
||||
} else {
|
||||
descItemEditText.clearFocus();
|
||||
captionItemEditText.clearFocus();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +199,7 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
* @param position
|
||||
* @param description
|
||||
*/
|
||||
private void initLanguageSpinner(int position, Description description) {
|
||||
private void initLanguageSpinner(int position, UploadMediaDetail description) {
|
||||
SpinnerLanguagesAdapter languagesAdapter = new SpinnerLanguagesAdapter(
|
||||
spinnerDescriptionLanguages.getContext(),
|
||||
R.layout.row_item_languages_spinner, selectedLanguages,
|
||||
|
|
@ -205,6 +262,10 @@ public class DescriptionsAdapter extends RecyclerView.Adapter<DescriptionsAdapte
|
|||
void showAlert(int mediaDetailDescription, int descriptionInfo);
|
||||
}
|
||||
|
||||
public interface EventListener {
|
||||
void onEvent(Boolean data);
|
||||
}
|
||||
|
||||
/**
|
||||
* converts dp to pixel
|
||||
* @param dp
|
||||
|
|
@ -17,6 +17,15 @@ import javax.inject.Inject;
|
|||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import fr.free.nrw.commons.CommonsApplication;
|
||||
import fr.free.nrw.commons.Utils;
|
||||
import fr.free.nrw.commons.auth.SessionManager;
|
||||
|
|
@ -57,6 +66,7 @@ public class UploadModel {
|
|||
private FileProcessor fileProcessor;
|
||||
private final ImageProcessingService imageProcessingService;
|
||||
private List<String> selectedCategories;
|
||||
private ArrayList<String> selectedDepictions;
|
||||
|
||||
@Inject
|
||||
UploadModel(@Named("licenses") List<String> licenses,
|
||||
|
|
@ -86,6 +96,9 @@ public class UploadModel {
|
|||
if (this.selectedCategories != null) {
|
||||
this.selectedCategories.clear();
|
||||
}
|
||||
if (this.selectedDepictions != null) {
|
||||
this.selectedDepictions.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSelectedCategories(List<String> selectedCategories) {
|
||||
|
|
@ -108,7 +121,6 @@ public class UploadModel {
|
|||
similarImageInterface));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* pre process a one item at a time
|
||||
*/
|
||||
|
|
@ -146,11 +158,15 @@ public class UploadModel {
|
|||
createdTimestampSource);
|
||||
if (place != null) {
|
||||
uploadItem.title.setTitleText(place.name);
|
||||
if(uploadItem.descriptions.isEmpty()) {
|
||||
uploadItem.descriptions.add(new Description());
|
||||
if(uploadItem.uploadMediaDetails.isEmpty()) {
|
||||
uploadItem.uploadMediaDetails.add(new UploadMediaDetail());
|
||||
}
|
||||
uploadItem.descriptions.get(0).setDescriptionText(place.getLongDescription());
|
||||
uploadItem.descriptions.get(0).setLanguageCode("en");
|
||||
uploadItem.uploadMediaDetails.get(0).setDescriptionText(place.getLongDescription());
|
||||
uploadItem.uploadMediaDetails.get(0).setLanguageCode("en");
|
||||
String languageCode = Locale.getDefault().getLanguage();
|
||||
uploadItem.uploadMediaDetails.get(0).setDescriptionText(place.getLongDescription());
|
||||
uploadItem.uploadMediaDetails.get(0).setLanguageCode(languageCode);
|
||||
uploadItem.uploadMediaDetails.get(0).setCaptionText(place.name);
|
||||
}
|
||||
if (!items.contains(uploadItem)) {
|
||||
items.add(uploadItem);
|
||||
|
|
@ -191,16 +207,19 @@ public class UploadModel {
|
|||
return Observable.fromIterable(items).map(item ->
|
||||
{
|
||||
Contribution contribution = new Contribution(item.mediaUri, null,
|
||||
item.getFileName(),
|
||||
Description.formatList(item.descriptions), -1,
|
||||
item.getFileName(), item.uploadMediaDetails.size()!=0? UploadMediaDetail.formatCaptions(item.uploadMediaDetails):new HashMap<>(),
|
||||
UploadMediaDetail.formatList(item.uploadMediaDetails), -1,
|
||||
null, null, sessionManager.getAuthorName(),
|
||||
CommonsApplication.DEFAULT_EDIT_SUMMARY, item.gpsCoords.getCoords());
|
||||
CommonsApplication.DEFAULT_EDIT_SUMMARY, selectedDepictions, item.gpsCoords.getCoords());
|
||||
if (item.place != null) {
|
||||
contribution.setWikiDataEntityId(item.place.getWikiDataEntityId());
|
||||
}
|
||||
if (null == selectedCategories) {//Just a fail safe, this should never be null
|
||||
selectedCategories = new ArrayList<>();
|
||||
}
|
||||
if (selectedDepictions == null) {
|
||||
selectedDepictions = new ArrayList<>();
|
||||
}
|
||||
contribution.setCategories(selectedCategories);
|
||||
contribution.setTag("mimeType", item.mimeType);
|
||||
contribution.setSource(item.source);
|
||||
|
|
@ -238,10 +257,17 @@ public class UploadModel {
|
|||
|
||||
public void updateUploadItem(int index, UploadItem uploadItem) {
|
||||
UploadItem uploadItem1 = items.get(index);
|
||||
uploadItem1.setDescriptions(uploadItem.descriptions);
|
||||
uploadItem1.setMediaDetails(uploadItem.uploadMediaDetails);
|
||||
uploadItem1.setTitle(uploadItem.title);
|
||||
}
|
||||
|
||||
public void setSelectedDepictions(List<String> selectedDepictions) {
|
||||
if (null == selectedDepictions) {
|
||||
selectedDepictions = new ArrayList<>();
|
||||
}
|
||||
this.selectedDepictions = (ArrayList<String>) selectedDepictions;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WeakerAccess")
|
||||
public static class UploadItem {
|
||||
|
||||
|
|
@ -254,7 +280,7 @@ public class UploadModel {
|
|||
private boolean selected = false;
|
||||
private boolean first = false;
|
||||
private Title title;
|
||||
private List<Description> descriptions;
|
||||
private List<UploadMediaDetail> uploadMediaDetails;
|
||||
private Place place;
|
||||
private boolean visited;
|
||||
private boolean error;
|
||||
|
|
@ -271,7 +297,8 @@ public class UploadModel {
|
|||
this.originalContentUri = originalContentUri;
|
||||
this.createdTimestampSource = createdTimestampSource;
|
||||
title = new Title();
|
||||
descriptions = new ArrayList<>();
|
||||
uploadMediaDetails = new ArrayList<>();
|
||||
uploadMediaDetails.add(new UploadMediaDetail());
|
||||
this.place = place;
|
||||
this.mediaUri = mediaUri;
|
||||
this.mimeType = mimeType;
|
||||
|
|
@ -305,8 +332,8 @@ public class UploadModel {
|
|||
return first;
|
||||
}
|
||||
|
||||
public List<Description> getDescriptions() {
|
||||
return descriptions;
|
||||
public List<UploadMediaDetail> getUploadMediaDetails() {
|
||||
return uploadMediaDetails;
|
||||
}
|
||||
|
||||
public boolean isVisited() {
|
||||
|
|
@ -341,11 +368,6 @@ public class UploadModel {
|
|||
return MimeTypeMapWrapper.getExtensionFromMimeType(mimeType);
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return title
|
||||
!= null ? Utils.fixExtension(title.toString(), getFileExt()) : null;
|
||||
}
|
||||
|
||||
public Place getPlace() {
|
||||
return place;
|
||||
}
|
||||
|
|
@ -354,8 +376,9 @@ public class UploadModel {
|
|||
this.title = title;
|
||||
}
|
||||
|
||||
public void setDescriptions(List<Description> descriptions) {
|
||||
this.descriptions = descriptions;
|
||||
|
||||
public void setMediaDetails(List<UploadMediaDetail> uploadMediaDetails) {
|
||||
this.uploadMediaDetails = uploadMediaDetails;
|
||||
}
|
||||
|
||||
public Uri getContentUri() {
|
||||
|
|
@ -376,6 +399,14 @@ public class UploadModel {
|
|||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 uploadMediaDetails.get(0).getCaptionText();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,6 +4,8 @@ import dagger.Binds;
|
|||
import dagger.Module;
|
||||
import fr.free.nrw.commons.upload.categories.CategoriesContract;
|
||||
import fr.free.nrw.commons.upload.categories.CategoriesPresenter;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsContract;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsPresenter;
|
||||
import fr.free.nrw.commons.upload.license.MediaLicenseContract;
|
||||
import fr.free.nrw.commons.upload.license.MediaLicensePresenter;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.UploadMediaDetailsContract;
|
||||
|
|
@ -33,4 +35,9 @@ public abstract class UploadModule {
|
|||
UploadMediaPresenter
|
||||
presenter);
|
||||
|
||||
@Binds
|
||||
public abstract DepictsContract.UserActionListener bindsDepictsPresenter(
|
||||
DepictsPresenter
|
||||
presenter
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package fr.free.nrw.commons.upload;
|
|||
import android.annotation.SuppressLint;
|
||||
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
|
@ -83,13 +84,29 @@ public class UploadPresenter implements UploadContract.UserActionListener {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of UploadMedia Details
|
||||
* to be passed on to categories and depicts fragment
|
||||
*/
|
||||
|
||||
public List<UploadMediaDetail> getImageDetailList() {
|
||||
int titleListCount = 0;
|
||||
List<UploadMediaDetail> titleList = new ArrayList<>();
|
||||
for (UploadModel.UploadItem item : repository.getUploads()) {
|
||||
if (!item.getUploadMediaDetails().isEmpty()) {
|
||||
titleList.add(item.getUploadMediaDetails().get(titleListCount));
|
||||
titleListCount++;
|
||||
}
|
||||
}
|
||||
return titleList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deletePictureAtIndex(int index) {
|
||||
List<UploadableFile> uploadableFiles = view.getUploadableFiles();
|
||||
if (index == uploadableFiles.size() - 1) {//If the next fragment to be shown is not one of the MediaDetailsFragment, lets hide the top card
|
||||
view.showHideTopCard(false);
|
||||
}
|
||||
//Ask the repository to delete the picture
|
||||
repository.deletePicture(uploadableFiles.get(index).getFilePath());
|
||||
if (uploadableFiles.size() == 1) {
|
||||
view.showMessage(R.string.upload_cancelled);
|
||||
|
|
|
|||
|
|
@ -39,6 +39,14 @@ import io.reactivex.SingleObserver;
|
|||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import javax.inject.Inject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class UploadService extends HandlerService<Contribution> {
|
||||
|
|
@ -280,7 +288,15 @@ public class UploadService extends HandlerService<Contribution> {
|
|||
String canonicalFilename = "File:" + uploadResult.getFilename();
|
||||
Timber.d("Contribution upload success. Initiating Wikidata edit for entity id %s",
|
||||
contribution.getWikiDataEntityId());
|
||||
// to perform upload of depictions we pass on depiction entityId of the selected depictions to the wikidataEditService
|
||||
if (contribution.getDepictionsEntityIds() != null) {
|
||||
for (String s : contribution.getDepictionsEntityIds()) {
|
||||
wikidataEditService.createClaimWithLogging(s, canonicalFilename);
|
||||
}
|
||||
}
|
||||
wikidataEditService.createClaimWithLogging(contribution.getWikiDataEntityId(), canonicalFilename);
|
||||
wikidataEditService.createLabelforWikidataEntity(contribution.getWikiDataEntityId(), canonicalFilename,
|
||||
(Map) contribution.getCaptions());
|
||||
contribution.setFilename(canonicalFilename);
|
||||
contribution.setImageUrl(uploadResult.getImageinfo().getOriginalUrl());
|
||||
contribution.setState(Contribution.STATE_COMPLETED);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
package fr.free.nrw.commons.upload;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.wikipedia.dataclient.mwapi.MwPostResponse;
|
||||
import org.wikipedia.dataclient.mwapi.MwQueryResponse;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Headers;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
|
||||
|
||||
/**
|
||||
* Retrofit calls for managing responses network calls of entity ids required for uploading depictions
|
||||
*/
|
||||
|
||||
public interface WikiBaseInterface {
|
||||
|
||||
@Headers("Cache-Control: no-cache")
|
||||
@FormUrlEncoded
|
||||
@POST(MW_API_PREFIX + "action=wbeditentity")
|
||||
Observable<MwPostResponse> postEditEntity(@NonNull @Field("id") String fileEntityId,
|
||||
@NonNull @Field("token") String editToken,
|
||||
@NonNull @Field("data") String data);
|
||||
|
||||
@GET(MW_API_PREFIX + "action=query&prop=info")
|
||||
Observable<MwQueryResponse> getFileEntityId(@Query("titles") String fileName);
|
||||
|
||||
}
|
||||
|
|
@ -32,6 +32,7 @@ import fr.free.nrw.commons.category.CategoryClickedListener;
|
|||
import fr.free.nrw.commons.category.CategoryItem;
|
||||
import fr.free.nrw.commons.upload.UploadBaseFragment;
|
||||
import fr.free.nrw.commons.upload.UploadCategoriesAdapterFactory;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
|
|
@ -54,7 +55,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
|||
@Inject
|
||||
CategoriesContract.UserActionListener presenter;
|
||||
private RVRendererAdapter<CategoryItem> adapter;
|
||||
private List<String> mediaTitleList=new ArrayList<>();
|
||||
private List<UploadMediaDetail> mediaTitleList;
|
||||
private Disposable subscribe;
|
||||
private List<CategoryItem> categories;
|
||||
private boolean isVisible;
|
||||
|
|
@ -64,7 +65,7 @@ public class UploadCategoriesFragment extends UploadBaseFragment implements Cate
|
|||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
public void setMediaTitleList(List<String> mediaTitleList) {
|
||||
public void setMediaDetailList(List<UploadMediaDetail> mediaTitleList) {
|
||||
this.mediaTitleList = mediaTitleList;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
package fr.free.nrw.commons.upload.depicts;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import fr.free.nrw.commons.BasePresenter;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
|
||||
/**
|
||||
* The contract with which DepictsFragment and its presenter would talk to each other
|
||||
*/
|
||||
public interface DepictsContract {
|
||||
|
||||
interface View {
|
||||
/**
|
||||
* Go to category screen
|
||||
*/
|
||||
void goToNextScreen();
|
||||
|
||||
/**
|
||||
* Go to media detail screen
|
||||
*/
|
||||
void goToPreviousScreen();
|
||||
|
||||
/**
|
||||
* show error in case of no depiction selected
|
||||
*/
|
||||
void noDepictionSelected();
|
||||
|
||||
/**
|
||||
* Show progress/Hide progress depending on the boolean value
|
||||
*/
|
||||
void showProgress(boolean shouldShow);
|
||||
|
||||
/**
|
||||
* decides whether to show error values or not depending on the boolean value
|
||||
*/
|
||||
void showError(Boolean value);
|
||||
|
||||
/**
|
||||
* add depictions to list
|
||||
*/
|
||||
void setDepictsList(List<DepictedItem> depictedItemList);
|
||||
|
||||
/**
|
||||
* Set thumbnail image for depicted item
|
||||
*/
|
||||
void onImageUrlFetched(String response, int position);
|
||||
}
|
||||
|
||||
interface UserActionListener extends BasePresenter<View> {
|
||||
|
||||
/**
|
||||
* Takes to previous screen
|
||||
*/
|
||||
void onPreviousButtonClicked();
|
||||
|
||||
/**
|
||||
* Listener for the depicted items selected from the list
|
||||
*/
|
||||
void onDepictItemClicked(DepictedItem depictedItem);
|
||||
|
||||
/**
|
||||
* asks the repository to fetch depictions for the query
|
||||
* @param query
|
||||
*/
|
||||
void searchForDepictions(String query);
|
||||
|
||||
/**
|
||||
* Check if depictions were selected
|
||||
* from the depiction list
|
||||
*/
|
||||
void verifyDepictions();
|
||||
|
||||
/**
|
||||
* Fetch thumbnail for the Wikidata Item
|
||||
* @param entityId entityId of the item
|
||||
* @param position position of the item
|
||||
*/
|
||||
void fetchThumbnailForEntityId(String entityId, int position);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
package fr.free.nrw.commons.upload.depicts;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
import com.jakewharton.rxbinding2.view.RxView;
|
||||
import com.jakewharton.rxbinding2.widget.RxTextView;
|
||||
import com.pedrogomez.renderers.RVRendererAdapter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.upload.UploadBaseFragment;
|
||||
import fr.free.nrw.commons.upload.UploadDepictsAdapterFactory;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.UploadDepictsCallback;
|
||||
import fr.free.nrw.commons.utils.DialogUtil;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
/**
|
||||
* Fragment for showing depicted items list in Upload activity after media details
|
||||
*/
|
||||
public class DepictsFragment extends UploadBaseFragment implements DepictsContract.View, UploadDepictsCallback {
|
||||
|
||||
@BindView(R.id.depicts_title)
|
||||
TextView depictsTitle;
|
||||
@BindView(R.id.depicts_subtitle)
|
||||
TextView depictsSubtitile;
|
||||
@BindView(R.id.depicts_search_container)
|
||||
TextInputLayout depictsSearchContainer;
|
||||
@BindView(R.id.depicts_search)
|
||||
TextInputEditText depictsSearch;
|
||||
@BindView(R.id.depictsSearchInProgress)
|
||||
ProgressBar depictsSearchInProgress;
|
||||
@BindView(R.id.depicts_recycler_view)
|
||||
RecyclerView depictsRecyclerView;
|
||||
|
||||
@Inject
|
||||
DepictsContract.UserActionListener presenter;
|
||||
private RVRendererAdapter<DepictedItem> adapter;
|
||||
private List<UploadMediaDetail> mediaTitleList;
|
||||
private Disposable subscribe;
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public android.view.View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
|
||||
@Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.upload_depicts_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull android.view.View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
ButterKnife.bind(this, view);
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize presenter and views
|
||||
*/
|
||||
private void init() {
|
||||
depictsTitle.setText(getString(R.string.step_count, callback.getIndexInViewFlipper(this) + 1,
|
||||
callback.getTotalNumberOfSteps()));
|
||||
presenter.onAttachView(this);
|
||||
initRecyclerView();
|
||||
addTextChangeListenerToSearchBox();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise recyclerView and set adapter
|
||||
*/
|
||||
private void initRecyclerView() {
|
||||
adapter = new UploadDepictsAdapterFactory(this)
|
||||
.create(new ArrayList<>());
|
||||
depictsRecyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
depictsRecyclerView.setAdapter(adapter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToNextScreen() {
|
||||
callback.onNextButtonClicked(callback.getIndexInViewFlipper(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void goToPreviousScreen() {
|
||||
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void noDepictionSelected() {
|
||||
DialogUtil.showAlertDialog(getActivity(),
|
||||
getString(R.string.no_depictions_selected),
|
||||
getString(R.string.no_depictions_selected_warning_desc),
|
||||
getString(R.string.no_go_back),
|
||||
getString(R.string.yes_submit),
|
||||
null,
|
||||
() -> goToNextScreen());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
presenter.onDetachView();
|
||||
subscribe.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showProgress(boolean shouldShow) {
|
||||
depictsSearchInProgress.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showError(Boolean value) {
|
||||
if (value)
|
||||
depictsSearchContainer.setError(getString(R.string.no_depiction_found));
|
||||
else depictsSearchContainer.setErrorEnabled(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDepictsList(List<DepictedItem> depictedItemList) {
|
||||
adapter.clear();
|
||||
if (depictedItemList != null) {
|
||||
adapter.addAll(depictedItemList);
|
||||
adapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set thumbnail image for depicted item
|
||||
*/
|
||||
@Override
|
||||
public void onImageUrlFetched(String response, int position) {
|
||||
adapter.getItem(position).setImageUrl(response);
|
||||
adapter.notifyItemChanged(position);
|
||||
}
|
||||
|
||||
@OnClick(R.id.depicts_next)
|
||||
public void onNextButtonClicked() {
|
||||
presenter.verifyDepictions();
|
||||
}
|
||||
|
||||
@OnClick(R.id.depicts_previous)
|
||||
public void onPreviousButtonClicked() {
|
||||
callback.onPreviousButtonClicked(callback.getIndexInViewFlipper(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void depictsClicked(DepictedItem item) {
|
||||
presenter.onDepictItemClicked(item);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch thumbnail for the given entityId at the given position
|
||||
*/
|
||||
@Override
|
||||
public void fetchThumbnailUrlForEntity(String entityId, int position) {
|
||||
presenter.fetchThumbnailForEntityId(entityId,position);
|
||||
}
|
||||
|
||||
/**
|
||||
* Text change listener for the edit text view of depicts
|
||||
*/
|
||||
private void addTextChangeListenerToSearchBox() {
|
||||
subscribe = RxTextView.textChanges(depictsSearch)
|
||||
.doOnEach(v -> depictsSearchContainer.setError(null))
|
||||
.takeUntil(RxView.detaches(depictsSearch))
|
||||
.debounce(500, TimeUnit.MILLISECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(filter -> searchForDepictions(filter.toString()), Timber::e);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for depictions for the following query
|
||||
* @param query query string
|
||||
*/
|
||||
private void searchForDepictions(String query) {
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
presenter.searchForDepictions(query);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sets mediaList of UploadMediaDetail object
|
||||
*/
|
||||
public void setMediaDetailList(List<UploadMediaDetail> imageDetailList) {
|
||||
this.mediaTitleList = imageDetailList;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package fr.free.nrw.commons.upload.depicts;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import fr.free.nrw.commons.depictions.models.DepictionResponse;
|
||||
import fr.free.nrw.commons.wikidata.model.DepictSearchResponse;
|
||||
import io.reactivex.Observable;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
/**
|
||||
* Manges retrofit calls for Searching of depicts from DepictsFragment
|
||||
*/
|
||||
|
||||
public interface DepictsInterface {
|
||||
|
||||
/**
|
||||
* Search for depictions using the wbsearchentities API
|
||||
* @param query search for depictions based on user query
|
||||
* @param limit number of depictions to be retrieved
|
||||
* @param language current locale of the phone
|
||||
* @param uselang current locale of the phone
|
||||
* @param offset number of depictions already fetched useful in implementing pagination
|
||||
*/
|
||||
@GET("/w/api.php?action=wbsearchentities&format=json&type=item&uselang=en")
|
||||
Observable<DepictSearchResponse> searchForDepicts(@Query("search") String query, @Query("limit") String limit, @Query("language") String language, @Query("uselang") String uselang, @Query("continue") String offset);
|
||||
|
||||
@GET("/w/api.php?action=wbgetclaims&format=json&property=P18")
|
||||
Observable<JsonObject> getImageForEntity(@Query("entity") String entityId);
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
package fr.free.nrw.commons.upload.depicts;
|
||||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
||||
import fr.free.nrw.commons.repository.UploadRepository;
|
||||
import fr.free.nrw.commons.upload.UploadModel;
|
||||
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Scheduler;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import io.reactivex.disposables.Disposable;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* presenter for DepictsFragment
|
||||
*/
|
||||
@Singleton
|
||||
public class DepictsPresenter implements DepictsContract.UserActionListener {
|
||||
|
||||
private static final DepictsContract.View DUMMY = (DepictsContract.View) Proxy
|
||||
.newProxyInstance(
|
||||
DepictsContract.View.class.getClassLoader(),
|
||||
new Class[]{DepictsContract.View.class},
|
||||
(proxy, method, methodArgs) -> null);
|
||||
|
||||
|
||||
private final Scheduler ioScheduler;
|
||||
private final Scheduler mainThreadScheduler;
|
||||
private DepictsContract.View view = DUMMY;
|
||||
private UploadRepository repository;
|
||||
private DepictsClient depictsClient;
|
||||
private static int TIMEOUT_SECONDS = 15;
|
||||
|
||||
private CompositeDisposable compositeDisposable;
|
||||
|
||||
@Inject
|
||||
public DepictsPresenter(UploadRepository uploadRepository, @Named(IO_THREAD) Scheduler ioScheduler,
|
||||
@Named(MAIN_THREAD) Scheduler mainThreadScheduler, DepictsClient depictsClient) {
|
||||
this.repository = uploadRepository;
|
||||
this.ioScheduler = ioScheduler;
|
||||
this.mainThreadScheduler = mainThreadScheduler;
|
||||
this.depictsClient = depictsClient;
|
||||
compositeDisposable = new CompositeDisposable();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachView(DepictsContract.View view) {
|
||||
this.view = view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDetachView() {
|
||||
this.view = DUMMY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreviousButtonClicked() {
|
||||
view.goToPreviousScreen();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDepictItemClicked(DepictedItem depictedItem) {
|
||||
repository.onDepictItemClicked(depictedItem);
|
||||
}
|
||||
|
||||
/**
|
||||
* asks the repository to fetch depictions for the query
|
||||
* @param query
|
||||
*/
|
||||
@Override
|
||||
public void searchForDepictions(String query) {
|
||||
List<DepictedItem> depictedItemList = new ArrayList<>();
|
||||
List<String> imageTitleList = getImageTitleList();
|
||||
Observable<DepictedItem> distinctDepictsObservable = Observable
|
||||
.fromIterable(repository.getSelectedDepictions())
|
||||
.subscribeOn(ioScheduler)
|
||||
.observeOn(mainThreadScheduler)
|
||||
.doOnSubscribe(disposable -> {
|
||||
view.showError(true);
|
||||
view.showProgress(true);
|
||||
view.setDepictsList(null);
|
||||
})
|
||||
.observeOn(ioScheduler)
|
||||
.concatWith(
|
||||
repository.searchAllEntities(query, imageTitleList)
|
||||
)
|
||||
.distinct();
|
||||
|
||||
Disposable searchDepictsDisposable = distinctDepictsObservable
|
||||
.observeOn(mainThreadScheduler)
|
||||
.subscribe(
|
||||
s -> depictedItemList.add(s),
|
||||
Timber::e,
|
||||
() -> {
|
||||
view.showProgress(false);
|
||||
|
||||
if (null == depictedItemList || depictedItemList.isEmpty()) {
|
||||
view.showError(true);
|
||||
} else {
|
||||
view.showError(false);
|
||||
//Understand this is shitty, but yes, doing it the other way is even worse and adapter positions can not be trusted
|
||||
for (int position = 0; position < depictedItemList.size();
|
||||
position++) {
|
||||
//depictedItemList.get(position).setPosition(position);
|
||||
}
|
||||
view.setDepictsList(depictedItemList);
|
||||
}
|
||||
}
|
||||
);
|
||||
compositeDisposable.add(searchDepictsDisposable);
|
||||
view.setDepictsList(depictedItemList);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if depictions were selected
|
||||
* from the depiction list
|
||||
*/
|
||||
@Override
|
||||
public void verifyDepictions() {
|
||||
List<DepictedItem> selectedDepictions = repository.getSelectedDepictions();
|
||||
if (selectedDepictions != null && !selectedDepictions.isEmpty()) {
|
||||
repository.setSelectedDepictions(repository.getDepictionsEntityIdList());
|
||||
view.goToNextScreen();
|
||||
} else {
|
||||
view.noDepictionSelected();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch thumbnail for the Wikidata Item
|
||||
* @param entityId entityId of the item
|
||||
* @param position position of the item
|
||||
*/
|
||||
@Override
|
||||
public void fetchThumbnailForEntityId(String entityId, int position) {
|
||||
compositeDisposable.add(depictsClient.getP18ForItem(entityId)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
|
||||
.subscribe(response -> {
|
||||
view.onImageUrlFetched(response,position);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns image title list from UploadItem
|
||||
* @return
|
||||
*/
|
||||
private List<String> getImageTitleList() {
|
||||
List<String> titleList = new ArrayList<>();
|
||||
for (UploadModel.UploadItem item : repository.getUploads()) {
|
||||
if (item.getTitle().isSet()) {
|
||||
titleList.add(item.getTitle().toString());
|
||||
}
|
||||
}
|
||||
return titleList;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package fr.free.nrw.commons.upload.mediaDetails;
|
||||
|
||||
|
||||
import org.wikipedia.dataclient.mwapi.MwPostResponse;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
import retrofit2.http.Field;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
import static org.wikipedia.dataclient.Service.MW_API_PREFIX;
|
||||
|
||||
public interface CaptionInterface {
|
||||
|
||||
/**
|
||||
* Upload Captions for the image when upload is successful
|
||||
*
|
||||
* @param FileEntityId enityId for the uploaded file
|
||||
* @param editToken editToken for the file
|
||||
* @param captionValue value of the caption to be uploaded
|
||||
* @param caption additional data associated with caption
|
||||
*/
|
||||
@FormUrlEncoded
|
||||
@POST(MW_API_PREFIX + "action=wbsetlabel&language=en")
|
||||
Observable<MwPostResponse> addLabelstoWikidata(@Field("id") String FileEntityId,
|
||||
@Field("token") String editToken,
|
||||
@Field("value") String captionValue,
|
||||
@Field("data") Map<String, String> caption);
|
||||
}
|
||||
|
|
@ -44,7 +44,9 @@ 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.Description;
|
||||
import fr.free.nrw.commons.upload.DescriptionsAdapter;
|
||||
//import fr.free.nrw.commons.upload.DescriptionsAdapter;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetailAdapter;
|
||||
import fr.free.nrw.commons.upload.SimilarImageDialogFragment;
|
||||
import fr.free.nrw.commons.upload.Title;
|
||||
import fr.free.nrw.commons.upload.UploadBaseFragment;
|
||||
|
|
@ -59,7 +61,7 @@ import timber.log.Timber;
|
|||
import static fr.free.nrw.commons.utils.ImageUtils.getErrorMessageForResult;
|
||||
|
||||
public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
||||
UploadMediaDetailsContract.View {
|
||||
UploadMediaDetailsContract.View, UploadMediaDetailAdapter.EventListener {
|
||||
|
||||
@BindView(R.id.tv_title)
|
||||
TextView tvTitle;
|
||||
|
|
@ -79,12 +81,12 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
AppCompatButton btnNext;
|
||||
@BindView(R.id.btn_previous)
|
||||
AppCompatButton btnPrevious;
|
||||
private DescriptionsAdapter descriptionsAdapter;
|
||||
private UploadMediaDetailAdapter uploadMediaDetailAdapter;
|
||||
@BindView(R.id.btn_copy_prev_title_desc)
|
||||
AppCompatButton btnCopyPreviousTitleDesc;
|
||||
|
||||
private UploadModel.UploadItem uploadItem;
|
||||
private List<Description> descriptions;
|
||||
private List<UploadMediaDetail> descriptions;
|
||||
|
||||
@Inject
|
||||
UploadMediaDetailsContract.UserActionListener presenter;
|
||||
|
|
@ -222,13 +224,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
}
|
||||
|
||||
/**
|
||||
* init the recycler veiw
|
||||
* init the description recycler veiw and caption recyclerview
|
||||
*/
|
||||
private void initRecyclerView() {
|
||||
descriptionsAdapter = new DescriptionsAdapter(defaultKvStore.getString(Prefs.KEY_LANGUAGE_VALUE, ""));
|
||||
descriptionsAdapter.setCallback(this::showInfoAlert);
|
||||
uploadMediaDetailAdapter = new UploadMediaDetailAdapter();
|
||||
uploadMediaDetailAdapter.setCallback(this::showInfoAlert);
|
||||
uploadMediaDetailAdapter.setEventListener(this::onEvent);
|
||||
rvDescriptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
rvDescriptions.setAdapter(descriptionsAdapter);
|
||||
rvDescriptions.setAdapter(uploadMediaDetailAdapter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -250,7 +253,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
|
||||
@OnClick(R.id.btn_next)
|
||||
public void onNextButtonClicked() {
|
||||
uploadItem.setDescriptions(descriptionsAdapter.getDescriptions());
|
||||
uploadItem.setMediaDetails(uploadMediaDetailAdapter.getUploadMediaDetails());
|
||||
presenter.verifyImageQuality(uploadItem, true);
|
||||
}
|
||||
|
||||
|
|
@ -261,9 +264,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
|
||||
@OnClick(R.id.btn_add_description)
|
||||
public void onButtonAddDescriptionClicked() {
|
||||
Description description = new Description();
|
||||
description.setManuallyAdded(true);//This was manually added by the user
|
||||
descriptionsAdapter.addDescription(description);
|
||||
UploadMediaDetail uploadMediaDetail = new UploadMediaDetail();
|
||||
uploadMediaDetail.setManuallyAdded(true);//This was manually added by the user
|
||||
uploadMediaDetailAdapter.addDescription(uploadMediaDetail);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -290,11 +293,11 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
@Override
|
||||
public void onImageProcessed(UploadItem uploadItem, Place place) {
|
||||
this.uploadItem = uploadItem;
|
||||
if (uploadItem.getTitle() != null) {
|
||||
etTitle.setText(uploadItem.getTitle().toString());
|
||||
if (uploadItem.getFileName() != null) {
|
||||
setDescriptionsInAdapter(uploadItem.getUploadMediaDetails());
|
||||
}
|
||||
|
||||
descriptions = uploadItem.getDescriptions();
|
||||
descriptions = uploadItem.getUploadMediaDetails();
|
||||
photoViewBackgroundImage.setImageURI(uploadItem.getMediaUri());
|
||||
setDescriptionsInAdapter(descriptions);
|
||||
}
|
||||
|
|
@ -317,7 +320,7 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
},
|
||||
() -> {
|
||||
etTitle.setText(place.getName());
|
||||
Description description = new Description();
|
||||
UploadMediaDetail description = new UploadMediaDetail();
|
||||
description.setLanguageCode("en");
|
||||
description.setDescriptionText(place.getLongDescription());
|
||||
descriptions = Arrays.asList(description);
|
||||
|
|
@ -384,9 +387,9 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
}
|
||||
|
||||
@Override
|
||||
public void setTitleAndDescription(String title, List<Description> descriptions) {
|
||||
public void setTitleAndDescription(String title, List<UploadMediaDetail> uploadMediaDetails) {
|
||||
etTitle.setText(title);
|
||||
setDescriptionsInAdapter(descriptions);
|
||||
setDescriptionsInAdapter(uploadMediaDetails);
|
||||
}
|
||||
|
||||
private void deleteThisPicture() {
|
||||
|
|
@ -420,6 +423,13 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
uploadItem.getGpsCoords().getDecLongitude(), 0.0f));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEvent(Boolean data) {
|
||||
btnNext.setEnabled(data);
|
||||
btnNext.setClickable(data);
|
||||
btnNext.setAlpha(data ? 1.0f: 0.5f);
|
||||
}
|
||||
|
||||
|
||||
public interface UploadMediaDetailFragmentCallback extends Callback {
|
||||
|
||||
|
|
@ -432,15 +442,14 @@ public class UploadMediaDetailFragment extends UploadBaseFragment implements
|
|||
presenter.fetchPreviousTitleAndDescription(callback.getIndexInViewFlipper(this));
|
||||
}
|
||||
|
||||
private void setDescriptionsInAdapter(List<Description> descriptions) {
|
||||
if (descriptions == null) {
|
||||
descriptions = new ArrayList<>();
|
||||
private void setDescriptionsInAdapter(List<UploadMediaDetail> uploadMediaDetails){
|
||||
if(uploadMediaDetails==null){
|
||||
uploadMediaDetails=new ArrayList<>();
|
||||
}
|
||||
if (descriptions.size() == 0) {
|
||||
descriptionsAdapter.addDescription(new Description());
|
||||
} else {
|
||||
descriptionsAdapter.setItems(descriptions);
|
||||
}
|
||||
}
|
||||
|
||||
if(uploadMediaDetails.size()==0){
|
||||
uploadMediaDetails.add(new UploadMediaDetail());
|
||||
}
|
||||
uploadMediaDetailAdapter.setItems(uploadMediaDetails);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import fr.free.nrw.commons.BasePresenter;
|
|||
import fr.free.nrw.commons.contributions.Contribution;
|
||||
import fr.free.nrw.commons.filepicker.UploadableFile;
|
||||
import fr.free.nrw.commons.nearby.Place;
|
||||
import fr.free.nrw.commons.upload.Description;
|
||||
import fr.free.nrw.commons.upload.UploadMediaDetail;
|
||||
import fr.free.nrw.commons.upload.SimilarImageInterface;
|
||||
import fr.free.nrw.commons.upload.UploadModel.UploadItem;
|
||||
|
||||
|
|
@ -35,7 +35,7 @@ public interface UploadMediaDetailsContract {
|
|||
|
||||
void showMapWithImageCoordinates(boolean shouldShow);
|
||||
|
||||
void setTitleAndDescription(String title, List<Description> descriptions);
|
||||
void setTitleAndDescription(String title, List<UploadMediaDetail> uploadMediaDetails);
|
||||
}
|
||||
|
||||
interface UserActionListener extends BasePresenter<View> {
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ import timber.log.Timber;
|
|||
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.IO_THREAD;
|
||||
import static fr.free.nrw.commons.di.CommonsApplicationModule.MAIN_THREAD;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_TITLE;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.EMPTY_CAPTION;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.FILE_NAME_EXISTS;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_KEEP;
|
||||
import static fr.free.nrw.commons.utils.ImageUtils.IMAGE_OK;
|
||||
|
|
@ -146,7 +146,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
|||
}
|
||||
|
||||
/**
|
||||
* Fetches and sets the title and desctiption of the previous item
|
||||
* Fetches and sets the caption and desctiption of the previous item
|
||||
*
|
||||
* @param indexInViewFlipper
|
||||
*/
|
||||
|
|
@ -154,7 +154,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
|||
public void fetchPreviousTitleAndDescription(int indexInViewFlipper) {
|
||||
UploadItem previousUploadItem = repository.getPreviousUploadItem(indexInViewFlipper);
|
||||
if (null != previousUploadItem) {
|
||||
view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getDescriptions());
|
||||
view.setTitleAndDescription(previousUploadItem.getTitle().getTitleText(), previousUploadItem.getUploadMediaDetails());
|
||||
} else {
|
||||
view.showMessage(R.string.previous_image_title_description_not_found, R.color.color_error);
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
|||
}
|
||||
|
||||
/**
|
||||
* Handle images, say empty title, duplicate file name, bad picture(in all other cases)
|
||||
* Handle images, say empty caption, duplicate file name, bad picture(in all other cases)
|
||||
*
|
||||
* @param errorCode
|
||||
*/
|
||||
|
|
@ -186,9 +186,9 @@ public class UploadMediaPresenter implements UserActionListener, SimilarImageInt
|
|||
}
|
||||
|
||||
switch (errorCode) {
|
||||
case EMPTY_TITLE:
|
||||
Timber.d("Title is empty. Showing toast");
|
||||
view.showMessage(R.string.add_title_toast, R.color.color_error);
|
||||
case EMPTY_CAPTION:
|
||||
Timber.d("Captions are empty. Showing toast");
|
||||
view.showMessage(R.string.add_caption_toast, R.color.color_error);
|
||||
break;
|
||||
case FILE_NAME_EXISTS:
|
||||
Timber.d("Trying to show duplicate picture popup");
|
||||
|
|
|
|||
|
|
@ -0,0 +1,141 @@
|
|||
package fr.free.nrw.commons.upload.structure.depictions;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
|
||||
import fr.free.nrw.commons.explore.depictions.DepictsClient;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.upload.depicts.DepictsInterface;
|
||||
import fr.free.nrw.commons.utils.StringSortingUtils;
|
||||
import fr.free.nrw.commons.wikidata.model.DepictSearchItem;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.disposables.CompositeDisposable;
|
||||
import timber.log.Timber;
|
||||
|
||||
|
||||
/**
|
||||
* The model class for depictions in upload
|
||||
*/
|
||||
public class DepictModel {
|
||||
private static final int SEARCH_DEPICTS_LIMIT = 25;
|
||||
private final DepictionDao depictDao;
|
||||
private final DepictsInterface depictsInterface;
|
||||
private final JsonKvStore directKvStore;
|
||||
@Inject
|
||||
DepictsClient depictsClient;
|
||||
|
||||
private List<DepictedItem> selectedDepictedItems;
|
||||
private HashMap<String, ArrayList<String>> depictsCache;
|
||||
private CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||
|
||||
@Inject
|
||||
public DepictModel(DepictionDao depictDao, @Named("default_preferences") JsonKvStore directKvStore, DepictsInterface depictsInterface) {
|
||||
this.depictDao = depictDao;
|
||||
this.directKvStore = directKvStore;
|
||||
this.depictsInterface = depictsInterface;
|
||||
this.depictsCache = new HashMap<>();
|
||||
this.selectedDepictedItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
public Comparator<DepictedItem> sortBySimilarity(final String filter) {
|
||||
Comparator<String> stringSimilarityComparator = StringSortingUtils.sortBySimilarity(filter);
|
||||
return (firstItem, secondItem) -> stringSimilarityComparator
|
||||
.compare(firstItem.getDepictsLabel(), secondItem.getDescription());
|
||||
}
|
||||
|
||||
public void cacheAll(HashMap<String, ArrayList<String>> depictsCache) {
|
||||
depictsCache.putAll(depictsCache);
|
||||
}
|
||||
|
||||
public HashMap<String, ArrayList<String>> getDepictsCache() {
|
||||
return depictsCache;
|
||||
}
|
||||
|
||||
boolean cacheContainsKey(String term) {
|
||||
return depictsCache.containsKey(term);
|
||||
}
|
||||
|
||||
public void onDepictItemClicked(DepictedItem depictedItem) {
|
||||
if (depictedItem.isSelected()) {
|
||||
selectDepictItem(depictedItem);
|
||||
// updateDepictCount(depictedItem);
|
||||
} else {
|
||||
unselectDepiction(depictedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void unselectDepiction(DepictedItem depictedItem) {
|
||||
selectedDepictedItems.remove(depictedItem);
|
||||
}
|
||||
|
||||
private void updateDepictCount(DepictedItem depictedItem) {
|
||||
Depiction depiction = depictDao.find(depictedItem.getDepictsLabel());
|
||||
|
||||
if (depictedItem == null) {
|
||||
depiction = new Depiction(null, depictedItem.getDepictsLabel(), new Date(), 0);
|
||||
}
|
||||
|
||||
depiction.incTimesUsed();
|
||||
depictDao.save(depiction);
|
||||
}
|
||||
|
||||
private void selectDepictItem(DepictedItem depictedItem) {
|
||||
selectedDepictedItems.add(depictedItem);
|
||||
}
|
||||
|
||||
private Observable<DepictedItem> titleDepicts(List<String> titleList) {
|
||||
return Observable.fromIterable(titleList)
|
||||
.concatMap(this::getTitleDepicts);
|
||||
}
|
||||
|
||||
private Observable<DepictedItem> getTitleDepicts(String title) {
|
||||
return depictsInterface.searchForDepicts(title, String.valueOf(SEARCH_DEPICTS_LIMIT), Locale.getDefault().getLanguage(), Locale.getDefault().getLanguage(),"0")
|
||||
.map(depictSearchResponse -> {
|
||||
DepictSearchItem depictedItem = depictSearchResponse.getSearch().get(0);
|
||||
return new DepictedItem(depictedItem.getLabel(), depictedItem.getDescription(), "", false, depictedItem.getId());
|
||||
});
|
||||
}
|
||||
|
||||
private Observable<DepictedItem> recentDepicts() {
|
||||
return Observable.fromIterable(depictDao.recentDepicts(SEARCH_DEPICTS_LIMIT))
|
||||
.map(s -> new DepictedItem(s, "", "", false, ""));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get selected Depictions
|
||||
* @return selected depictions
|
||||
*/
|
||||
public List<DepictedItem> getSelectedDepictions() {
|
||||
return selectedDepictedItems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for depictions
|
||||
* @param query
|
||||
* @param imageTitleList
|
||||
* @return
|
||||
*/
|
||||
public Observable<DepictedItem> searchAllEntities(String query, List<String> imageTitleList) {
|
||||
return depictsInterface.searchForDepicts(query, String.valueOf(SEARCH_DEPICTS_LIMIT), Locale.getDefault().getLanguage(), Locale.getDefault().getLanguage(), "0")
|
||||
.flatMap(depictSearchResponse -> Observable.fromIterable(depictSearchResponse.getSearch()))
|
||||
.map(depictSearchItem -> new DepictedItem(depictSearchItem.getLabel(), depictSearchItem.getDescription(), "", false, depictSearchItem.getId()));
|
||||
}
|
||||
|
||||
public List<String> depictionsEntityIdList() {
|
||||
List<String> output = new ArrayList<>();
|
||||
for (DepictedItem d : selectedDepictedItems) {
|
||||
output.add(d.getEntityId());
|
||||
}
|
||||
return output;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
package fr.free.nrw.commons.upload.structure.depictions;
|
||||
|
||||
/**
|
||||
* Model class for Depicted Item in Upload and Explore
|
||||
*/
|
||||
public class DepictedItem {
|
||||
private final String depictsLabel;
|
||||
private final String description;
|
||||
private String imageUrl;
|
||||
private boolean selected;
|
||||
private String entityId;
|
||||
private int position;
|
||||
|
||||
public DepictedItem(String depictsLabel, String description, String imageUrl, boolean selected, String entityId) {
|
||||
this.depictsLabel = depictsLabel;
|
||||
this.selected = selected;
|
||||
this.description = description;
|
||||
this.imageUrl = imageUrl;
|
||||
this.entityId = entityId;
|
||||
}
|
||||
|
||||
public String getEntityId() {
|
||||
return entityId;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getImageUrl() {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
public void setImageUrl(String imageUrl) {
|
||||
this.imageUrl = imageUrl;
|
||||
}
|
||||
|
||||
public String getDepictsLabel() {
|
||||
return depictsLabel;
|
||||
}
|
||||
|
||||
public boolean isSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
public void setSelected(boolean selected) {
|
||||
this.selected = selected;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return super.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DepictedItem that = (DepictedItem) o;
|
||||
|
||||
return depictsLabel.equals(that.depictsLabel);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
package fr.free.nrw.commons.upload.structure.depictions;
|
||||
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* Represents the fact that a given Commons picture depicts a given Wikidata item.
|
||||
* Example: https://commons.wikimedia.org/wiki/File:Sorting_quicksort_anim.gif depicts https://www.wikidata.org/wiki/Q486598
|
||||
*/
|
||||
public class Depiction {
|
||||
private Uri contentUri;
|
||||
private String name;
|
||||
private Date lastUsed;
|
||||
private int timesUsed;
|
||||
|
||||
public Depiction() {
|
||||
}
|
||||
|
||||
public Depiction(Uri contentUri, String name, Date lastUsed, int timesUsed) {
|
||||
this.contentUri = contentUri;
|
||||
this.name = name;
|
||||
this.lastUsed = lastUsed;
|
||||
this.timesUsed = timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the content URI for this category
|
||||
*
|
||||
* @return content URI
|
||||
*/
|
||||
public Uri getContentUri() {
|
||||
return contentUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies the content URI - marking this depiction as already saved in the database
|
||||
*
|
||||
* @param contentUri the content URI
|
||||
*/
|
||||
public void setContentUri(Uri contentUri) {
|
||||
this.contentUri = contentUri;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Gets name
|
||||
*
|
||||
* @return name
|
||||
*/
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies name
|
||||
*
|
||||
* @param name Depicts name
|
||||
*/
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets last used date
|
||||
*
|
||||
* @return Last used date
|
||||
*/
|
||||
public Date getLastUsed() {
|
||||
return lastUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set last used date
|
||||
*
|
||||
* @param lastUsed last used date of depiction
|
||||
*/
|
||||
|
||||
public void setLastUsed(Date lastUsed) {
|
||||
this.lastUsed = lastUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets no. of times the depiction is used
|
||||
*
|
||||
* @return no. of times used
|
||||
*/
|
||||
public int getTimesUsed() {
|
||||
return timesUsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Increments timesUsed by 1 and sets last used date as now.
|
||||
*/
|
||||
public void incTimesUsed() {
|
||||
timesUsed++;
|
||||
touch();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new last used date
|
||||
*/
|
||||
private void touch() {
|
||||
lastUsed = new Date();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package fr.free.nrw.commons.upload.structure.depictions;
|
||||
|
||||
import android.content.ContentProviderClient;
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.os.RemoteException;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Provider;
|
||||
|
||||
public class DepictionDao {
|
||||
|
||||
private final Provider<ContentProviderClient> clientProvider;
|
||||
|
||||
@Inject
|
||||
public DepictionDao(@Named("depictions") Provider<ContentProviderClient> clientProvider) {
|
||||
this.clientProvider = clientProvider;
|
||||
}
|
||||
|
||||
public void save(Depiction depiction) {
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
if (depiction.getContentUri() == null) {
|
||||
depiction.setContentUri(db.insert(DepictsContentProvider.BASE_URI, toContentValues(depiction)));
|
||||
} else {
|
||||
db.update(depiction.getContentUri(), toContentValues(depiction), null, null);
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
db.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find persisted depicts in database, based on its name.
|
||||
*
|
||||
* @param name Depiction name
|
||||
* @return depiction from database, or null if not found
|
||||
*/
|
||||
|
||||
@Nullable
|
||||
Depiction find(String name) {
|
||||
Cursor cursor = null;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
DepictsContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
Table.COLUMN_NAME + "=?",
|
||||
new String[]{name},
|
||||
null);
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return fromCursor(cursor);
|
||||
}
|
||||
db.release();
|
||||
} catch (RemoteException e) {
|
||||
// This feels lazy, but to hell with checked exceptions. :)
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve recently-used depictions, ordered by descending date.
|
||||
*
|
||||
* @return a list containing recent depicts
|
||||
*/
|
||||
@NonNull
|
||||
List<String> recentDepicts(int limit) {
|
||||
List<String> items = new ArrayList<>();
|
||||
Cursor cursor = null;
|
||||
ContentProviderClient db = clientProvider.get();
|
||||
try {
|
||||
cursor = db.query(
|
||||
DepictsContentProvider.BASE_URI,
|
||||
Table.ALL_FIELDS,
|
||||
null,
|
||||
new String[]{},
|
||||
Table.COLUMN_LAST_USED + "DESC");
|
||||
while (cursor != null && cursor.moveToNext()
|
||||
&& cursor.getPosition() < limit) {
|
||||
items.add(fromCursor(cursor).getName());
|
||||
}
|
||||
} catch (RemoteException e) {
|
||||
throw new RuntimeException(e);
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
db.release();
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
Depiction fromCursor(Cursor cursor) {
|
||||
// Hardcoding column positions!
|
||||
return new Depiction(
|
||||
DepictsContentProvider.uriForId(cursor.getInt(cursor.getColumnIndex(DepictionDao.Table.COLUMN_ID))),
|
||||
cursor.getString(cursor.getColumnIndex(DepictionDao.Table.COLUMN_NAME)),
|
||||
new Date(cursor.getLong(cursor.getColumnIndex(DepictionDao.Table.COLUMN_LAST_USED))),
|
||||
cursor.getInt(cursor.getColumnIndex(DepictionDao.Table.COLUMN_TIMES_USED))
|
||||
);
|
||||
}
|
||||
|
||||
private ContentValues toContentValues(Depiction depiction) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(DepictionDao.Table.COLUMN_NAME, depiction.getName());
|
||||
cv.put(DepictionDao.Table.COLUMN_LAST_USED, depiction.getLastUsed().getTime());
|
||||
cv.put(DepictionDao.Table.COLUMN_TIMES_USED, depiction.getTimesUsed());
|
||||
return cv;
|
||||
}
|
||||
|
||||
/**
|
||||
* Example Table: TABLE_NAME: depictions
|
||||
* COLUMN_ID: unique id for the column
|
||||
* COLUMN_NAME: depiction name
|
||||
* COLUMN_LAST_USED: Time stamp for the previous usage of the depiction
|
||||
* COLUMN_TIMES_USED: Number of times the depiction was used previously
|
||||
*/
|
||||
|
||||
public static class Table {
|
||||
public static final String TABLE_NAME = "depictions";
|
||||
public static final String COLUMN_ID = "_id";
|
||||
static final String COLUMN_NAME = "name";
|
||||
static final String COLUMN_LAST_USED = "last_used";
|
||||
static final String COLUMN_TIMES_USED = "times_used";
|
||||
|
||||
|
||||
// NOTE! KEEP IN SAME ORDER AS THEY ARE DEFINED UP THERE. HELPS HARD CODE COLUMN INDICES.
|
||||
public static final String[] ALL_FIELDS = {
|
||||
COLUMN_ID,
|
||||
COLUMN_NAME,
|
||||
COLUMN_LAST_USED,
|
||||
COLUMN_TIMES_USED
|
||||
};
|
||||
|
||||
static final String DROP_TABLE_STATEMENT = "DROP TABLE IF EXISTS " + TABLE_NAME;
|
||||
|
||||
static final String CREATE_TABLE_STATEMENT = "CREATE TABLE " + TABLE_NAME + " ("
|
||||
+ COLUMN_ID + " INTEGER PRIMARY KEY,"
|
||||
+ COLUMN_NAME + " STRING,"
|
||||
+ COLUMN_LAST_USED + " INTEGER,"
|
||||
+ COLUMN_TIMES_USED + " INTEGER"
|
||||
+ ");";
|
||||
|
||||
public static void onCreate(SQLiteDatabase db) {
|
||||
db.execSQL(CREATE_TABLE_STATEMENT);
|
||||
}
|
||||
|
||||
public static void onDelete(SQLiteDatabase db) {
|
||||
db.execSQL(DROP_TABLE_STATEMENT);
|
||||
onCreate(db);
|
||||
}
|
||||
|
||||
public static void onUpdate(SQLiteDatabase db, int from, int to) {
|
||||
if (from == to) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package fr.free.nrw.commons.upload.structure.depictions;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckedTextView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.pedrogomez.renderers.Renderer;
|
||||
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import fr.free.nrw.commons.R;
|
||||
|
||||
public class DepictionRenderer extends Renderer<DepictedItem> {
|
||||
@BindView(R.id.depict_checkbox)
|
||||
CheckedTextView checkedView;
|
||||
private final UploadDepictsCallback listener;
|
||||
@BindView(R.id.depicts_label)
|
||||
TextView depictsLabel;
|
||||
@BindView(R.id.description) TextView description;
|
||||
|
||||
public DepictionRenderer(UploadDepictsCallback listener) {
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setUpView(View rootView) {
|
||||
ButterKnife.bind(this, rootView);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void hookListeners(View rootView) {
|
||||
rootView.setOnClickListener( v -> {
|
||||
DepictedItem item = getContent();
|
||||
item.setSelected(true);
|
||||
checkedView.setChecked(item.isSelected());
|
||||
if (listener != null) {
|
||||
listener.depictsClicked(item);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected View inflate(LayoutInflater inflater, ViewGroup parent) {
|
||||
return inflater.inflate(R.layout.layout_upload_depicts_item, parent, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void render() {
|
||||
DepictedItem item = getContent();
|
||||
checkedView.setChecked(item.isSelected());
|
||||
depictsLabel.setText(item.getDepictsLabel());
|
||||
description.setText(item.getDescription());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
package fr.free.nrw.commons.upload.structure.depictions;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteQueryBuilder;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import fr.free.nrw.commons.data.DBOpenHelper;
|
||||
import fr.free.nrw.commons.di.CommonsDaggerContentProvider;
|
||||
import timber.log.Timber;
|
||||
|
||||
import static fr.free.nrw.commons.BuildConfig.DEPICTION_AUTHORITY;
|
||||
import static fr.free.nrw.commons.upload.structure.depictions.DepictionDao.Table.ALL_FIELDS;
|
||||
import static fr.free.nrw.commons.upload.structure.depictions.DepictionDao.Table.COLUMN_ID;
|
||||
import static fr.free.nrw.commons.upload.structure.depictions.DepictionDao.Table.TABLE_NAME;
|
||||
|
||||
|
||||
@SuppressLint("Registered")
|
||||
public class DepictsContentProvider extends CommonsDaggerContentProvider {
|
||||
|
||||
private static final int DEPICTS = 1;
|
||||
private static final int DEPICTS_ID = 2;
|
||||
private static final String BASE_PATH = "depictions";
|
||||
public static final Uri BASE_URI = Uri.parse("content://" + DEPICTION_AUTHORITY + "/" + BASE_PATH);
|
||||
|
||||
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
static {
|
||||
uriMatcher.addURI(DEPICTION_AUTHORITY, BASE_PATH, DEPICTS);
|
||||
uriMatcher.addURI(DEPICTION_AUTHORITY, BASE_PATH + "/#", DEPICTS_ID);
|
||||
}
|
||||
|
||||
@Inject
|
||||
DBOpenHelper dbOpenHelper;
|
||||
|
||||
public static Uri uriForId(int id) {
|
||||
return Uri.parse(BASE_URI.toString() + "/" + id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(@NotNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
|
||||
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
|
||||
queryBuilder.setTables(TABLE_NAME);
|
||||
|
||||
int uriType = uriMatcher.match(uri);
|
||||
|
||||
SQLiteDatabase db = dbOpenHelper.getReadableDatabase();
|
||||
Cursor cursor;
|
||||
|
||||
switch (uriType) {
|
||||
case DEPICTS:
|
||||
cursor = queryBuilder.query(db, projection, selection, selectionArgs,
|
||||
null, null, sortOrder);
|
||||
break;
|
||||
case DEPICTS_ID:
|
||||
cursor = queryBuilder.query(db,
|
||||
ALL_FIELDS,
|
||||
"_id = ?",
|
||||
new String[]{uri.getLastPathSegment()},
|
||||
null,
|
||||
null,
|
||||
sortOrder
|
||||
);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI" + uri);
|
||||
}
|
||||
|
||||
cursor.setNotificationUri(getContext().getContentResolver(), uri);
|
||||
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NotNull Uri uri) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(@NotNull Uri uri, ContentValues values) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
long id;
|
||||
switch (uriType) {
|
||||
case DEPICTS:
|
||||
id = sqlDB.insert(TABLE_NAME, null, values);
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return Uri.parse(BASE_URI + "/" + id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(@NotNull Uri uri, String selection, String[] selectionArgs) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int bulkInsert(@NotNull Uri uri, @NotNull ContentValues[] values) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
sqlDB.beginTransaction();
|
||||
switch (uriType) {
|
||||
case DEPICTS:
|
||||
for (ContentValues value : values) {
|
||||
Timber.d("Inserting! %s", value);
|
||||
sqlDB.insert(TABLE_NAME, null, value);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri);
|
||||
}
|
||||
sqlDB.setTransactionSuccessful();
|
||||
sqlDB.endTransaction();
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return values.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(@NotNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
||||
int uriType = uriMatcher.match(uri);
|
||||
SQLiteDatabase sqlDB = dbOpenHelper.getWritableDatabase();
|
||||
int rowsUpdated;
|
||||
switch (uriType) {
|
||||
case DEPICTS:
|
||||
if (TextUtils.isEmpty(selection)) {
|
||||
int id = Integer.valueOf(uri.getLastPathSegment());
|
||||
rowsUpdated = sqlDB.update(TABLE_NAME,
|
||||
values,
|
||||
COLUMN_ID + " = ?",
|
||||
new String[]{String.valueOf(id)});
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Parameter `selection` should be empty when updating an ID");
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI: " + uri + " with type " + uriType);
|
||||
}
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
return rowsUpdated;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package fr.free.nrw.commons.upload.structure.depictions;
|
||||
|
||||
/**
|
||||
* Listener to trigger callback whenever a depicts item is clicked
|
||||
*/
|
||||
public interface UploadDepictsCallback {
|
||||
void depictsClicked(DepictedItem item);
|
||||
|
||||
void fetchThumbnailUrlForEntity(String entityId,int position);
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ public class ImageUtils {
|
|||
public static final int IMAGE_OK = 0;
|
||||
public static final int IMAGE_KEEP = -1;
|
||||
public static final int IMAGE_WAIT = -2;
|
||||
public static final int EMPTY_TITLE = -3;
|
||||
public static final int EMPTY_CAPTION = -3;
|
||||
public static final int FILE_NAME_EXISTS = -4;
|
||||
static final int NO_CATEGORY_SELECTED = -5;
|
||||
|
||||
|
|
@ -80,7 +80,7 @@ public class ImageUtils {
|
|||
IMAGE_OK,
|
||||
IMAGE_KEEP,
|
||||
IMAGE_WAIT,
|
||||
EMPTY_TITLE,
|
||||
EMPTY_CAPTION,
|
||||
FILE_NAME_EXISTS,
|
||||
NO_CATEGORY_SELECTED,
|
||||
IMAGE_GEOLOCATION_DIFFERENT
|
||||
|
|
|
|||
|
|
@ -0,0 +1,44 @@
|
|||
package fr.free.nrw.commons.wikidata;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.upload.WikiBaseInterface;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import io.reactivex.Observable;
|
||||
|
||||
import static fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF;
|
||||
|
||||
/**
|
||||
* Wikibase Client for calling WikiBase APIs
|
||||
*/
|
||||
@Singleton
|
||||
public class WikiBaseClient {
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public Observable<Boolean> postEditEntity(String fileEntityId, String data) {
|
||||
try {
|
||||
return wikiBaseInterface.postEditEntity(fileEntityId, csrfTokenClient.getTokenBlocking(), data)
|
||||
.map(response -> (response.getSuccessVal() == 1));
|
||||
} catch (Throwable throwable) {
|
||||
return Observable.just(false);
|
||||
}
|
||||
}
|
||||
|
||||
public Observable<Long> getFileEntityId(String fileName) {
|
||||
return wikiBaseInterface.getFileEntityId(fileName)
|
||||
.map(response -> (long) (response.query().pages().get(0).pageId()));
|
||||
}
|
||||
}
|
||||
|
|
@ -3,15 +3,30 @@ package fr.free.nrw.commons.wikidata;
|
|||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import org.wikipedia.csrf.CsrfTokenClient;
|
||||
import org.wikipedia.dataclient.Service;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Named;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import fr.free.nrw.commons.BuildConfig;
|
||||
import fr.free.nrw.commons.R;
|
||||
import fr.free.nrw.commons.kvstore.JsonKvStore;
|
||||
import fr.free.nrw.commons.media.MediaClient;
|
||||
import fr.free.nrw.commons.upload.mediaDetails.CaptionInterface;
|
||||
import fr.free.nrw.commons.utils.ConfigUtils;
|
||||
import fr.free.nrw.commons.utils.ViewUtil;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.ObservableSource;
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
|
@ -30,21 +45,37 @@ public class WikidataEditService {
|
|||
private final Context context;
|
||||
private final WikidataEditListener wikidataEditListener;
|
||||
private final JsonKvStore directKvStore;
|
||||
private final CaptionInterface captionInterface;
|
||||
private final WikiBaseClient wikiBaseClient;
|
||||
private final WikidataClient wikidataClient;
|
||||
private final MediaClient mediaClient;
|
||||
private final CsrfTokenClient csrfTokenClient;
|
||||
private final Service service;
|
||||
|
||||
@Inject
|
||||
WikidataEditService(Context context,
|
||||
WikidataEditListener wikidataEditListener,
|
||||
@Named("default_preferences") JsonKvStore directKvStore,
|
||||
WikidataClient wikidataClient) {
|
||||
public WikidataEditService(Context context,
|
||||
WikidataEditListener wikidataEditListener,
|
||||
MediaClient mediaClient,
|
||||
@Named("default_preferences") JsonKvStore directKvStore,
|
||||
WikiBaseClient wikiBaseClient,
|
||||
CaptionInterface captionInterface,
|
||||
WikidataClient wikidataClient,
|
||||
@Named("commons-csrf") CsrfTokenClient csrfTokenClient,
|
||||
@Named("commons-service") Service service) {
|
||||
this.context = context;
|
||||
this.wikidataEditListener = wikidataEditListener;
|
||||
this.directKvStore = directKvStore;
|
||||
this.captionInterface = captionInterface;
|
||||
this.wikiBaseClient = wikiBaseClient;
|
||||
this.mediaClient = mediaClient;
|
||||
this.wikidataClient = wikidataClient;
|
||||
this.csrfTokenClient = csrfTokenClient;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a P18 claim and log the edit with custom tag
|
||||
*
|
||||
* @param wikidataEntityId
|
||||
* @param fileName
|
||||
*/
|
||||
|
|
@ -65,8 +96,11 @@ public class WikidataEditService {
|
|||
}
|
||||
|
||||
editWikidataProperty(wikidataEntityId, fileName);
|
||||
//editWikiBaseDepictsProperty(wikidataEntityId, fileName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Edits the wikidata entity by adding the P18 property to it.
|
||||
* Adding the P18 edit requires calling the wikidata API to create a claim against the entity
|
||||
|
|
@ -97,6 +131,81 @@ public class WikidataEditService {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Edits the wikibase entity by adding DEPICTS property.
|
||||
* Adding DEPICTS property requires call to the wikibase API to set tag against the entity.
|
||||
*
|
||||
* @param wikidataEntityId
|
||||
* @param fileName
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
private void editWikiBaseDepictsProperty(String wikidataEntityId, String fileName) {
|
||||
wikiBaseClient.getFileEntityId(fileName)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(fileEntityId -> {
|
||||
if (fileEntityId != null) {
|
||||
Timber.d("EntityId for image was received successfully: %s", fileEntityId);
|
||||
addDepictsProperty(wikidataEntityId, fileEntityId.toString());
|
||||
} else {
|
||||
Timber.d("Error acquiring EntityId for image: %s", fileName);
|
||||
}
|
||||
}, throwable -> {
|
||||
Timber.e(throwable, "Error occurred while getting EntityID to set DEPICTS property");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
});
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void addDepictsProperty(String entityId, String fileEntityId) {
|
||||
if (ConfigUtils.isBetaFlavour()) {
|
||||
entityId = "Q10"; // Wikipedia:Sandbox (Q10)
|
||||
}
|
||||
|
||||
JsonObject value = new JsonObject();
|
||||
value.addProperty("entity-type", "item");
|
||||
value.addProperty("numeric-id", entityId.replace("Q", ""));
|
||||
value.addProperty("id", entityId);
|
||||
|
||||
JsonObject dataValue = new JsonObject();
|
||||
dataValue.add("value", value);
|
||||
dataValue.addProperty("type", "wikibase-entityid");
|
||||
|
||||
JsonObject mainSnak = new JsonObject();
|
||||
mainSnak.addProperty("snaktype", "value");
|
||||
mainSnak.addProperty("property", BuildConfig.DEPICTS_PROPERTY);
|
||||
mainSnak.add("datavalue", dataValue);
|
||||
|
||||
JsonObject claim = new JsonObject();
|
||||
claim.add("mainsnak", mainSnak);
|
||||
claim.addProperty("type", "statement");
|
||||
claim.addProperty("rank", "preferred");
|
||||
|
||||
JsonArray claims = new JsonArray();
|
||||
claims.add(claim);
|
||||
|
||||
JsonObject jsonData = new JsonObject();
|
||||
jsonData.add("claims", claims);
|
||||
|
||||
String data = jsonData.toString();
|
||||
|
||||
Observable.defer((Callable<ObservableSource<Boolean>>) () ->
|
||||
wikiBaseClient.postEditEntity("M" + fileEntityId, data))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(success -> {
|
||||
if (success)
|
||||
Timber.d("DEPICTS property was set successfully for %s", fileEntityId);
|
||||
else
|
||||
Timber.d("Unable to set DEPICTS property for %s", fileEntityId);
|
||||
},
|
||||
throwable -> {
|
||||
Timber.e(throwable, "Error occurred while setting DEPICTS property");
|
||||
ViewUtil.showLongToast(context, throwable.toString());
|
||||
});
|
||||
}
|
||||
|
||||
private void handleClaimResult(String wikidataEntityId, String revisionId) {
|
||||
if (revisionId != null) {
|
||||
if (wikidataEditListener != null) {
|
||||
|
|
@ -109,13 +218,14 @@ public class WikidataEditService {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Show a success toast when the edit is made successfully
|
||||
*/
|
||||
private void showSuccessToast() {
|
||||
String title = directKvStore.getString("Title", "");
|
||||
String caption = directKvStore.getString("Title", "");
|
||||
String successStringTemplate = context.getString(R.string.successful_wikidata_edit);
|
||||
String successMessage = String.format(Locale.getDefault(), successStringTemplate, title);
|
||||
@SuppressLint({"StringFormatInvalid", "LocalSuppress"}) String successMessage = String.format(Locale.getDefault(), successStringTemplate, caption);
|
||||
ViewUtil.showLongToast(context, successMessage);
|
||||
}
|
||||
|
||||
|
|
@ -131,4 +241,79 @@ public class WikidataEditService {
|
|||
Timber.d("Wikidata property name is %s", fileName);
|
||||
return fileName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adding captions as labels after image is successfully uploaded
|
||||
*/
|
||||
@SuppressLint("CheckResult")
|
||||
public void createLabelforWikidataEntity(String wikiDataEntityId, String fileName, Map<String, String> captions) {
|
||||
Observable.fromCallable(() -> wikiBaseClient.getFileEntityId(fileName))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(fileEntityId -> {
|
||||
if (fileEntityId != null) {
|
||||
for (Map.Entry<String, String> entry : captions.entrySet()) {
|
||||
Map<String, String> caption = new HashMap<>();
|
||||
caption.put(entry.getKey(), entry.getValue());
|
||||
try {
|
||||
wikidataAddLabels(wikiDataEntityId, fileEntityId.toString(), caption);
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Timber.d("Error acquiring EntityId for image");
|
||||
}
|
||||
}, throwable -> {
|
||||
Timber.e(throwable, "Error occurred while getting EntityID for the file");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds label to Wikidata using the fileEntityId and the edit token, obtained from csrfTokenClient
|
||||
*
|
||||
* @param wikiDataEntityId entityId for the current contribution
|
||||
* @param fileEntityId
|
||||
* @param caption
|
||||
*/
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private void wikidataAddLabels(String wikiDataEntityId, String fileEntityId, Map<String, String> caption) throws Throwable {
|
||||
Observable.fromCallable(() -> {
|
||||
try {
|
||||
return csrfTokenClient.getTokenBlocking();
|
||||
} catch (Throwable throwable) {
|
||||
throwable.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(editToken -> {
|
||||
if (editToken != null) {
|
||||
Observable.fromCallable(() -> captionInterface.addLabelstoWikidata(fileEntityId, editToken, caption.get(0), caption))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(revisionId ->
|
||||
{
|
||||
if (revisionId != null) {
|
||||
Timber.d("Caption successfully set, revision id = %s", revisionId);
|
||||
} else {
|
||||
Timber.d("Error occurred while setting Captions, fileEntityId = %s", fileEntityId);
|
||||
}
|
||||
|
||||
},
|
||||
throwable -> {
|
||||
Timber.e(throwable, "Error occurred while setting Captions");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
});
|
||||
}else {
|
||||
Timber.d("Error acquiring EntityId for image");
|
||||
}
|
||||
}, throwable -> {
|
||||
Timber.e(throwable, "Error occurred while getting EntityID for the File");
|
||||
ViewUtil.showLongToast(context, context.getString(R.string.wikidata_edit_failure));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package fr.free.nrw.commons.wikidata.model;
|
||||
|
||||
|
||||
/**
|
||||
* Model class for Depiction item returned from API after calling searchForDepicts
|
||||
*/
|
||||
|
||||
public class DepictSearchItem {
|
||||
private final String id;
|
||||
private final String pageid;
|
||||
private final String url;
|
||||
private final String label;
|
||||
private final String description;
|
||||
|
||||
public DepictSearchItem(String id, String pageid, String url, String label, String description) {
|
||||
this.id = id;
|
||||
this.pageid = pageid;
|
||||
this.url = url;
|
||||
this.label = label;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getPageid() {
|
||||
return pageid;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package fr.free.nrw.commons.wikidata.model;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Model class for API response obtained from search for depictions
|
||||
*/
|
||||
public class DepictSearchResponse {
|
||||
private final List<DepictSearchItem> search;
|
||||
|
||||
/**
|
||||
* Constructor to initialise value of the search object
|
||||
*/
|
||||
public DepictSearchResponse(List<DepictSearchItem> search) {
|
||||
this.search = search;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return List<DepictSearchItem> for the DepictSearchResponse
|
||||
*/
|
||||
public List<DepictSearchItem> getSearch() {
|
||||
return search;
|
||||
}
|
||||
}
|
||||
49
app/src/main/res/layout/activity_wikidata_item_details.xml
Normal file
49
app/src/main/res/layout/activity_wikidata_item_details.xml
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/toolbar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/primaryDarkColor">
|
||||
|
||||
<include layout="@layout/toolbar"/>
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/toolbar"
|
||||
android:background="?attr/tabBackground"
|
||||
app:tabIndicatorColor="?attr/tabIndicatorColor"
|
||||
app:tabMode="scrollable"
|
||||
app:tabSelectedTextColor="?attr/tabSelectedTextColor"
|
||||
app:tabTextColor="?attr/tabTextColor" />
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/mediaContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/toolbar_layout"
|
||||
android:orientation="horizontal"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.viewpager.widget.ViewPager
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/toolbar_layout" />
|
||||
</RelativeLayout>
|
||||
|
||||
<include layout="@layout/drawer_view" />
|
||||
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue