*.kt: bulk correction of formatting using ktlint --format

This commit is contained in:
tristan81 2024-09-18 15:23:24 +10:00
parent f751ab4a75
commit 4aca2a26ae
401 changed files with 10409 additions and 8827 deletions

View file

@ -25,7 +25,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class AboutActivityTest { class AboutActivityTest {
@get:Rule @get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java) var activityRule: ActivityTestRule<*> = ActivityTestRule(AboutActivity::class.java)
@ -36,7 +35,8 @@ class AboutActivityTest {
device.setOrientationNatural() device.setOrientationNatural()
device.freezeRotation() device.freezeRotation()
Intents.init() Intents.init()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
} }
@ -47,11 +47,12 @@ class AboutActivityTest {
@Test @Test
fun testBuildNumber() { fun testBuildNumber() {
Espresso.onView(ViewMatchers.withId(R.id.about_version)) Espresso
.onView(ViewMatchers.withId(R.id.about_version))
.check( .check(
ViewAssertions.matches( ViewAssertions.matches(
withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()) withText(getApplicationContext<CommonsApplication>().getVersionNameWithSha()),
) ),
) )
} }
@ -61,8 +62,8 @@ class AboutActivityTest {
Intents.intended( Intents.intended(
CoreMatchers.allOf( CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.WEBSITE_URL) IntentMatchers.hasData(Urls.WEBSITE_URL),
) ),
) )
} }
@ -73,8 +74,8 @@ class AboutActivityTest {
CoreMatchers.anyOf( CoreMatchers.anyOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL), IntentMatchers.hasData(Urls.FACEBOOK_WEB_URL),
IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME) IntentMatchers.hasPackage(Urls.FACEBOOK_PACKAGE_NAME),
) ),
) )
} }
@ -84,8 +85,8 @@ class AboutActivityTest {
Intents.intended( Intents.intended(
CoreMatchers.allOf( CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.GITHUB_REPO_URL) IntentMatchers.hasData(Urls.GITHUB_REPO_URL),
) ),
) )
} }
@ -95,8 +96,8 @@ class AboutActivityTest {
Intents.intended( Intents.intended(
CoreMatchers.allOf( CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL) IntentMatchers.hasData(BuildConfig.PRIVACY_POLICY_URL),
) ),
) )
} }
@ -108,8 +109,8 @@ class AboutActivityTest {
Intents.intended( Intents.intended(
CoreMatchers.allOf( CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode") IntentMatchers.hasData("${Urls.TRANSLATE_WIKI_URL}$langCode"),
) ),
) )
} }
@ -119,27 +120,30 @@ class AboutActivityTest {
Intents.intended( Intents.intended(
CoreMatchers.allOf( CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.CREDITS_URL) IntentMatchers.hasData(Urls.CREDITS_URL),
) ),
) )
} }
@Test @Test
fun testLaunchUserGuide() { fun testLaunchUserGuide() {
Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.about_user_guide)).perform(ViewActions.click())
Intents.intended(CoreMatchers.allOf(IntentMatchers.hasAction(Intent.ACTION_VIEW), Intents.intended(
IntentMatchers.hasData(Urls.USER_GUIDE_URL))) CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.USER_GUIDE_URL),
),
)
} }
@Test @Test
fun testLaunchAboutFaq() { fun testLaunchAboutFaq() {
Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click()) Espresso.onView(ViewMatchers.withId(R.id.about_faq)).perform(ViewActions.click())
Intents.intended( Intents.intended(
CoreMatchers.allOf( CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(Urls.FAQ_URL) IntentMatchers.hasData(Urls.FAQ_URL),
) ),
) )
} }
} }

View file

@ -23,7 +23,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class LoginActivityTest { class LoginActivityTest {
@get:Rule @get:Rule
var activityRule = ActivityTestRule(LoginActivity::class.java) var activityRule = ActivityTestRule(LoginActivity::class.java)
@ -49,8 +48,8 @@ class LoginActivityTest {
Intents.intended( Intents.intended(
CoreMatchers.allOf( CoreMatchers.allOf(
IntentMatchers.hasAction(Intent.ACTION_VIEW), IntentMatchers.hasAction(Intent.ACTION_VIEW),
IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL) IntentMatchers.hasData(BuildConfig.FORGOT_PASSWORD_URL),
) ),
) )
} }

View file

@ -27,13 +27,13 @@ import org.junit.runner.RunWith
@LargeTest @LargeTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class MainActivityTest { class MainActivityTest {
@get:Rule @get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java) var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
@get:Rule @get:Rule
var mGrantPermissionRule: GrantPermissionRule = GrantPermissionRule.grant( var mGrantPermissionRule: GrantPermissionRule =
"android.permission.ACCESS_FINE_LOCATION" GrantPermissionRule.grant(
"android.permission.ACCESS_FINE_LOCATION",
) )
private val device: UiDevice = private val device: UiDevice =
@ -48,7 +48,8 @@ class MainActivityTest {
UITestHelper.loginUser() UITestHelper.loginUser()
UITestHelper.skipWelcome() UITestHelper.skipWelcome()
Intents.init() Intents.init()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
val context = InstrumentationRegistry.getInstrumentation().targetContext val context = InstrumentationRegistry.getInstrumentation().targetContext
val storeName = context.packageName + "_preferences" val storeName = context.packageName + "_preferences"
@ -62,33 +63,37 @@ class MainActivityTest {
@Test @Test
fun testNearby() { fun testNearby() {
Espresso.onView( Espresso
.onView(
Matchers.allOf( Matchers.allOf(
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
1 1,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer)) Espresso
.onView(ViewMatchers.withId(R.id.fragmentContainer))
.check(matches(ViewMatchers.isDisplayed())) .check(matches(ViewMatchers.isDisplayed()))
UITestHelper.sleep(10000) UITestHelper.sleep(10000)
val actionMenuItemView2 = Espresso.onView( val actionMenuItemView2 =
Espresso.onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(R.id.list_sheet), ViewMatchers.withContentDescription("List"), ViewMatchers.withId(R.id.list_sheet),
ViewMatchers.withContentDescription("List"),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.toolbar), ViewMatchers.withId(R.id.toolbar),
1 1,
), ),
0 0,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
) )
actionMenuItemView2.perform(ViewActions.click()) actionMenuItemView2.perform(ViewActions.click())
UITestHelper.sleep(1000) UITestHelper.sleep(1000)
@ -96,64 +101,70 @@ class MainActivityTest {
@Test @Test
fun testExplore() { fun testExplore() {
Espresso.onView( Espresso
.onView(
Matchers.allOf( Matchers.allOf(
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
2 2,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer)) Espresso
.onView(ViewMatchers.withId(R.id.fragmentContainer))
.check(matches(ViewMatchers.isDisplayed())) .check(matches(ViewMatchers.isDisplayed()))
UITestHelper.sleep(1000) UITestHelper.sleep(1000)
} }
@Test @Test
fun testContributions() { fun testContributions() {
Espresso.onView( Espresso
.onView(
Matchers.allOf( Matchers.allOf(
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
0 0,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
Espresso.onView(ViewMatchers.withId(R.id.fragmentContainer)) Espresso
.onView(ViewMatchers.withId(R.id.fragmentContainer))
.check(matches(ViewMatchers.isDisplayed())) .check(matches(ViewMatchers.isDisplayed()))
Espresso.onView( Espresso
.onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(R.id.contributionImage), ViewMatchers.withId(R.id.contributionImage),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.contributionsList), ViewMatchers.withId(R.id.contributionsList),
0 0,
), ),
1 1,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
val actionMenuItemView = Espresso.onView( val actionMenuItemView =
Espresso.onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(R.id.menu_bookmark_current_image), ViewMatchers.withId(R.id.menu_bookmark_current_image),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.toolbar), ViewMatchers.withId(R.id.toolbar),
1 1,
), ),
0 0,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
) )
actionMenuItemView.perform(ViewActions.click()) actionMenuItemView.perform(ViewActions.click())
UITestHelper.sleep(3000) UITestHelper.sleep(3000)
@ -161,35 +172,37 @@ class MainActivityTest {
@Test @Test
fun testBookmarks() { fun testBookmarks() {
Espresso.onView( Espresso
.onView(
Matchers.allOf( Matchers.allOf(
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
3 3,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
UITestHelper.sleep(1000) UITestHelper.sleep(1000)
} }
@Test @Test
fun testNotifications() { fun testNotifications() {
Espresso.onView( Espresso
.onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(R.id.notifications), ViewMatchers.withId(R.id.notifications),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.toolbar), ViewMatchers.withId(R.id.toolbar),
1 1,
), ),
1 1,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name)) Intents.intended(IntentMatchers.hasComponent(NotificationActivity::class.java.name))
Espresso.pressBack() Espresso.pressBack()

View file

@ -4,7 +4,6 @@ import android.app.Activity
import android.app.Instrumentation import android.app.Instrumentation
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.action.ViewActions.swipeRight
import androidx.test.espresso.intent.Intents import androidx.test.espresso.intent.Intents
import androidx.test.espresso.intent.matcher.IntentMatchers import androidx.test.espresso.intent.matcher.IntentMatchers
import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent import androidx.test.espresso.intent.matcher.IntentMatchers.hasComponent
@ -26,7 +25,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ProfileActivityTest { class ProfileActivityTest {
@get:Rule @get:Rule
var activityRule = IntentsTestRule(LoginActivity::class.java) var activityRule = IntentsTestRule(LoginActivity::class.java)
@ -38,7 +36,8 @@ class ProfileActivityTest {
device.freezeRotation() device.freezeRotation()
UITestHelper.loginUser() UITestHelper.loginUser()
UITestHelper.skipWelcome() UITestHelper.skipWelcome()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
} }
@ -50,20 +49,19 @@ class ProfileActivityTest {
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
withId(R.id.fragment_main_nav_tab_layout), withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
4 4,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
onView(Matchers.allOf(withId(R.id.more_profile))).perform( onView(Matchers.allOf(withId(R.id.more_profile))).perform(
ViewActions.scrollTo(), ViewActions.scrollTo(),
ViewActions.click() ViewActions.click(),
) )
device.swipe(1033,1346,531,1346,20) device.swipe(1033, 1346, 531, 1346, 20)
UITestHelper.sleep(5000) UITestHelper.sleep(5000)
Intents.intended(hasComponent(ProfileActivity::class.java.name)) Intents.intended(hasComponent(ProfileActivity::class.java.name))
} }
} }

View file

@ -9,7 +9,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class ReviewActivityTest { class ReviewActivityTest {
@get:Rule @get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java) var activityRule: ActivityTestRule<*> = ActivityTestRule(ReviewActivity::class.java)
@ -17,5 +16,4 @@ class ReviewActivityTest {
fun orientationChange() { fun orientationChange() {
UITestHelper.changeOrientation(activityRule) UITestHelper.changeOrientation(activityRule)
} }
} }

View file

@ -16,7 +16,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SearchActivityTest { class SearchActivityTest {
@get:Rule @get:Rule
var activityRule = ActivityTestRule(SearchActivity::class.java) var activityRule = ActivityTestRule(SearchActivity::class.java)
@ -31,20 +30,21 @@ class SearchActivityTest {
@Test @Test
fun exploreActivityTest() { fun exploreActivityTest() {
val searchAutoComplete = Espresso.onView( val searchAutoComplete =
Espresso.onView(
Matchers.allOf( Matchers.allOf(
UITestHelper.childAtPosition( UITestHelper.childAtPosition(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")), ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
UITestHelper.childAtPosition( UITestHelper.childAtPosition(
ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")), ViewMatchers.withClassName(Matchers.`is`("android.widget.LinearLayout")),
1 1,
)
), ),
0
), ),
ViewMatchers.isDisplayed() 0,
) ),
ViewMatchers.isDisplayed(),
),
) )
searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard()) searchAutoComplete.perform(ViewActions.replaceText("cat"), ViewActions.closeSoftKeyboard())
UITestHelper.sleep(5000) UITestHelper.sleep(5000)

View file

@ -22,7 +22,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SettingsActivityLoggedInTest { class SettingsActivityLoggedInTest {
@get:Rule @get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java) var activityRule: ActivityTestRule<*> = ActivityTestRule(LoginActivity::class.java)
@ -35,31 +34,32 @@ class SettingsActivityLoggedInTest {
device.freezeRotation() device.freezeRotation()
UITestHelper.loginUser() UITestHelper.loginUser()
UITestHelper.skipWelcome() UITestHelper.skipWelcome()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
} }
@Test @Test
fun testSettings() { fun testSettings() {
Espresso.onView( Espresso
.onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withContentDescription("More"), ViewMatchers.withContentDescription("More"),
UITestHelper.childAtPosition( UITestHelper.childAtPosition(
UITestHelper.childAtPosition( UITestHelper.childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
4 4,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform( Espresso.onView(Matchers.allOf(ViewMatchers.withId(R.id.more_settings))).perform(
ViewActions.scrollTo(), ViewActions.scrollTo(),
ViewActions.click() ViewActions.click(),
) )
Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name)) Intents.intended(IntentMatchers.hasComponent(SettingsActivity::class.java.name))
UITestHelper.sleep(1000) UITestHelper.sleep(1000)
} }
} }

View file

@ -23,7 +23,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class SettingsActivityTest { class SettingsActivityTest {
private lateinit var defaultKvStore: JsonKvStore private lateinit var defaultKvStore: JsonKvStore
@get:Rule @get:Rule
@ -44,21 +43,23 @@ class SettingsActivityTest {
fun useAuthorNameTogglesOn() { fun useAuthorNameTogglesOn() {
// Turn on "Use author name" preference if currently off // Turn on "Use author name" preference if currently off
if (!defaultKvStore.getBoolean("useAuthorName", false)) { if (!defaultKvStore.getBoolean("useAuthorName", false)) {
Espresso.onView( Espresso
.onView(
allOf( allOf(
withId(R.id.recycler_view), withId(R.id.recycler_view),
childAtPosition(withId(android.R.id.list_container), 0) childAtPosition(withId(android.R.id.list_container), 0),
) ),
).perform( ).perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()) RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(6, click()),
) )
} }
// Check authorName preference is enabled // Check authorName preference is enabled
Espresso.onView( Espresso
.onView(
allOf( allOf(
withId(R.id.recycler_view), withId(R.id.recycler_view),
childAtPosition(withId(android.R.id.list_container), 0) childAtPosition(withId(android.R.id.list_container), 0),
) ),
).check(matches(isEnabled())) ).check(matches(isEnabled()))
} }

View file

@ -13,14 +13,13 @@ import org.apache.commons.lang3.StringUtils
import org.hamcrest.* import org.hamcrest.*
import timber.log.Timber import timber.log.Timber
class UITestHelper { class UITestHelper {
companion object { companion object {
fun skipWelcome() { fun skipWelcome() {
try { try {
onView(ViewMatchers.withId(R.id.button_ok)) onView(ViewMatchers.withId(R.id.button_ok))
.perform(ViewActions.click()) .perform(ViewActions.click())
//Skip tutorial // Skip tutorial
onView(ViewMatchers.withId(R.id.finishTutorialButton)) onView(ViewMatchers.withId(R.id.finishTutorialButton))
.perform(ViewActions.click()) .perform(ViewActions.click())
} catch (ignored: NoMatchingViewException) { } catch (ignored: NoMatchingViewException) {
@ -29,26 +28,30 @@ class UITestHelper {
fun skipLogin() { fun skipLogin() {
try { try {
//Skip Login // Skip Login
val htmlTextView = onView( val htmlTextView =
onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(R.id.skip_login), ViewMatchers.withText("Skip"), ViewMatchers.withId(R.id.skip_login),
ViewMatchers.isDisplayed() ViewMatchers.withText("Skip"),
) ViewMatchers.isDisplayed(),
),
) )
htmlTextView.perform(ViewActions.click()) htmlTextView.perform(ViewActions.click())
val appCompatButton = onView( val appCompatButton =
onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"), ViewMatchers.withId(android.R.id.button1),
ViewMatchers.withText("Yes"),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.buttonPanel), ViewMatchers.withId(R.id.buttonPanel),
0 0,
),
3,
),
), ),
3
)
)
) )
appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click()) appCompatButton.perform(ViewActions.scrollTo(), ViewActions.click())
} catch (ignored: NoMatchingViewException) { } catch (ignored: NoMatchingViewException) {
@ -57,18 +60,18 @@ class UITestHelper {
fun loginUser() { fun loginUser() {
try { try {
//Perform Login // Perform Login
sleep(3000) sleep(3000)
onView(ViewMatchers.withId(R.id.login_username)) onView(ViewMatchers.withId(R.id.login_username))
.perform( .perform(
ViewActions.replaceText(getTestUsername()), ViewActions.replaceText(getTestUsername()),
ViewActions.closeSoftKeyboard() ViewActions.closeSoftKeyboard(),
) )
sleep(2000) sleep(2000)
onView(ViewMatchers.withId(R.id.login_password)) onView(ViewMatchers.withId(R.id.login_password))
.perform( .perform(
ViewActions.replaceText(getTestUserPassword()), ViewActions.replaceText(getTestUserPassword()),
ViewActions.closeSoftKeyboard() ViewActions.closeSoftKeyboard(),
) )
sleep(2000) sleep(2000)
onView(ViewMatchers.withId(R.id.login_button)) onView(ViewMatchers.withId(R.id.login_button))
@ -76,7 +79,6 @@ class UITestHelper {
sleep(10000) sleep(10000)
} catch (ignored: NoMatchingViewException) { } catch (ignored: NoMatchingViewException) {
} }
} }
fun logoutUser() { fun logoutUser() {
@ -87,36 +89,38 @@ class UITestHelper {
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.fragment_main_nav_tab_layout), ViewMatchers.withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
4 4,
),
ViewMatchers.isDisplayed(),
), ),
ViewMatchers.isDisplayed()
)
).perform(ViewActions.click()) ).perform(ViewActions.click())
onView( onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(R.id.more_logout), ViewMatchers.withText("Logout"), ViewMatchers.withId(R.id.more_logout),
ViewMatchers.withText("Logout"),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet), ViewMatchers.withId(R.id.scroll_view_more_bottom_sheet),
0 0,
),
6,
),
), ),
6
)
)
).perform(ViewActions.scrollTo(), ViewActions.click()) ).perform(ViewActions.scrollTo(), ViewActions.click())
onView( onView(
Matchers.allOf( Matchers.allOf(
ViewMatchers.withId(android.R.id.button1), ViewMatchers.withText("Yes"), ViewMatchers.withId(android.R.id.button1),
ViewMatchers.withText("Yes"),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
ViewMatchers.withId(R.id.buttonPanel), ViewMatchers.withId(R.id.buttonPanel),
0 0,
),
3,
),
), ),
3
)
)
).perform(ViewActions.scrollTo(), ViewActions.click()) ).perform(ViewActions.scrollTo(), ViewActions.click())
sleep(5000) sleep(5000)
} catch (ignored: NoMatchingViewException) { } catch (ignored: NoMatchingViewException) {
@ -124,9 +128,9 @@ class UITestHelper {
} }
fun childAtPosition( fun childAtPosition(
parentMatcher: Matcher<View>, position: Int parentMatcher: Matcher<View>,
position: Int,
): Matcher<View> { ): Matcher<View> {
return object : TypeSafeMatcher<View>() { return object : TypeSafeMatcher<View>() {
override fun describeTo(description: Description) { override fun describeTo(description: Description) {
description.appendText("Child at position $position in parent ") description.appendText("Child at position $position in parent ")
@ -135,8 +139,9 @@ class UITestHelper {
public override fun matchesSafely(view: View): Boolean { public override fun matchesSafely(view: View): Boolean {
val parent = view.parent val parent = view.parent
return parent is ViewGroup && parentMatcher.matches(parent) return parent is ViewGroup &&
&& view == parent.getChildAt(position) parentMatcher.matches(parent) &&
view == parent.getChildAt(position)
} }
} }
} }
@ -154,14 +159,18 @@ class UITestHelper {
val username = BuildConfig.TEST_USERNAME val username = BuildConfig.TEST_USERNAME
if (StringUtils.isEmpty(username) || username == "null") { if (StringUtils.isEmpty(username) || username == "null") {
throw NotImplementedError("Configure your beta account's username") throw NotImplementedError("Configure your beta account's username")
} else return username } else {
return username
}
} }
private fun getTestUserPassword(): String { private fun getTestUserPassword(): String {
val password = BuildConfig.TEST_PASSWORD val password = BuildConfig.TEST_PASSWORD
if (StringUtils.isEmpty(password) || password == "null") { if (StringUtils.isEmpty(password) || password == "null") {
throw NotImplementedError("Configure your beta account's password") throw NotImplementedError("Configure your beta account's password")
} else return password } else {
return password
}
} }
fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) { fun <T : Activity> changeOrientation(activityRule: ActivityTestRule<T>) {
@ -174,6 +183,7 @@ class UITestHelper {
fun <T> first(matcher: Matcher<T>): Matcher<T>? { fun <T> first(matcher: Matcher<T>): Matcher<T>? {
return object : BaseMatcher<T>() { return object : BaseMatcher<T>() {
var isFirst = true var isFirst = true
override fun matches(item: Any): Boolean { override fun matches(item: Any): Boolean {
if (isFirst && matcher.matches(item)) { if (isFirst && matcher.matches(item)) {
isFirst = false isFirst = false

View file

@ -28,7 +28,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class UploadCancelledTest { class UploadCancelledTest {
@Rule @Rule
@JvmField @JvmField
var mActivityTestRule = ActivityTestRule(LoginActivity::class.java) var mActivityTestRule = ActivityTestRule(LoginActivity::class.java)
@ -37,7 +36,7 @@ class UploadCancelledTest {
@JvmField @JvmField
var mGrantPermissionRule: GrantPermissionRule = var mGrantPermissionRule: GrantPermissionRule =
GrantPermissionRule.grant( GrantPermissionRule.grant(
"android.permission.WRITE_EXTERNAL_STORAGE" "android.permission.WRITE_EXTERNAL_STORAGE",
) )
private val device: UiDevice = private val device: UiDevice =
@ -48,14 +47,14 @@ class UploadCancelledTest {
try { try {
Intents.init() Intents.init()
} catch (ex: IllegalStateException) { } catch (ex: IllegalStateException) {
} }
device.unfreezeRotation() device.unfreezeRotation()
device.setOrientationNatural() device.setOrientationNatural()
device.freezeRotation() device.freezeRotation()
UITestHelper.loginUser() UITestHelper.loginUser()
UITestHelper.skipWelcome() UITestHelper.skipWelcome()
Intents.intending(CoreMatchers.not(IntentMatchers.isInternal())) Intents
.intending(CoreMatchers.not(IntentMatchers.isInternal()))
.respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null)) .respondWith(Instrumentation.ActivityResult(Activity.RESULT_OK, null))
} }
@ -64,129 +63,136 @@ class UploadCancelledTest {
try { try {
Intents.release() Intents.release()
} catch (ex: IllegalStateException) { } catch (ex: IllegalStateException) {
} }
} }
@Test @Test
fun uploadCancelledAfterLocationPickedTest() { fun uploadCancelledAfterLocationPickedTest() {
val bottomNavigationItemView =
val bottomNavigationItemView = onView( onView(
allOf( allOf(
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
withId(R.id.fragment_main_nav_tab_layout), withId(R.id.fragment_main_nav_tab_layout),
0 0,
), ),
1 1,
),
isDisplayed(),
), ),
isDisplayed()
)
) )
bottomNavigationItemView.perform(click()) bottomNavigationItemView.perform(click())
UITestHelper.sleep(12000) UITestHelper.sleep(12000)
val actionMenuItemView = onView( val actionMenuItemView =
onView(
allOf( allOf(
withId(R.id.list_sheet), withId(R.id.list_sheet),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
withId(R.id.toolbar), withId(R.id.toolbar),
1 1,
), ),
0 0,
),
isDisplayed(),
), ),
isDisplayed()
)
) )
actionMenuItemView.perform(click()) actionMenuItemView.perform(click())
val recyclerView = onView( val recyclerView =
onView(
allOf( allOf(
withId(R.id.rv_nearby_list), withId(R.id.rv_nearby_list),
) ),
) )
recyclerView.perform( recyclerView.perform(
RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>( RecyclerViewActions.actionOnItemAtPosition<RecyclerView.ViewHolder>(
0, 0,
click() click(),
) ),
) )
val linearLayout3 = onView( val linearLayout3 =
onView(
allOf( allOf(
withId(R.id.cameraButton), withId(R.id.cameraButton),
childAtPosition( childAtPosition(
allOf( allOf(
withId(R.id.nearby_button_layout), withId(R.id.nearby_button_layout),
), ),
0 0,
),
isDisplayed(),
), ),
isDisplayed()
)
) )
linearLayout3.perform(click()) linearLayout3.perform(click())
val pasteSensitiveTextInputEditText = onView( val pasteSensitiveTextInputEditText =
onView(
allOf( allOf(
withId(R.id.caption_item_edit_text), withId(R.id.caption_item_edit_text),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
withId(R.id.caption_item_edit_text_input_layout), withId(R.id.caption_item_edit_text_input_layout),
0 0,
), ),
0 0,
),
isDisplayed(),
), ),
isDisplayed()
)
) )
pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard()) pasteSensitiveTextInputEditText.perform(replaceText("test"), closeSoftKeyboard())
val pasteSensitiveTextInputEditText2 = onView( val pasteSensitiveTextInputEditText2 =
onView(
allOf( allOf(
withId(R.id.description_item_edit_text), withId(R.id.description_item_edit_text),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
withId(R.id.description_item_edit_text_input_layout), withId(R.id.description_item_edit_text_input_layout),
0 0,
), ),
0 0,
),
isDisplayed(),
), ),
isDisplayed()
)
) )
pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard()) pasteSensitiveTextInputEditText2.perform(replaceText("test"), closeSoftKeyboard())
val appCompatButton2 = onView( val appCompatButton2 =
onView(
allOf( allOf(
withId(R.id.btn_next), withId(R.id.btn_next),
childAtPosition( childAtPosition(
childAtPosition( childAtPosition(
withId(R.id.ll_container_media_detail), withId(R.id.ll_container_media_detail),
2 2,
), ),
1 1,
),
isDisplayed(),
), ),
isDisplayed()
)
) )
appCompatButton2.perform(click()) appCompatButton2.perform(click())
val appCompatButton3 = onView( val appCompatButton3 =
onView(
allOf( allOf(
withId(android.R.id.button1), withId(android.R.id.button1),
) ),
) )
appCompatButton3.perform(scrollTo(), click()) appCompatButton3.perform(scrollTo(), click())
Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name)) Intents.intended(IntentMatchers.hasComponent(LocationPickerActivity::class.java.name))
val floatingActionButton3 = onView( val floatingActionButton3 =
onView(
allOf( allOf(
withId(R.id.location_chosen_button), withId(R.id.location_chosen_button),
isDisplayed() isDisplayed(),
) ),
) )
UITestHelper.sleep(2000) UITestHelper.sleep(2000)
floatingActionButton3.perform(click()) floatingActionButton3.perform(click())

View file

@ -42,8 +42,11 @@ import java.util.*
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class UploadTest { class UploadTest {
@get:Rule @get:Rule
var permissionRule = GrantPermissionRule.grant(Manifest.permission.WRITE_EXTERNAL_STORAGE, var permissionRule =
Manifest.permission.ACCESS_FINE_LOCATION)!! GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.ACCESS_FINE_LOCATION,
)!!
@get:Rule @get:Rule
var activityRule = ActivityTestRule(LoginActivity::class.java) var activityRule = ActivityTestRule(LoginActivity::class.java)
@ -61,7 +64,6 @@ class UploadTest {
try { try {
Intents.init() Intents.init()
} catch (ex: IllegalStateException) { } catch (ex: IllegalStateException) {
} }
UITestHelper.loginUser() UITestHelper.loginUser()
UITestHelper.skipWelcome() UITestHelper.skipWelcome()
@ -99,7 +101,6 @@ class UploadTest {
onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text))) onView(allOf<View>(isDisplayed(), withId(R.id.description_item_edit_text)))
.perform(replaceText(commonsFileName)) .perform(replaceText(commonsFileName))
onView(allOf(isDisplayed(), withId(R.id.btn_next))) onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click()) .perform(click())
@ -131,7 +132,8 @@ class UploadTest {
UITestHelper.sleep(10000) UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + val fileUrl =
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg" commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl") Timber.i("File should be uploaded to $fileUrl")
} }
@ -200,7 +202,8 @@ class UploadTest {
UITestHelper.sleep(10000) UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + val fileUrl =
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg" commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl") Timber.i("File should be uploaded to $fileUrl")
} }
@ -231,16 +234,22 @@ class UploadTest {
onView(withId(R.id.rv_descriptions)).perform( onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions RecyclerViewActions
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(0, .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"))) 0,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Test description"),
),
)
onView(withId(R.id.btn_add)) onView(withId(R.id.btn_add))
.perform(click()) .perform(click())
onView(withId(R.id.rv_descriptions)).perform( onView(withId(R.id.rv_descriptions)).perform(
RecyclerViewActions RecyclerViewActions
.actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(1, .actionOnItemAtPosition<UploadMediaDetailAdapter.ViewHolder>(
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"))) 1,
MyViewAction.typeTextInChildViewWithId(R.id.description_item_edit_text, "Description"),
),
)
onView(allOf(isDisplayed(), withId(R.id.btn_next))) onView(allOf(isDisplayed(), withId(R.id.btn_next)))
.perform(click()) .perform(click())
@ -273,7 +282,8 @@ class UploadTest {
UITestHelper.sleep(10000) UITestHelper.sleep(10000)
val fileUrl = "https://commons.wikimedia.beta.wmflabs.org/wiki/File:" + val fileUrl =
"https://commons.wikimedia.beta.wmflabs.org/wiki/File:" +
commonsFileName.replace(' ', '_') + ".jpg" commonsFileName.replace(' ', '_') + ".jpg"
Timber.i("File should be uploaded to $fileUrl") Timber.i("File should be uploaded to $fileUrl")
} }
@ -306,7 +316,6 @@ class UploadTest {
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTrace() e.printStackTrace()
} }
} }
} }

View file

@ -3,7 +3,6 @@ package fr.free.nrw.commons
import androidx.test.espresso.Espresso.onView import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions import androidx.test.espresso.action.ViewActions
import androidx.test.espresso.assertion.ViewAssertions.matches import androidx.test.espresso.assertion.ViewAssertions.matches
import androidx.test.espresso.matcher.ViewMatchers
import androidx.test.espresso.matcher.ViewMatchers.isDisplayed import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
import androidx.test.espresso.matcher.ViewMatchers.withId import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
@ -22,7 +21,6 @@ import org.junit.runner.RunWith
@LargeTest @LargeTest
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class WelcomeActivityTest { class WelcomeActivityTest {
@get:Rule @get:Rule
var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java) var activityRule: ActivityTestRule<*> = ActivityTestRule(WelcomeActivity::class.java)

View file

@ -11,7 +11,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class) @RunWith(AndroidJUnit4::class)
class PasteSensitiveTextInputEditTextTest { class PasteSensitiveTextInputEditTextTest {
private var context: Context? = null private var context: Context? = null
private var textView: PasteSensitiveTextInputEditText? = null private var textView: PasteSensitiveTextInputEditText? = null
@ -23,9 +22,13 @@ class PasteSensitiveTextInputEditTextTest {
// this test has no real value, just % for test code coverage // this test has no real value, just % for test code coverage
@Test @Test
fun extractFormattingAttributeSet(){ fun extractFormattingAttributeSet() {
val methodExtractFormattingAttribute = textView!!.javaClass.getDeclaredMethod( val methodExtractFormattingAttribute =
"extractFormattingAttribute", Context::class.java, AttributeSet::class.java) textView!!.javaClass.getDeclaredMethod(
"extractFormattingAttribute",
Context::class.java,
AttributeSet::class.java,
)
methodExtractFormattingAttribute.isAccessible = true methodExtractFormattingAttribute.isAccessible = true
methodExtractFormattingAttribute.invoke(textView, context, null) methodExtractFormattingAttribute.invoke(textView, context, null)
} }

View file

@ -9,56 +9,58 @@ import org.hamcrest.Matcher
class MyViewAction { class MyViewAction {
companion object { companion object {
fun typeTextInChildViewWithId(id: Int, textToBeTyped: String): ViewAction { fun typeTextInChildViewWithId(
return object : ViewAction { id: Int,
override fun getConstraints(): Matcher<View>? { textToBeTyped: String,
return null ): ViewAction =
} object : ViewAction {
override fun getConstraints(): Matcher<View>? = null
override fun getDescription(): String { override fun getDescription(): String = "Click on a child view with specified id."
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) { override fun perform(
uiController: UiController,
view: View,
) {
val v = view.findViewById<View>(id) as EditText val v = view.findViewById<View>(id) as EditText
v.setText(textToBeTyped) v.setText(textToBeTyped)
} }
} }
}
fun selectSpinnerItemInChildViewWithId(id: Int, position: Int): ViewAction { fun selectSpinnerItemInChildViewWithId(
return object : ViewAction { id: Int,
override fun getConstraints(): Matcher<View>? { position: Int,
return null ): ViewAction =
} object : ViewAction {
override fun getConstraints(): Matcher<View>? = null
override fun getDescription(): String { override fun getDescription(): String = "Click on a child view with specified id."
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) { override fun perform(
uiController: UiController,
view: View,
) {
val v = view.findViewById<View>(id) as AppCompatSpinner val v = view.findViewById<View>(id) as AppCompatSpinner
v.setSelection(position) v.setSelection(position)
} }
} }
}
fun clickItemWithId(id: Int, position: Int): ViewAction { fun clickItemWithId(
return object : ViewAction { id: Int,
override fun getConstraints(): Matcher<View>? { position: Int,
return null ): ViewAction =
} object : ViewAction {
override fun getConstraints(): Matcher<View>? = null
override fun getDescription(): String { override fun getDescription(): String = "Click on a child view with specified id."
return "Click on a child view with specified id."
}
override fun perform(uiController: UiController, view: View) { override fun perform(
uiController: UiController,
view: View,
) {
val v = view.findViewById<View>(id) as View val v = view.findViewById<View>(id) as View
v.performClick() v.performClick()
} }
} }
} }
}
} }

View file

@ -39,14 +39,20 @@ class BaseMarker {
constructor() { constructor() {
} }
fun fromResource(context: Context, drawableResId: Int) { fun fromResource(
context: Context,
drawableResId: Int,
) {
val drawable: Drawable = context.resources.getDrawable(drawableResId) val drawable: Drawable = context.resources.getDrawable(drawableResId)
icon = if (drawable is BitmapDrawable) { icon =
if (drawable is BitmapDrawable) {
(drawable as BitmapDrawable).bitmap (drawable as BitmapDrawable).bitmap
} else { } else {
val bitmap = Bitmap.createBitmap( val bitmap =
Bitmap.createBitmap(
drawable.intrinsicWidth, drawable.intrinsicWidth,
drawable.intrinsicHeight, Bitmap.Config.ARGB_8888 drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888,
) )
val canvas = Canvas(bitmap) val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.setBounds(0, 0, canvas.width, canvas.height)
@ -55,10 +61,3 @@ class BaseMarker {
} }
} }
} }

View file

@ -10,6 +10,7 @@ object BetaConstants {
* production server where beta server does not work * production server where beta server does not work
*/ */
const val COMMONS_URL = "https://commons.wikimedia.org/" const val COMMONS_URL = "https://commons.wikimedia.org/"
/** /**
* Commons production's depicts property which is used in beta for some specific GET calls on * Commons production's depicts property which is used in beta for some specific GET calls on
* production server where beta server does not work * production server where beta server does not work

View file

@ -3,31 +3,31 @@ package fr.free.nrw.commons
import android.os.Parcel import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
class CameraPosition(val latitude: Double, val longitude: Double, val zoom: Double) : Parcelable { class CameraPosition(
val latitude: Double,
val longitude: Double,
val zoom: Double,
) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readDouble(), parcel.readDouble(),
parcel.readDouble(), parcel.readDouble(),
parcel.readDouble() parcel.readDouble(),
) )
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(
parcel: Parcel,
flags: Int,
) {
parcel.writeDouble(latitude) parcel.writeDouble(latitude)
parcel.writeDouble(longitude) parcel.writeDouble(longitude)
parcel.writeDouble(zoom) parcel.writeDouble(zoom)
} }
override fun describeContents(): Int { override fun describeContents(): Int = 0
return 0
}
companion object CREATOR : Parcelable.Creator<CameraPosition> { companion object CREATOR : Parcelable.Creator<CameraPosition> {
override fun createFromParcel(parcel: Parcel): CameraPosition { override fun createFromParcel(parcel: Parcel): CameraPosition = CameraPosition(parcel)
return CameraPosition(parcel)
}
override fun newArray(size: Int): Array<CameraPosition?> { override fun newArray(size: Int): Array<CameraPosition?> = arrayOfNulls(size)
return arrayOfNulls(size)
}
} }
} }

View file

@ -2,8 +2,8 @@ package fr.free.nrw.commons
import android.os.Parcelable import android.os.Parcelable
import fr.free.nrw.commons.location.LatLng import fr.free.nrw.commons.location.LatLng
import kotlinx.parcelize.Parcelize
import fr.free.nrw.commons.wikidata.model.page.PageTitle import fr.free.nrw.commons.wikidata.model.page.PageTitle
import kotlinx.parcelize.Parcelize
import java.util.* import java.util.*
@Parcelize @Parcelize
@ -14,7 +14,6 @@ class Media constructor(
*/ */
var pageId: String = UUID.randomUUID().toString(), var pageId: String = UUID.randomUUID().toString(),
var thumbUrl: String? = null, var thumbUrl: String? = null,
/** /**
* Gets image URL * Gets image URL
* @return Image URL * @return Image URL
@ -35,7 +34,6 @@ class Media constructor(
* @param fallbackDescription the new description of the file * @param fallbackDescription the new description of the file
*/ */
var fallbackDescription: String? = null, var fallbackDescription: String? = null,
/** /**
* Gets the upload date of the file. * Gets the upload date of the file.
* Can be null. * Can be null.
@ -62,9 +60,7 @@ class Media constructor(
* @param author creator name as a string * @param author creator name as a string
*/ */
var author: String? = null, var author: String? = null,
var user: String? = null,
var user:String?=null,
/** /**
* Gets the categories the file falls under. * Gets the categories the file falls under.
* @return file categories as an ArrayList of Strings * @return file categories as an ArrayList of Strings
@ -83,23 +79,23 @@ class Media constructor(
* Stores the mapping of category title to hidden attribute * Stores the mapping of category title to hidden attribute
* Example: "Mountains" => false, "CC-BY-SA-2.0" => true * Example: "Mountains" => false, "CC-BY-SA-2.0" => true
*/ */
var categoriesHiddenStatus: Map<String, Boolean> = emptyMap() var categoriesHiddenStatus: Map<String, Boolean> = emptyMap(),
) : Parcelable { ) : Parcelable {
constructor( constructor(
captions: Map<String, String>, captions: Map<String, String>,
categories: List<String>?, categories: List<String>?,
filename: String?, filename: String?,
fallbackDescription: String?, fallbackDescription: String?,
author: String?, user:String? author: String?,
user: String?,
) : this( ) : this(
filename = filename, filename = filename,
fallbackDescription = fallbackDescription, fallbackDescription = fallbackDescription,
dateUploaded = Date(), dateUploaded = Date(),
author = author, author = author,
user=user, user = user,
categories = categories, categories = categories,
captions = captions captions = captions,
) )
/** /**
@ -108,10 +104,11 @@ class Media constructor(
*/ */
val displayTitle: String val displayTitle: String
get() = get() =
if (filename != null) if (filename != null) {
pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "") pageTitle.displayTextWithoutNamespace.replaceFirst("[.][^.]+$".toRegex(), "")
else } else {
"" ""
}
/** /**
* Gets file page title * Gets file page title
@ -127,7 +124,8 @@ class Media constructor(
get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption) get() = String.format("[[%s|thumb|%s]]", filename, mostRelevantCaption)
val mostRelevantCaption: String val mostRelevantCaption: String
get() = captions[Locale.getDefault().language] get() =
captions[Locale.getDefault().language]
?: captions.values.firstOrNull() ?: captions.values.firstOrNull()
?: displayTitle ?: displayTitle
@ -139,5 +137,7 @@ class Media constructor(
// TODO added categories should be removed. It is added for a short fix. On category update, // TODO added categories should be removed. It is added for a short fix. On category update,
// categories should be re-fetched instead // categories should be re-fetched instead
get() = field // getter get() = field // getter
set(value) { field = value } // setter set(value) {
field = value
} // setter
} }

View file

@ -1,9 +1,9 @@
package fr.free.nrw.commons package fr.free.nrw.commons
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
import fr.free.nrw.commons.media.IdAndCaptions import fr.free.nrw.commons.media.IdAndCaptions
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import fr.free.nrw.commons.media.PAGE_ID_PREFIX
import io.reactivex.Single import io.reactivex.Single
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -17,42 +17,46 @@ import javax.inject.Singleton
* to the media and may change due to editing. * to the media and may change due to editing.
*/ */
@Singleton @Singleton
class MediaDataExtractor @Inject constructor(private val mediaClient: MediaClient) { class MediaDataExtractor
@Inject
constructor(
private val mediaClient: MediaClient,
) {
fun fetchDepictionIdsAndLabels(media: Media) = fun fetchDepictionIdsAndLabels(media: Media) =
mediaClient.getEntities(media.depictionIds) mediaClient
.getEntities(media.depictionIds)
.map { .map {
it.entities() it
.entities()
.mapValues { entry -> entry.value.labels().mapValues { it.value.value() } } .mapValues { entry -> entry.value.labels().mapValues { it.value.value() } }
} }.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
.map { it.map { (key, value) -> IdAndCaptions(key, value) } }
.onErrorReturn { emptyList() } .onErrorReturn { emptyList() }
fun checkDeletionRequestExists(media: Media) = fun checkDeletionRequestExists(media: Media) = mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
mediaClient.checkPageExistsUsingTitle("Commons:Deletion_requests/" + media.filename)
fun fetchDiscussion(media: Media) = fun fetchDiscussion(media: Media) =
mediaClient.getPageHtml(media.filename!!.replace("File", "File talk")) mediaClient
.getPageHtml(media.filename!!.replace("File", "File talk"))
.map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() } .map { HtmlCompat.fromHtml(it, HtmlCompat.FROM_HTML_MODE_LEGACY).toString() }
.onErrorReturn { .onErrorReturn {
Timber.d("Error occurred while fetching discussion") Timber.d("Error occurred while fetching discussion")
"" ""
} }
fun refresh(media: Media): Single<Media> { fun refresh(media: Media): Single<Media> =
return Single.ambArray( Single.ambArray(
mediaClient.getMediaById(PAGE_ID_PREFIX + media.pageId) mediaClient
.getMediaById(PAGE_ID_PREFIX + media.pageId)
.onErrorResumeNext { Single.never() },
mediaClient
.getMediaSuppressingErrors(media.filename)
.onErrorResumeNext { Single.never() }, .onErrorResumeNext { Single.never() },
mediaClient.getMediaSuppressingErrors(media.filename)
.onErrorResumeNext { Single.never() }
) )
} fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title)
fun getHtmlOfPage(title: String) = mediaClient.getPageHtml(title);
/** /**
* Fetches wikitext from mediaClient * Fetches wikitext from mediaClient
*/ */
fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title); fun getCurrentWikiText(title: String) = mediaClient.getCurrentWikiText(title)
} }

View file

@ -1,10 +1,9 @@
package fr.free.nrw.commons.actions package fr.free.nrw.commons.actions
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import timber.log.Timber
/** /**
* This class acts as a Client to facilitate wiki page editing * This class acts as a Client to facilitate wiki page editing
@ -15,9 +14,8 @@ import timber.log.Timber
*/ */
class PageEditClient( class PageEditClient(
private val csrfTokenClient: CsrfTokenClient, private val csrfTokenClient: CsrfTokenClient,
private val pageEditInterface: PageEditInterface private val pageEditInterface: PageEditInterface,
) { ) {
/** /**
* Replace the content of a wiki page * Replace the content of a wiki page
* @param pageTitle Title of the page to edit * @param pageTitle Title of the page to edit
@ -25,9 +23,14 @@ class PageEditClient(
* @param summary Edit summary * @param summary Edit summary
* @return whether the edit was successful * @return whether the edit was successful
*/ */
fun edit(pageTitle: String, text: String, summary: String): Observable<Boolean> { fun edit(
return try { pageTitle: String,
pageEditInterface.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking()) text: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postEdit(pageTitle, summary, text, csrfTokenClient.getTokenBlocking())
.map { editResponse -> .map { editResponse ->
editResponse.edit()!!.editSucceeded() editResponse.edit()!!.editSucceeded()
} }
@ -38,7 +41,6 @@ class PageEditClient(
Observable.just(false) Observable.just(false)
} }
} }
}
/** /**
* Creates a new page with the given title, text, and summary. * Creates a new page with the given title, text, and summary.
@ -49,9 +51,14 @@ class PageEditClient(
* @return An observable that emits true if the page creation succeeded, false otherwise. * @return An observable that emits true if the page creation succeeded, false otherwise.
* @throws InvalidLoginTokenException If an invalid login token is encountered during the process. * @throws InvalidLoginTokenException If an invalid login token is encountered during the process.
*/ */
fun postCreate(pageTitle: String, text: String, summary: String): Observable<Boolean> { fun postCreate(
return try { pageTitle: String,
pageEditInterface.postCreate( text: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postCreate(
pageTitle, pageTitle,
summary, summary,
text, text,
@ -59,7 +66,7 @@ class PageEditClient(
"wikitext", "wikitext",
true, true,
true, true,
csrfTokenClient.getTokenBlocking() csrfTokenClient.getTokenBlocking(),
).map { editResponse -> ).map { editResponse ->
editResponse.edit()!!.editSucceeded() editResponse.edit()!!.editSucceeded()
} }
@ -70,7 +77,6 @@ class PageEditClient(
Observable.just(false) Observable.just(false)
} }
} }
}
/** /**
* Append text to the end of a wiki page * Append text to the end of a wiki page
@ -79,9 +85,14 @@ class PageEditClient(
* @param summary Edit summary * @param summary Edit summary
* @return whether the edit was successful * @return whether the edit was successful
*/ */
fun appendEdit(pageTitle: String, appendText: String, summary: String): Observable<Boolean> { fun appendEdit(
return try { pageTitle: String,
pageEditInterface.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking()) appendText: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postAppendEdit(pageTitle, summary, appendText, csrfTokenClient.getTokenBlocking())
.map { editResponse -> editResponse.edit()!!.editSucceeded() } .map { editResponse -> editResponse.edit()!!.editSucceeded() }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) { if (throwable is InvalidLoginTokenException) {
@ -90,7 +101,6 @@ class PageEditClient(
Observable.just(false) Observable.just(false)
} }
} }
}
/** /**
* Prepend text to the beginning of a wiki page * Prepend text to the beginning of a wiki page
@ -99,9 +109,14 @@ class PageEditClient(
* @param summary Edit summary * @param summary Edit summary
* @return whether the edit was successful * @return whether the edit was successful
*/ */
fun prependEdit(pageTitle: String, prependText: String, summary: String): Observable<Boolean> { fun prependEdit(
return try { pageTitle: String,
pageEditInterface.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking()) prependText: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postPrependEdit(pageTitle, summary, prependText, csrfTokenClient.getTokenBlocking())
.map { editResponse -> editResponse.edit()?.editSucceeded() ?: false } .map { editResponse -> editResponse.edit()?.editSucceeded() ?: false }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) { if (throwable is InvalidLoginTokenException) {
@ -110,8 +125,6 @@ class PageEditClient(
Observable.just(false) Observable.just(false)
} }
} }
}
/** /**
* Appends a new section to the wiki page * Appends a new section to the wiki page
@ -121,9 +134,15 @@ class PageEditClient(
* @param summary Edit summary * @param summary Edit summary
* @return whether the edit was successful * @return whether the edit was successful
*/ */
fun createNewSection(pageTitle: String, sectionTitle: String, sectionText: String, summary: String): Observable<Boolean> { fun createNewSection(
return try { pageTitle: String,
pageEditInterface.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking()) sectionTitle: String,
sectionText: String,
summary: String,
): Observable<Boolean> =
try {
pageEditInterface
.postNewSection(pageTitle, summary, sectionTitle, sectionText, csrfTokenClient.getTokenBlocking())
.map { editResponse -> editResponse.edit()!!.editSucceeded() } .map { editResponse -> editResponse.edit()!!.editSucceeded() }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) { if (throwable is InvalidLoginTokenException) {
@ -132,8 +151,6 @@ class PageEditClient(
Observable.just(false) Observable.just(false)
} }
} }
}
/** /**
* Set new labels to Wikibase server of commons * Set new labels to Wikibase server of commons
@ -143,11 +160,20 @@ class PageEditClient(
* @param value label * @param value label
* @return 1 when the edit was successful * @return 1 when the edit was successful
*/ */
fun setCaptions(summary: String, title: String, fun setCaptions(
language: String, value: String) : Observable<Int>{ summary: String,
return try { title: String,
pageEditInterface.postCaptions(summary, title, language, language: String,
value, csrfTokenClient.getTokenBlocking() value: String,
): Observable<Int> =
try {
pageEditInterface
.postCaptions(
summary,
title,
language,
value,
csrfTokenClient.getTokenBlocking(),
).map { it.success } ).map { it.success }
} catch (throwable: Throwable) { } catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) { if (throwable is InvalidLoginTokenException) {
@ -156,16 +182,20 @@ class PageEditClient(
Observable.just(0) Observable.just(0)
} }
} }
}
/** /**
* Get whole WikiText of required file * Get whole WikiText of required file
* @param title : Name of the file * @param title : Name of the file
* @return Observable<MwQueryResult> * @return Observable<MwQueryResult>
*/ */
fun getCurrentWikiText(title: String): Single<String?> { fun getCurrentWikiText(title: String): Single<String?> =
return pageEditInterface.getWikiText(title).map { pageEditInterface.getWikiText(title).map {
it.query()?.pages()?.get(0)?.revisions()?.get(0)?.content() it
} .query()
?.pages()
?.get(0)
?.revisions()
?.get(0)
?.content()
} }
} }

View file

@ -3,9 +3,9 @@ package fr.free.nrw.commons.actions
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import fr.free.nrw.commons.wikidata.model.Entities import fr.free.nrw.commons.wikidata.model.Entities
import fr.free.nrw.commons.wikidata.model.edit.Edit import fr.free.nrw.commons.wikidata.model.edit.Edit
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.Single import io.reactivex.Single
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.http.* import retrofit2.http.*
/** /**
@ -33,7 +33,7 @@ interface PageEditInterface {
@Field("summary") summary: String, @Field("summary") summary: String,
@Field("text") text: String, @Field("text") text: String,
// NOTE: This csrf shold always be sent as the last field of form data // NOTE: This csrf shold always be sent as the last field of form data
@Field("token") token: String @Field("token") token: String,
): Observable<Edit> ): Observable<Edit>
/** /**
@ -60,7 +60,7 @@ interface PageEditInterface {
@Field("minor") minor: Boolean, @Field("minor") minor: Boolean,
@Field("recreate") recreate: Boolean, @Field("recreate") recreate: Boolean,
// NOTE: This csrf shold always be sent as the last field of form data // NOTE: This csrf shold always be sent as the last field of form data
@Field("token") token: String @Field("token") token: String,
): Observable<Edit> ): Observable<Edit>
/** /**
@ -79,7 +79,7 @@ interface PageEditInterface {
@Field("title") title: String, @Field("title") title: String,
@Field("summary") summary: String, @Field("summary") summary: String,
@Field("appendtext") appendText: String, @Field("appendtext") appendText: String,
@Field("token") token: String @Field("token") token: String,
): Observable<Edit> ): Observable<Edit>
/** /**
@ -98,7 +98,7 @@ interface PageEditInterface {
@Field("title") title: String, @Field("title") title: String,
@Field("summary") summary: String, @Field("summary") summary: String,
@Field("prependtext") prependText: String, @Field("prependtext") prependText: String,
@Field("token") token: String @Field("token") token: String,
): Observable<Edit> ): Observable<Edit>
@FormUrlEncoded @FormUrlEncoded
@ -109,7 +109,7 @@ interface PageEditInterface {
@Field("summary") summary: String, @Field("summary") summary: String,
@Field("sectiontitle") sectionTitle: String, @Field("sectiontitle") sectionTitle: String,
@Field("text") sectionText: String, @Field("text") sectionText: String,
@Field("token") token: String @Field("token") token: String,
): Observable<Edit> ): Observable<Edit>
@FormUrlEncoded @FormUrlEncoded
@ -120,7 +120,7 @@ interface PageEditInterface {
@Field("title") title: String, @Field("title") title: String,
@Field("language") language: String, @Field("language") language: String,
@Field("value") value: String, @Field("value") value: String,
@Field("token") token: String @Field("token") token: String,
): Observable<Entities> ): Observable<Entities>
/** /**
@ -130,6 +130,6 @@ interface PageEditInterface {
*/ */
@GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=") @GET(MW_API_PREFIX + "action=query&prop=revisions&rvprop=content|timestamp&rvlimit=1&converttitles=")
fun getWikiText( fun getWikiText(
@Query("titles") title: String @Query("titles") title: String,
): Single<MwQueryResponse?> ): Single<MwQueryResponse?>
} }

View file

@ -1,11 +1,10 @@
package fr.free.nrw.commons.actions package fr.free.nrw.commons.actions
import fr.free.nrw.commons.CommonsApplication import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
import io.reactivex.Observable
import fr.free.nrw.commons.auth.csrf.CsrfTokenClient import fr.free.nrw.commons.auth.csrf.CsrfTokenClient
import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException import fr.free.nrw.commons.auth.csrf.InvalidLoginTokenException
import fr.free.nrw.commons.auth.login.LoginFailedException import fr.free.nrw.commons.di.NetworkingModule.NAMED_COMMONS_CSRF
import io.reactivex.Observable
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
import javax.inject.Singleton import javax.inject.Singleton
@ -15,34 +14,33 @@ import javax.inject.Singleton
* Thanks are used by a user to show gratitude to another user for their contributions * Thanks are used by a user to show gratitude to another user for their contributions
*/ */
@Singleton @Singleton
class ThanksClient @Inject constructor( class ThanksClient
@Inject
constructor(
@param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient, @param:Named(NAMED_COMMONS_CSRF) private val csrfTokenClient: CsrfTokenClient,
private val service: ThanksInterface private val service: ThanksInterface,
) { ) {
/** /**
* Thanks a user for a particular revision * Thanks a user for a particular revision
* @param revisionId The revision ID the user would like to thank someone for * @param revisionId The revision ID the user would like to thank someone for
* @return if thanks was successfully sent to intended recipient * @return if thanks was successfully sent to intended recipient
*/ */
fun thank(revisionId: Long): Observable<Boolean> { fun thank(revisionId: Long): Observable<Boolean> =
return try { try {
service.thank( service
.thank(
revisionId.toString(), // Rev revisionId.toString(), // Rev
null, // Log null, // Log
csrfTokenClient.getTokenBlocking(), // Token csrfTokenClient.getTokenBlocking(), // Token
CommonsApplication.getInstance().userAgent // Source CommonsApplication.getInstance().userAgent, // Source
).map { ).map { mwThankPostResponse ->
mwThankPostResponse -> mwThankPostResponse.result?.success == 1 mwThankPostResponse.result?.success == 1
} }
} } catch (throwable: Throwable) {
catch (throwable: Throwable) {
if (throwable is InvalidLoginTokenException) { if (throwable is InvalidLoginTokenException) {
Observable.error(throwable) Observable.error(throwable)
} } else {
else {
Observable.just(false) Observable.just(false)
} }
} }
} }
}

View file

@ -19,6 +19,6 @@ interface ThanksInterface {
@Field("rev") rev: String?, @Field("rev") rev: String?,
@Field("log") log: String?, @Field("log") log: String?,
@Field("token") token: String, @Field("token") token: String,
@Field("source") source: String? @Field("source") source: String?,
): Observable<MwThankPostResponse?> ): Observable<MwThankPostResponse?>
} }

View file

@ -2,11 +2,11 @@ package fr.free.nrw.commons.auth.csrf
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginCallback import fr.free.nrw.commons.auth.login.LoginCallback
import fr.free.nrw.commons.auth.login.LoginClient
import fr.free.nrw.commons.auth.login.LoginFailedException import fr.free.nrw.commons.auth.login.LoginFailedException
import fr.free.nrw.commons.auth.login.LoginResult import fr.free.nrw.commons.auth.login.LoginResult
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.Call import retrofit2.Call
import retrofit2.Response import retrofit2.Response
import timber.log.Timber import timber.log.Timber
@ -17,12 +17,11 @@ class CsrfTokenClient(
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val csrfTokenInterface: CsrfTokenInterface, private val csrfTokenInterface: CsrfTokenInterface,
private val loginClient: LoginClient, private val loginClient: LoginClient,
private val logoutClient: LogoutClient private val logoutClient: LogoutClient,
) { ) {
private var retries = 0 private var retries = 0
private var csrfTokenCall: Call<MwQueryResponse?>? = null private var csrfTokenCall: Call<MwQueryResponse?>? = null
@Throws(Throwable::class) @Throws(Throwable::class)
fun getTokenBlocking(): String { fun getTokenBlocking(): String {
var token = "" var token = ""
@ -37,11 +36,20 @@ class CsrfTokenClient(
} }
// Get CSRFToken response off the main thread. // Get CSRFToken response off the main thread.
val response = newSingleThreadExecutor().submit(Callable { val response =
newSingleThreadExecutor()
.submit(
Callable {
csrfTokenInterface.getCsrfTokenCall().execute() csrfTokenInterface.getCsrfTokenCall().execute()
}).get() },
).get()
if (response.body()?.query()?.csrfToken().isNullOrEmpty()) { if (response
.body()
?.query()
?.csrfToken()
.isNullOrEmpty()
) {
continue continue
} }
@ -52,8 +60,7 @@ class CsrfTokenClient(
break break
} catch (e: LoginFailedException) { } catch (e: LoginFailedException) {
throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE) throw InvalidLoginTokenException(ANONYMOUS_TOKEN_MESSAGE)
} } catch (t: Throwable) {
catch (t: Throwable) {
Timber.w(t) Timber.w(t)
} }
} }
@ -65,8 +72,13 @@ class CsrfTokenClient(
} }
@VisibleForTesting @VisibleForTesting
fun request(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> = fun request(
requestToken(service, object : Callback { service: CsrfTokenInterface,
cb: Callback,
): Call<MwQueryResponse?> =
requestToken(
service,
object : Callback {
override fun success(token: String?) { override fun success(token: String?) {
if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) { if (sessionManager.isUserLoggedIn && token == ANON_TOKEN) {
retryWithLogin(cb) { retryWithLogin(cb) {
@ -80,30 +92,45 @@ class CsrfTokenClient(
override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught } override fun failure(caught: Throwable?) = retryWithLogin(cb) { caught }
override fun twoFactorPrompt() = cb.twoFactorPrompt() override fun twoFactorPrompt() = cb.twoFactorPrompt()
}) },
)
@VisibleForTesting @VisibleForTesting
fun requestToken(service: CsrfTokenInterface, cb: Callback): Call<MwQueryResponse?> { fun requestToken(
service: CsrfTokenInterface,
cb: Callback,
): Call<MwQueryResponse?> {
val call = service.getCsrfTokenCall() val call = service.getCsrfTokenCall()
call.enqueue(object : retrofit2.Callback<MwQueryResponse?> { call.enqueue(
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) { object : retrofit2.Callback<MwQueryResponse?> {
override fun onResponse(
call: Call<MwQueryResponse?>,
response: Response<MwQueryResponse?>,
) {
if (call.isCanceled) { if (call.isCanceled) {
return return
} }
cb.success(response.body()!!.query()!!.csrfToken()) cb.success(response.body()!!.query()!!.csrfToken())
} }
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) { override fun onFailure(
call: Call<MwQueryResponse?>,
t: Throwable,
) {
if (call.isCanceled) { if (call.isCanceled) {
return return
} }
cb.failure(t) cb.failure(t)
} }
}) },
)
return call return call
} }
private fun retryWithLogin(callback: Callback, caught: () -> Throwable?) { private fun retryWithLogin(
callback: Callback,
caught: () -> Throwable?,
) {
val userName = sessionManager.userName val userName = sessionManager.userName
val password = sessionManager.password val password = sessionManager.password
if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) { if (retries < MAX_RETRIES && !userName.isNullOrEmpty() && !password.isNullOrEmpty()) {
@ -123,8 +150,11 @@ class CsrfTokenClient(
username: String, username: String,
password: String, password: String,
callback: Callback, callback: Callback,
retryCallback: () -> Unit retryCallback: () -> Unit,
) = loginClient.request(username, password, object : LoginCallback { ) = loginClient.request(
username,
password,
object : LoginCallback {
override fun success(loginResult: LoginResult) { override fun success(loginResult: LoginResult) {
if (loginResult.pass) { if (loginResult.pass) {
sessionManager.updateAccount(loginResult) sessionManager.updateAccount(loginResult)
@ -134,15 +164,17 @@ class CsrfTokenClient(
} }
} }
override fun twoFactorPrompt(caught: Throwable, token: String?) = override fun twoFactorPrompt(
callback.twoFactorPrompt() caught: Throwable,
token: String?,
) = callback.twoFactorPrompt()
// Should not happen here, but call the callback just in case. // Should not happen here, but call the callback just in case.
override fun passwordResetPrompt(token: String?) = override fun passwordResetPrompt(token: String?) = callback.failure(LoginFailedException("Logged in with temporary password."))
callback.failure(LoginFailedException("Logged in with temporary password."))
override fun error(caught: Throwable) = callback.failure(caught) override fun error(caught: Throwable) = callback.failure(caught)
}) },
)
private fun cancel() { private fun cancel() {
loginClient.cancel() loginClient.cancel()
@ -154,7 +186,9 @@ class CsrfTokenClient(
interface Callback { interface Callback {
fun success(token: String?) fun success(token: String?)
fun failure(caught: Throwable?) fun failure(caught: Throwable?)
fun twoFactorPrompt() fun twoFactorPrompt()
} }
@ -166,5 +200,7 @@ class CsrfTokenClient(
const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token." const val ANONYMOUS_TOKEN_MESSAGE = "App believes we're logged in, but got anonymous token."
} }
} }
class InvalidLoginTokenException(message: String) : Exception(message)
class InvalidLoginTokenException(
message: String,
) : Exception(message)

View file

@ -3,6 +3,10 @@ package fr.free.nrw.commons.auth.csrf
import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage import fr.free.nrw.commons.wikidata.cookies.CommonsCookieStorage
import javax.inject.Inject import javax.inject.Inject
class LogoutClient @Inject constructor(private val store: CommonsCookieStorage) { class LogoutClient
@Inject
constructor(
private val store: CommonsCookieStorage,
) {
fun logout() = store.clear() fun logout() = store.clear()
} }

View file

@ -2,7 +2,13 @@ package fr.free.nrw.commons.auth.login
interface LoginCallback { interface LoginCallback {
fun success(loginResult: LoginResult) fun success(loginResult: LoginResult)
fun twoFactorPrompt(caught: Throwable, token: String?)
fun twoFactorPrompt(
caught: Throwable,
token: String?,
)
fun passwordResetPrompt(token: String?) fun passwordResetPrompt(token: String?)
fun error(caught: Throwable) fun error(caught: Throwable)
} }

View file

@ -4,9 +4,9 @@ import android.text.TextUtils
import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult import fr.free.nrw.commons.auth.login.LoginResult.OAuthResult
import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult import fr.free.nrw.commons.auth.login.LoginResult.ResetPasswordResult
import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL import fr.free.nrw.commons.wikidata.WikidataConstants.WIKIPEDIA_URL
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.schedulers.Schedulers import io.reactivex.schedulers.Schedulers
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import retrofit2.Call import retrofit2.Call
import retrofit2.Callback import retrofit2.Callback
import retrofit2.Response import retrofit2.Response
@ -16,7 +16,9 @@ import java.io.IOException
/** /**
* Responsible for making login related requests to the server. * Responsible for making login related requests to the server.
*/ */
class LoginClient(private val loginInterface: LoginInterface) { class LoginClient(
private val loginInterface: LoginInterface,
) {
private var tokenCall: Call<MwQueryResponse?>? = null private var tokenCall: Call<MwQueryResponse?>? = null
private var loginCall: Call<LoginResponse?>? = null private var loginCall: Call<LoginResponse?>? = null
@ -30,45 +32,75 @@ class LoginClient(private val loginInterface: LoginInterface) {
private fun getLoginToken() = loginInterface.getLoginToken() private fun getLoginToken() = loginInterface.getLoginToken()
fun request(userName: String, password: String, cb: LoginCallback) { fun request(
userName: String,
password: String,
cb: LoginCallback,
) {
cancel() cancel()
tokenCall = getLoginToken() tokenCall = getLoginToken()
tokenCall!!.enqueue(object : Callback<MwQueryResponse?> { tokenCall!!.enqueue(
override fun onResponse(call: Call<MwQueryResponse?>, response: Response<MwQueryResponse?>) { object : Callback<MwQueryResponse?> {
override fun onResponse(
call: Call<MwQueryResponse?>,
response: Response<MwQueryResponse?>,
) {
login( login(
userName, password, null, null, response.body()!!.query()!!.loginToken(), userName,
userLanguage, cb password,
null,
null,
response.body()!!.query()!!.loginToken(),
userLanguage,
cb,
) )
} }
override fun onFailure(call: Call<MwQueryResponse?>, caught: Throwable) { override fun onFailure(
call: Call<MwQueryResponse?>,
caught: Throwable,
) {
if (call.isCanceled) { if (call.isCanceled) {
return return
} }
cb.error(caught) cb.error(caught)
} }
}) },
}
fun login(
userName: String, password: String, retypedPassword: String?, twoFactorCode: String?,
loginToken: String?, userLanguage: String, cb: LoginCallback
) {
this.userLanguage = userLanguage
loginCall = if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
userName, password, retypedPassword, twoFactorCode, loginToken, userLanguage, true
) )
} }
loginCall!!.enqueue(object : Callback<LoginResponse?> { fun login(
userName: String,
password: String,
retypedPassword: String?,
twoFactorCode: String?,
loginToken: String?,
userLanguage: String,
cb: LoginCallback,
) {
this.userLanguage = userLanguage
loginCall =
if (twoFactorCode.isNullOrEmpty() && retypedPassword.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else {
loginInterface.postLogIn(
userName,
password,
retypedPassword,
twoFactorCode,
loginToken,
userLanguage,
true,
)
}
loginCall!!.enqueue(
object : Callback<LoginResponse?> {
override fun onResponse( override fun onResponse(
call: Call<LoginResponse?>, call: Call<LoginResponse?>,
response: Response<LoginResponse?> response: Response<LoginResponse?>,
) { ) {
val loginResult = response.body()?.toLoginResult(password) val loginResult = response.body()?.toLoginResult(password)
if (loginResult != null) { if (loginResult != null) {
@ -78,15 +110,17 @@ class LoginClient(private val loginInterface: LoginInterface) {
getExtendedInfo(loginResult.userName, loginResult, cb) getExtendedInfo(loginResult.userName, loginResult, cb)
} else if ("UI" == loginResult.status) { } else if ("UI" == loginResult.status) {
when (loginResult) { when (loginResult) {
is OAuthResult -> cb.twoFactorPrompt( is OAuthResult ->
cb.twoFactorPrompt(
LoginFailedException(loginResult.message), LoginFailedException(loginResult.message),
loginToken loginToken,
) )
is ResetPasswordResult -> cb.passwordResetPrompt(loginToken) is ResetPasswordResult -> cb.passwordResetPrompt(loginToken)
is LoginResult.Result -> cb.error( is LoginResult.Result ->
LoginFailedException(loginResult.message) cb.error(
LoginFailedException(loginResult.message),
) )
} }
} else { } else {
@ -97,13 +131,17 @@ class LoginClient(private val loginInterface: LoginInterface) {
} }
} }
override fun onFailure(call: Call<LoginResponse?>, t: Throwable) { override fun onFailure(
call: Call<LoginResponse?>,
t: Throwable,
) {
if (call.isCanceled) { if (call.isCanceled) {
return return
} }
cb.error(t) cb.error(t)
} }
}) },
)
} }
fun doLogin( fun doLogin(
@ -111,13 +149,14 @@ class LoginClient(private val loginInterface: LoginInterface) {
password: String, password: String,
twoFactorCode: String, twoFactorCode: String,
userLanguage: String, userLanguage: String,
loginCallback: LoginCallback loginCallback: LoginCallback,
) { ) {
getLoginToken().enqueue(object :Callback<MwQueryResponse?>{ getLoginToken().enqueue(
object : Callback<MwQueryResponse?> {
override fun onResponse( override fun onResponse(
call: Call<MwQueryResponse?>, call: Call<MwQueryResponse?>,
response: Response<MwQueryResponse?> response: Response<MwQueryResponse?>,
) = if (response.isSuccessful){ ) = if (response.isSuccessful) {
val loginToken = response.body()?.query()?.loginToken() val loginToken = response.body()?.query()?.loginToken()
loginToken?.let { loginToken?.let {
login(username, password, null, twoFactorCode, it, userLanguage, loginCallback) login(username, password, null, twoFactorCode, it, userLanguage, loginCallback)
@ -128,24 +167,45 @@ class LoginClient(private val loginInterface: LoginInterface) {
loginCallback.error(IOException("Failed to retrieve login token")) loginCallback.error(IOException("Failed to retrieve login token"))
} }
override fun onFailure(call: Call<MwQueryResponse?>, t: Throwable) { override fun onFailure(
call: Call<MwQueryResponse?>,
t: Throwable,
) {
loginCallback.error(t) loginCallback.error(t)
} }
}) },
)
} }
@Throws(Throwable::class) @Throws(Throwable::class)
fun loginBlocking(userName: String, password: String, twoFactorCode: String?) { fun loginBlocking(
userName: String,
password: String,
twoFactorCode: String?,
) {
val tokenResponse = getLoginToken().execute() val tokenResponse = getLoginToken().execute()
if (tokenResponse.body()?.query()?.loginToken().isNullOrEmpty()) { if (tokenResponse
.body()
?.query()
?.loginToken()
.isNullOrEmpty()
) {
throw IOException("Unexpected response when getting login token.") throw IOException("Unexpected response when getting login token.")
} }
val loginToken = tokenResponse.body()?.query()?.loginToken() val loginToken = tokenResponse.body()?.query()?.loginToken()
val tempLoginCall = if (twoFactorCode.isNullOrEmpty()) { val tempLoginCall =
if (twoFactorCode.isNullOrEmpty()) {
loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL) loginInterface.postLogIn(userName, password, loginToken, userLanguage, WIKIPEDIA_URL)
} else { } else {
loginInterface.postLogIn( loginInterface.postLogIn(
userName, password, null, twoFactorCode, loginToken, userLanguage, true userName,
password,
null,
twoFactorCode,
loginToken,
userLanguage,
true,
) )
} }
@ -166,9 +226,14 @@ class LoginClient(private val loginInterface: LoginInterface) {
} }
} }
private fun getExtendedInfo(userName: String, loginResult: LoginResult, cb: LoginCallback) = private fun getExtendedInfo(
loginInterface.getUserInfo(userName) userName: String,
.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) loginResult: LoginResult,
cb: LoginCallback,
) = loginInterface
.getUserInfo(userName)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ response: MwQueryResponse? -> .subscribe({ response: MwQueryResponse? ->
loginResult.userId = response?.query()?.userInfo()?.id() ?: 0 loginResult.userId = response?.query()?.userInfo()?.id() ?: 0
loginResult.groups = loginResult.groups =

View file

@ -1,3 +1,5 @@
package fr.free.nrw.commons.auth.login package fr.free.nrw.commons.auth.login
class LoginFailedException(message: String?) : Throwable(message) class LoginFailedException(
message: String?,
) : Throwable(message)

View file

@ -1,8 +1,8 @@
package fr.free.nrw.commons.auth.login package fr.free.nrw.commons.auth.login
import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX import fr.free.nrw.commons.wikidata.WikidataConstants.MW_API_PREFIX
import io.reactivex.Observable
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.Observable
import retrofit2.Call import retrofit2.Call
import retrofit2.http.Field import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded import retrofit2.http.FormUrlEncoded
@ -24,7 +24,7 @@ interface LoginInterface {
@Field("password") pass: String?, @Field("password") pass: String?,
@Field("logintoken") token: String?, @Field("logintoken") token: String?,
@Field("uselang") userLanguage: String?, @Field("uselang") userLanguage: String?,
@Field("loginreturnurl") url: String? @Field("loginreturnurl") url: String?,
): Call<LoginResponse?> ): Call<LoginResponse?>
@Headers("Cache-Control: no-cache") @Headers("Cache-Control: no-cache")
@ -37,9 +37,11 @@ interface LoginInterface {
@Field("OATHToken") twoFactorCode: String?, @Field("OATHToken") twoFactorCode: String?,
@Field("logintoken") token: String?, @Field("logintoken") token: String?,
@Field("uselang") userLanguage: String?, @Field("uselang") userLanguage: String?,
@Field("logincontinue") loginContinue: Boolean @Field("logincontinue") loginContinue: Boolean,
): Call<LoginResponse?> ): Call<LoginResponse?>
@GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate") @GET(MW_API_PREFIX + "action=query&meta=userinfo&list=users&usprop=groups|cancreate")
fun getUserInfo(@Query("ususers") userName: String): Observable<MwQueryResponse?> fun getUserInfo(
@Query("ususers") userName: String,
): Observable<MwQueryResponse?>
} }

View file

@ -13,9 +13,7 @@ class LoginResponse {
@SerializedName("clientlogin") @SerializedName("clientlogin")
private val clientLogin: ClientLogin? = null private val clientLogin: ClientLogin? = null
fun toLoginResult(password: String): LoginResult? { fun toLoginResult(password: String): LoginResult? = clientLogin?.toLoginResult(password)
return clientLogin?.toLoginResult(password)
}
} }
internal class ClientLogin { internal class ClientLogin {
@ -39,7 +37,7 @@ internal class ClientLogin {
} }
} }
} else if ("PASS" != status && "FAIL" != status) { } else if ("PASS" != status && "FAIL" != status) {
//TODO: String resource -- Looks like needed for others in this class too // TODO: String resource -- Looks like needed for others in this class too
userMessage = "An unknown error occurred." userMessage = "An unknown error occurred."
} }
return Result(status ?: "", userName, password, userMessage) return Result(status ?: "", userName, password, userMessage)

View file

@ -4,7 +4,7 @@ sealed class LoginResult(
val status: String, val status: String,
val userName: String?, val userName: String?,
val password: String?, val password: String?,
val message: String? val message: String?,
) { ) {
var userId = 0 var userId = 0
var groups = emptySet<String>() var groups = emptySet<String>()
@ -14,20 +14,20 @@ sealed class LoginResult(
status: String, status: String,
userName: String?, userName: String?,
password: String?, password: String?,
message: String? message: String?,
): LoginResult(status, userName, password, message) ) : LoginResult(status, userName, password, message)
class OAuthResult( class OAuthResult(
status: String, status: String,
userName: String?, userName: String?,
password: String?, password: String?,
message: String? message: String?,
) : LoginResult(status, userName, password, message) ) : LoginResult(status, userName, password, message)
class ResetPasswordResult( class ResetPasswordResult(
status: String, status: String,
userName: String?, userName: String?,
password: String?, password: String?,
message: String? message: String?,
) : LoginResult(status, userName, password, message) ) : LoginResult(status, userName, password, message)
} }

View file

@ -15,25 +15,34 @@ import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
/** /**
* Helps to inflate Wikidata Items into Items tab * Helps to inflate Wikidata Items into Items tab
*/ */
class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context) : class BookmarkItemsAdapter(
RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() { val list: List<DepictedItem>,
val context: Context,
class BookmarkItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { ) : RecyclerView.Adapter<BookmarkItemsAdapter.BookmarkItemViewHolder>() {
class BookmarkItemViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label) var depictsLabel: TextView = itemView.findViewById(R.id.depicts_label)
var description: TextView = itemView.findViewById(R.id.description) var description: TextView = itemView.findViewById(R.id.description)
var depictsImage: SimpleDraweeView = itemView.findViewById(R.id.depicts_image) var depictsImage: SimpleDraweeView = itemView.findViewById(R.id.depicts_image)
var layout : ConstraintLayout = itemView.findViewById(R.id.layout_item) var layout: ConstraintLayout = itemView.findViewById(R.id.layout_item)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BookmarkItemViewHolder { override fun onCreateViewHolder(
val v: View = LayoutInflater.from(context) parent: ViewGroup,
viewType: Int,
): BookmarkItemViewHolder {
val v: View =
LayoutInflater
.from(context)
.inflate(R.layout.item_depictions, parent, false) .inflate(R.layout.item_depictions, parent, false)
return BookmarkItemViewHolder(v) return BookmarkItemViewHolder(v)
} }
override fun onBindViewHolder(holder: BookmarkItemViewHolder, position: Int) { override fun onBindViewHolder(
holder: BookmarkItemViewHolder,
position: Int,
) {
val depictedItem = list[position] val depictedItem = list[position]
holder.depictsLabel.text = depictedItem.name holder.depictsLabel.text = depictedItem.name
holder.description.text = depictedItem.description holder.description.text = depictedItem.description
@ -48,7 +57,5 @@ class BookmarkItemsAdapter (val list: List<DepictedItem>, val context: Context)
} }
} }
override fun getItemCount(): Int { override fun getItemCount(): Int = list.size
return list.size
}
} }

View file

@ -2,12 +2,15 @@ package fr.free.nrw.commons.bookmarks.models
import android.net.Uri import android.net.Uri
class Bookmark(mediaName: String?, mediaCreator: String?, class Bookmark(
mediaName: String?,
mediaCreator: String?,
/** /**
* Modifies the content URI - marking this bookmark as already saved in the database * Modifies the content URI - marking this bookmark as already saved in the database
* @param contentUri the content URI * @param contentUri the content URI
*/ */
var contentUri: Uri?) { var contentUri: Uri?,
) {
/** /**
* Gets the content URI for this bookmark * Gets the content URI for this bookmark
* @return content URI * @return content URI
@ -17,10 +20,10 @@ class Bookmark(mediaName: String?, mediaCreator: String?,
* @return the media name * @return the media name
*/ */
val mediaName: String = mediaName ?: "" val mediaName: String = mediaName ?: ""
/** /**
* Gets media creator * Gets media creator
* @return creator name * @return creator name
*/ */
val mediaCreator: String = mediaCreator ?: "" val mediaCreator: String = mediaCreator ?: ""
} }

View file

@ -8,6 +8,7 @@ import com.google.gson.annotations.SerializedName
class CampaignConfig { class CampaignConfig {
@SerializedName("showOnlyLiveCampaigns") @SerializedName("showOnlyLiveCampaigns")
private val showOnlyLiveCampaigns = false private val showOnlyLiveCampaigns = false
@SerializedName("sortBy") @SerializedName("sortBy")
private val sortBy: String? = null private val sortBy: String? = null
} }

View file

@ -9,7 +9,7 @@ import fr.free.nrw.commons.campaigns.models.Campaign
class CampaignResponseDTO { class CampaignResponseDTO {
@SerializedName("config") @SerializedName("config")
val campaignConfig: CampaignConfig? = null val campaignConfig: CampaignConfig? = null
@SerializedName("campaigns") @SerializedName("campaigns")
val campaigns: List<Campaign>? = null val campaigns: List<Campaign>? = null
} }

View file

@ -3,9 +3,11 @@ package fr.free.nrw.commons.campaigns.models
/** /**
* A data class to hold a campaign * A data class to hold a campaign
*/ */
data class Campaign(var title: String? = null, data class Campaign(
var title: String? = null,
var description: String? = null, var description: String? = null,
var startDate: String? = null, var startDate: String? = null,
var endDate: String? = null, var endDate: String? = null,
var link: String? = null, var link: String? = null,
var isWLMCampaign: Boolean = false) var isWLMCampaign: Boolean = false,
)

View file

@ -14,11 +14,13 @@ import javax.inject.Inject
/** /**
* The model class for categories in upload * The model class for categories in upload
*/ */
class CategoriesModel @Inject constructor( class CategoriesModel
@Inject
constructor(
private val categoryClient: CategoryClient, private val categoryClient: CategoryClient,
private val categoryDao: CategoryDao, private val categoryDao: CategoryDao,
private val gpsCategoryModel: GpsCategoryModel private val gpsCategoryModel: GpsCategoryModel,
) { ) {
private val selectedCategories: MutableList<CategoryItem> = mutableListOf() private val selectedCategories: MutableList<CategoryItem> = mutableListOf()
/** /**
@ -33,7 +35,7 @@ class CategoriesModel @Inject constructor(
* @return * @return
*/ */
fun isSpammyCategory(item: String): Boolean { fun isSpammyCategory(item: String): Boolean {
//Check for current and previous year to exclude these categories from removal // Check for current and previous year to exclude these categories from removal
val now = Calendar.getInstance() val now = Calendar.getInstance()
val curYear = now[Calendar.YEAR] val curYear = now[Calendar.YEAR]
val curYearInString = curYear.toString() val curYearInString = curYear.toString()
@ -43,8 +45,9 @@ class CategoriesModel @Inject constructor(
val mentionsDecade = item.matches(".*0s.*".toRegex()) val mentionsDecade = item.matches(".*0s.*".toRegex())
val recentDecade = item.matches(".*20[0-2]0s.*".toRegex()) val recentDecade = item.matches(".*20[0-2]0s.*".toRegex())
val spammyCategory = item.matches("(.*)needing(.*)".toRegex()) val spammyCategory =
|| item.matches("(.*)taken on(.*)".toRegex()) item.matches("(.*)needing(.*)".toRegex()) ||
item.matches("(.*)taken on(.*)".toRegex())
// always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750) // always skip irrelevant categories such as Media_needing_categories_as_of_16_June_2017(Issue #750)
if (spammyCategory) { if (spammyCategory) {
@ -59,9 +62,9 @@ class CategoriesModel @Inject constructor(
// If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year // If it is not an year in decade form (e.g. 19xxs/20xxs), then check if item contains a 4-digit year
// anywhere within the string (.* is wildcard) (Issue #47) // anywhere within the string (.* is wildcard) (Issue #47)
// And that item does not equal the current year or previous year // And that item does not equal the current year or previous year
return item.matches(".*(19|20)\\d{2}.*".toRegex()) return item.matches(".*(19|20)\\d{2}.*".toRegex()) &&
&& !item.contains(curYearInString) !item.contains(curYearInString) &&
&& !item.contains(prevYearInString) !item.contains(prevYearInString)
} }
} }
@ -89,27 +92,27 @@ class CategoriesModel @Inject constructor(
fun searchAll( fun searchAll(
term: String, term: String,
imageTitleList: List<String>, imageTitleList: List<String>,
selectedDepictions: List<DepictedItem> selectedDepictions: List<DepictedItem>,
): Observable<List<CategoryItem>> { ): Observable<List<CategoryItem>> =
return suggestionsOrSearch(term, imageTitleList, selectedDepictions) suggestionsOrSearch(term, imageTitleList, selectedDepictions)
.map { it.map { CategoryItem(it.name, it.description, it.thumbnail, false) } } .map { it.map { CategoryItem(it.name, it.description, it.thumbnail, false) } }
}
private fun suggestionsOrSearch( private fun suggestionsOrSearch(
term: String, term: String,
imageTitleList: List<String>, imageTitleList: List<String>,
selectedDepictions: List<DepictedItem> selectedDepictions: List<DepictedItem>,
): Observable<List<CategoryItem>> { ): Observable<List<CategoryItem>> =
return if (TextUtils.isEmpty(term)) if (TextUtils.isEmpty(term)) {
Observable.combineLatest( Observable.combineLatest(
categoriesFromDepiction(selectedDepictions), categoriesFromDepiction(selectedDepictions),
gpsCategoryModel.categoriesFromLocation, gpsCategoryModel.categoriesFromLocation,
titleCategories(imageTitleList), titleCategories(imageTitleList),
Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)), Observable.just(categoryDao.recentCategories(SEARCH_CATS_LIMIT)),
Function4(::combine) Function4(::combine),
) )
else } else {
categoryClient.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT) categoryClient
.searchCategoriesForPrefix(term, SEARCH_CATS_LIMIT)
.map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) } .map { it.sortedWith(StringSortingUtils.sortBySimilarity(term)) }
.toObservable() .toObservable()
} }
@ -121,20 +124,26 @@ class CategoriesModel @Inject constructor(
* @param selectedDepictions selected DepictItems * @param selectedDepictions selected DepictItems
* @return List of CategoryItem associated with selected depictions * @return List of CategoryItem associated with selected depictions
*/ */
private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): private fun categoriesFromDepiction(selectedDepictions: List<DepictedItem>): Observable<MutableList<CategoryItem>>? =
Observable<MutableList<CategoryItem>>? { Observable
return Observable.fromIterable( .fromIterable(
selectedDepictions.map { it.commonsCategories }.flatten()) selectedDepictions.map { it.commonsCategories }.flatten(),
.map { categoryItem -> ).map { categoryItem ->
categoryClient.getCategoriesByName(categoryItem.name, categoryClient
categoryItem.name, SEARCH_CATS_LIMIT).map { .getCategoriesByName(
categoryItem.name,
CategoryItem(it[0].name, it[0].description, categoryItem.name,
it[0].thumbnail, it[0].isSelected) SEARCH_CATS_LIMIT,
).map {
CategoryItem(
it[0].name,
it[0].description,
it[0].thumbnail,
it[0].isSelected,
)
}.blockingGet() }.blockingGet()
}.toList().toObservable() }.toList()
} .toObservable()
/** /**
* Fetches details of every category by their name, converts them into * Fetches details of every category by their name, converts them into
@ -143,74 +152,80 @@ class CategoriesModel @Inject constructor(
* @param categoryNames selected Categories * @param categoryNames selected Categories
* @return List of CategoryItem * @return List of CategoryItem
*/ */
fun getCategoriesByName(categoryNames: List<String>): fun getCategoriesByName(categoryNames: List<String>): Observable<MutableList<CategoryItem>>? =
Observable<MutableList<CategoryItem>>? { Observable
return Observable.fromIterable(categoryNames) .fromIterable(categoryNames)
.map { categoryName -> .map { categoryName ->
buildCategories(categoryName) buildCategories(categoryName)
} }.filter { categoryItem ->
.filter { categoryItem ->
categoryItem.name != "Hidden" categoryItem.name != "Hidden"
} }.toList()
.toList().toObservable() .toObservable()
}
/** /**
* Fetches the categories and converts them into CategoryItem * Fetches the categories and converts them into CategoryItem
*/ */
fun buildCategories(categoryName: String): CategoryItem { fun buildCategories(categoryName: String): CategoryItem =
return categoryClient.getCategoriesByName(categoryName, categoryClient
categoryName, SEARCH_CATS_LIMIT).map { .getCategoriesByName(
if(it.isNotEmpty()) { categoryName,
categoryName,
SEARCH_CATS_LIMIT,
).map {
if (it.isNotEmpty()) {
CategoryItem( CategoryItem(
it[0].name, it[0].description, it[0].name,
it[0].thumbnail, it[0].isSelected it[0].description,
it[0].thumbnail,
it[0].isSelected,
) )
} else { } else {
CategoryItem( CategoryItem(
"Hidden", "Hidden", "Hidden",
"hidden", false "Hidden",
"hidden",
false,
) )
} }
}.blockingGet() }.blockingGet()
}
private fun combine( private fun combine(
depictionCategories: List<CategoryItem>, depictionCategories: List<CategoryItem>,
locationCategories: List<CategoryItem>, locationCategories: List<CategoryItem>,
titles: List<CategoryItem>, titles: List<CategoryItem>,
recents: List<CategoryItem> recents: List<CategoryItem>,
) = depictionCategories + locationCategories + titles + recents ) = depictionCategories + locationCategories + titles + recents
/** /**
* Returns title based categories * Returns title based categories
* @param titleList * @param titleList
* @return * @return
*/ */
private fun titleCategories(titleList: List<String>) = private fun titleCategories(titleList: List<String>) =
if (titleList.isNotEmpty()) if (titleList.isNotEmpty()) {
Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults -> Observable.combineLatest(titleList.map { getTitleCategories(it) }) { searchResults ->
searchResults.map { it as List<CategoryItem> }.flatten() searchResults.map { it as List<CategoryItem> }.flatten()
} }
else } else {
Observable.just(emptyList()) Observable.just(emptyList())
}
/** /**
* Return category for single title * Return category for single title
* @param title * @param title
* @return * @return
*/ */
private fun getTitleCategories(title: String): Observable<List<CategoryItem>> { private fun getTitleCategories(title: String): Observable<List<CategoryItem>> =
return categoryClient.searchCategories(title, SEARCH_CATS_LIMIT).toObservable() categoryClient.searchCategories(title, SEARCH_CATS_LIMIT).toObservable()
}
/** /**
* Handles category item selection * Handles category item selection
* @param item * @param item
*/ */
fun onCategoryItemClicked(item: CategoryItem, media: Media?) { fun onCategoryItemClicked(
item: CategoryItem,
media: Media?,
) {
if (media == null) { if (media == null) {
if (item.isSelected) { if (item.isSelected) {
selectedCategories.add(item) selectedCategories.add(item)
@ -246,9 +261,7 @@ class CategoriesModel @Inject constructor(
* Get Selected Categories * Get Selected Categories
* @return * @return
*/ */
fun getSelectedCategories(): List<CategoryItem> { fun getSelectedCategories(): List<CategoryItem> = selectedCategories
return selectedCategories
}
/** /**
* Cleanup the existing in memory cache's * Cleanup the existing in memory cache's
@ -267,9 +280,7 @@ class CategoriesModel @Inject constructor(
* *
* @return selected existing categories * @return selected existing categories
*/ */
fun getSelectedExistingCategories(): List<String> { fun getSelectedExistingCategories(): List<String> = selectedExistingCategories
return selectedExistingCategories
}
/** /**
* Initialize existing categories * Initialize existing categories
@ -279,4 +290,4 @@ class CategoriesModel @Inject constructor(
fun setSelectedExistingCategories(selectedExistingCategories: MutableList<String>) { fun setSelectedExistingCategories(selectedExistingCategories: MutableList<String>) {
this.selectedExistingCategories = selectedExistingCategories this.selectedExistingCategories = selectedExistingCategories
} }
} }

View file

@ -1,7 +1,7 @@
package fr.free.nrw.commons.category package fr.free.nrw.commons.category
import io.reactivex.Single
import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse import fr.free.nrw.commons.wikidata.mwapi.MwQueryResponse
import io.reactivex.Single
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@ -15,9 +15,11 @@ const val CATEGORY_NEEDING_CATEGORIES = "needing categories"
* Category Client to handle custom calls to Commons MediaWiki APIs * Category Client to handle custom calls to Commons MediaWiki APIs
*/ */
@Singleton @Singleton
class CategoryClient @Inject constructor(private val categoryInterface: CategoryInterface) : class CategoryClient
ContinuationClient<MwQueryResponse, CategoryItem>() { @Inject
constructor(
private val categoryInterface: CategoryInterface,
) : ContinuationClient<MwQueryResponse, CategoryItem>() {
/** /**
* Searches for categories containing the specified string. * Searches for categories containing the specified string.
* *
@ -27,10 +29,11 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @return * @return
*/ */
@JvmOverloads @JvmOverloads
fun searchCategories(filter: String?, itemLimit: Int, offset: Int = 0): fun searchCategories(
Single<List<CategoryItem>> { filter: String?,
return responseMapper(categoryInterface.searchCategories(filter, itemLimit, offset)) itemLimit: Int,
} offset: Int = 0,
): Single<List<CategoryItem>> = responseMapper(categoryInterface.searchCategories(filter, itemLimit, offset))
/** /**
* Searches for categories starting with the specified string. * Searches for categories starting with the specified string.
@ -41,12 +44,14 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @return * @return
*/ */
@JvmOverloads @JvmOverloads
fun searchCategoriesForPrefix(prefix: String?, itemLimit: Int, offset: Int = 0): fun searchCategoriesForPrefix(
Single<List<CategoryItem>> { prefix: String?,
return responseMapper( itemLimit: Int,
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset) offset: Int = 0,
): Single<List<CategoryItem>> =
responseMapper(
categoryInterface.searchCategoriesForPrefix(prefix, itemLimit, offset),
) )
}
/** /**
* Fetches categories starting and ending with a specified name. * Fetches categories starting and ending with a specified name.
@ -58,13 +63,20 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @return MwQueryResponse * @return MwQueryResponse
*/ */
@JvmOverloads @JvmOverloads
fun getCategoriesByName(startingCategoryName: String?, endingCategoryName: String?, fun getCategoriesByName(
itemLimit: Int, offset: Int = 0): Single<List<CategoryItem>> { startingCategoryName: String?,
return responseMapper( endingCategoryName: String?,
categoryInterface.getCategoriesByName(startingCategoryName, endingCategoryName, itemLimit: Int,
itemLimit, offset) offset: Int = 0,
): Single<List<CategoryItem>> =
responseMapper(
categoryInterface.getCategoriesByName(
startingCategoryName,
endingCategoryName,
itemLimit,
offset,
),
) )
}
/** /**
* The method takes categoryName as input and returns a List of Subcategories * The method takes categoryName as input and returns a List of Subcategories
@ -73,13 +85,13 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @param categoryName Category name as defined on commons * @param categoryName Category name as defined on commons
* @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted. * @return Observable emitting the categories returned. If our search yielded "Category:Test", "Test" is emitted.
*/ */
fun getSubCategoryList(categoryName: String): Single<List<CategoryItem>> { fun getSubCategoryList(categoryName: String): Single<List<CategoryItem>> =
return continuationRequest(SUB_CATEGORY_CONTINUATION_PREFIX, categoryName) { continuationRequest(SUB_CATEGORY_CONTINUATION_PREFIX, categoryName) {
categoryInterface.getSubCategoryList( categoryInterface.getSubCategoryList(
categoryName, it categoryName,
it,
) )
} }
}
/** /**
* The method takes categoryName as input and returns a List of parent categories * The method takes categoryName as input and returns a List of parent categories
@ -88,11 +100,10 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
* @param categoryName Category name as defined on commons * @param categoryName Category name as defined on commons
* @return * @return
*/ */
fun getParentCategoryList(categoryName: String): Single<List<CategoryItem>> { fun getParentCategoryList(categoryName: String): Single<List<CategoryItem>> =
return continuationRequest(PARENT_CATEGORY_CONTINUATION_PREFIX, categoryName) { continuationRequest(PARENT_CATEGORY_CONTINUATION_PREFIX, categoryName) {
categoryInterface.getParentCategoryList(categoryName, it) categoryInterface.getParentCategoryList(categoryName, it)
} }
}
fun resetSubCategoryContinuation(category: String) { fun resetSubCategoryContinuation(category: String) {
resetContinuation(SUB_CATEGORY_CONTINUATION_PREFIX, category) resetContinuation(SUB_CATEGORY_CONTINUATION_PREFIX, category)
@ -104,20 +115,23 @@ class CategoryClient @Inject constructor(private val categoryInterface: Category
override fun responseMapper( override fun responseMapper(
networkResult: Single<MwQueryResponse>, networkResult: Single<MwQueryResponse>,
key: String? key: String?,
): Single<List<CategoryItem>> { ): Single<List<CategoryItem>> =
return networkResult networkResult
.map { .map {
handleContinuationResponse(it.continuation(), key) handleContinuationResponse(it.continuation(), key)
it.query()?.pages() ?: emptyList() it.query()?.pages() ?: emptyList()
}
.map {
it.filter {
page -> page.categoryInfo() == null || !page.categoryInfo().isHidden
}.map { }.map {
CategoryItem(it.title().replace(CATEGORY_PREFIX, ""), it
it.description().toString(), it.thumbUrl().toString(), false) .filter { page ->
page.categoryInfo() == null || !page.categoryInfo().isHidden
}.map {
CategoryItem(
it.title().replace(CATEGORY_PREFIX, ""),
it.description().toString(),
it.thumbUrl().toString(),
false,
)
} }
} }
} }
}

View file

@ -17,11 +17,13 @@ interface CategoryInterface {
* @param itemLimit How many results are returned * @param itemLimit How many results are returned
* @return * @return
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70&gsrnamespace=14") @GET(
"w/api.php?action=query&format=json&formatversion=2&generator=search&prop=description|pageimages&piprop=thumbnail&pithumbsize=70&gsrnamespace=14",
)
fun searchCategories( fun searchCategories(
@Query("gsrsearch") filter: String?, @Query("gsrsearch") filter: String?,
@Query("gsrlimit") itemLimit: Int, @Query("gsrlimit") itemLimit: Int,
@Query("gsroffset") offset: Int @Query("gsroffset") offset: Int,
): Single<MwQueryResponse> ): Single<MwQueryResponse>
/** /**
@ -31,11 +33,13 @@ interface CategoryInterface {
* @param itemLimit How many results are returned * @param itemLimit How many results are returned
* @return * @return
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70") @GET(
"w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70",
)
fun searchCategoriesForPrefix( fun searchCategoriesForPrefix(
@Query("gacprefix") prefix: String?, @Query("gacprefix") prefix: String?,
@Query("gaclimit") itemLimit: Int, @Query("gaclimit") itemLimit: Int,
@Query("gacoffset") offset: Int @Query("gacoffset") offset: Int,
): Single<MwQueryResponse> ): Single<MwQueryResponse>
/** /**
@ -47,23 +51,25 @@ interface CategoryInterface {
* @param offset offset * @param offset offset
* @return MwQueryResponse * @return MwQueryResponse
*/ */
@GET("w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70") @GET(
"w/api.php?action=query&format=json&formatversion=2&generator=allcategories&prop=categoryinfo|description|pageimages&piprop=thumbnail&pithumbsize=70",
)
fun getCategoriesByName( fun getCategoriesByName(
@Query("gacfrom") startingCategory: String?, @Query("gacfrom") startingCategory: String?,
@Query("gacto") endingCategory: String?, @Query("gacto") endingCategory: String?,
@Query("gaclimit") itemLimit: Int, @Query("gaclimit") itemLimit: Int,
@Query("gacoffset") offset: Int @Query("gacoffset") offset: Int,
): Single<MwQueryResponse> ): Single<MwQueryResponse>
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50") @GET("w/api.php?action=query&format=json&formatversion=2&generator=categorymembers&gcmtype=subcat&prop=info&gcmlimit=50")
fun getSubCategoryList( fun getSubCategoryList(
@Query("gcmtitle") categoryName: String, @Query("gcmtitle") categoryName: String,
@QueryMap(encoded = true) continuation: Map<String, String> @QueryMap(encoded = true) continuation: Map<String, String>,
): Single<MwQueryResponse> ): Single<MwQueryResponse>
@GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50") @GET("w/api.php?action=query&format=json&formatversion=2&generator=categories&prop=info&gcllimit=50")
fun getParentCategoryList( fun getParentCategoryList(
@Query("titles") categoryName: String?, @Query("titles") categoryName: String?,
@QueryMap(encoded = true) continuation: Map<String, String> @QueryMap(encoded = true) continuation: Map<String, String>,
): Single<MwQueryResponse> ): Single<MwQueryResponse>
} }

View file

@ -4,12 +4,13 @@ import android.os.Parcelable
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
data class CategoryItem(val name: String, val description: String?, data class CategoryItem(
val thumbnail: String?, var isSelected: Boolean) : Parcelable { val name: String,
val description: String?,
override fun toString(): String { val thumbnail: String?,
return "CategoryItem: '$name'" var isSelected: Boolean,
} ) : Parcelable {
override fun toString(): String = "CategoryItem: '$name'"
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
@ -22,7 +23,5 @@ data class CategoryItem(val name: String, val description: String?,
return true return true
} }
override fun hashCode(): Int { override fun hashCode(): Int = name.hashCode()
return name.hashCode()
}
} }

View file

@ -2,16 +2,16 @@ package fr.free.nrw.commons.category
import io.reactivex.Single import io.reactivex.Single
abstract class ContinuationClient<Network, Domain> { abstract class ContinuationClient<Network, Domain> {
private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf() private val continuationStore: MutableMap<String, Map<String, String>?> = mutableMapOf()
private val continuationExists: MutableMap<String, Boolean> = mutableMapOf() private val continuationExists: MutableMap<String, Boolean> = mutableMapOf()
private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true private fun hasMorePagesFor(key: String) = continuationExists[key] ?: true
fun continuationRequest( fun continuationRequest(
prefix: String, prefix: String,
name: String, name: String,
requestFunction: (Map<String, String>) -> Single<Network> requestFunction: (Map<String, String>) -> Single<Network>,
): Single<List<Domain>> { ): Single<List<Domain>> {
val key = "$prefix$name" val key = "$prefix$name"
return if (hasMorePagesFor(key)) { return if (hasMorePagesFor(key)) {
@ -21,9 +21,15 @@ abstract class ContinuationClient<Network, Domain> {
} }
} }
abstract fun responseMapper(networkResult: Single<Network>, key: String?=null): Single<List<Domain>> abstract fun responseMapper(
networkResult: Single<Network>,
key: String? = null,
): Single<List<Domain>>
fun handleContinuationResponse(continuation:Map<String,String>?, key:String?){ fun handleContinuationResponse(
continuation: Map<String, String>?,
key: String?,
) {
if (key != null) { if (key != null) {
continuationExists[key] = continuationExists[key] =
continuation?.let { continuation -> continuation?.let { continuation ->
@ -33,7 +39,10 @@ abstract class ContinuationClient<Network, Domain> {
} }
} }
protected fun resetContinuation(prefix: String, category: String) { protected fun resetContinuation(
prefix: String,
category: String,
) {
continuationExists.remove("$prefix$category") continuationExists.remove("$prefix$category")
continuationStore.remove("$prefix$category") continuationStore.remove("$prefix$category")
} }
@ -44,9 +53,11 @@ abstract class ContinuationClient<Network, Domain> {
* @param prefix * @param prefix
* @param userName the username * @param userName the username
*/ */
protected fun resetUserContinuation(prefix: String, userName: String) { protected fun resetUserContinuation(
prefix: String,
userName: String,
) {
continuationExists.remove("$prefix$userName") continuationExists.remove("$prefix$userName")
continuationStore.remove("$prefix$userName") continuationStore.remove("$prefix$userName")
} }
} }

View file

@ -7,32 +7,29 @@ import fr.free.nrw.commons.upload.UploadResult
data class ChunkInfo( data class ChunkInfo(
val uploadResult: UploadResult?, val uploadResult: UploadResult?,
val indexOfNextChunkToUpload: Int, val indexOfNextChunkToUpload: Int,
val totalChunks: Int val totalChunks: Int,
) : Parcelable { ) : Parcelable {
constructor(parcel: Parcel) : this( constructor(parcel: Parcel) : this(
parcel.readParcelable(UploadResult::class.java.classLoader), parcel.readParcelable(UploadResult::class.java.classLoader),
parcel.readInt(), parcel.readInt(),
parcel.readInt() parcel.readInt(),
) { ) {
} }
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(
parcel: Parcel,
flags: Int,
) {
parcel.writeParcelable(uploadResult, flags) parcel.writeParcelable(uploadResult, flags)
parcel.writeInt(indexOfNextChunkToUpload) parcel.writeInt(indexOfNextChunkToUpload)
parcel.writeInt(totalChunks) parcel.writeInt(totalChunks)
} }
override fun describeContents(): Int { override fun describeContents(): Int = 0
return 0
}
companion object CREATOR : Parcelable.Creator<ChunkInfo> { companion object CREATOR : Parcelable.Creator<ChunkInfo> {
override fun createFromParcel(parcel: Parcel): ChunkInfo { override fun createFromParcel(parcel: Parcel): ChunkInfo = ChunkInfo(parcel)
return ChunkInfo(parcel)
}
override fun newArray(size: Int): Array<ChunkInfo?> { override fun newArray(size: Int): Array<ChunkInfo?> = arrayOfNulls(size)
return arrayOfNulls(size)
}
} }
} }

View file

@ -5,7 +5,6 @@ import android.os.Parcelable
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import fr.free.nrw.commons.CommonsApplication
import fr.free.nrw.commons.Media import fr.free.nrw.commons.Media
import fr.free.nrw.commons.auth.SessionManager import fr.free.nrw.commons.auth.SessionManager
import fr.free.nrw.commons.upload.UploadItem import fr.free.nrw.commons.upload.UploadItem
@ -44,26 +43,23 @@ data class Contribution constructor(
var dateCreatedString: String? = null, var dateCreatedString: String? = null,
var dateModified: Date? = null, var dateModified: Date? = null,
var dateUploadStarted: Date? = null, var dateUploadStarted: Date? = null,
var hasInvalidLocation : Int = 0, var hasInvalidLocation: Int = 0,
var contentUri: Uri? = null, var contentUri: Uri? = null,
var countryCode : String? = null, var countryCode: String? = null,
var imageSHA1 : String? = null, var imageSHA1: String? = null,
/** /**
* Number of times a contribution has been retried after a failure * Number of times a contribution has been retried after a failure
*/ */
var retries: Int = 0 var retries: Int = 0,
) : Parcelable { ) : Parcelable {
fun completeWith(media: Media): Contribution = copy(pageId = media.pageId, media = media, state = STATE_COMPLETED)
fun completeWith(media: Media): Contribution {
return copy(pageId = media.pageId, media = media, state = STATE_COMPLETED)
}
constructor( constructor(
item: UploadItem, item: UploadItem,
sessionManager: SessionManager, sessionManager: SessionManager,
depictedItems: List<DepictedItem>, depictedItems: List<DepictedItem>,
categories: List<String>, categories: List<String>,
imageSHA1: String imageSHA1: String,
) : this( ) : this(
Media( Media(
formatCaptions(item.uploadMediaDetails), formatCaptions(item.uploadMediaDetails),
@ -71,7 +67,7 @@ data class Contribution constructor(
item.fileName, item.fileName,
formatDescriptions(item.uploadMediaDetails), formatDescriptions(item.uploadMediaDetails),
sessionManager.userName, sessionManager.userName,
sessionManager.userName sessionManager.userName,
), ),
localUri = item.mediaUri, localUri = item.mediaUri,
decimalCoords = item.gpsCoords.decimalCoords, decimalCoords = item.gpsCoords.decimalCoords,
@ -80,7 +76,7 @@ data class Contribution constructor(
wikidataPlace = from(item.place), wikidataPlace = from(item.place),
contentUri = item.contentUri, contentUri = item.contentUri,
dateCreatedString = item.fileCreatedDateString, dateCreatedString = item.fileCreatedDateString,
imageSHA1 = imageSHA1 imageSHA1 = imageSHA1,
) )
/** /**
@ -91,9 +87,7 @@ data class Contribution constructor(
this.hasInvalidLocation = if (hasInvalidLocation) 1 else 0 this.hasInvalidLocation = if (hasInvalidLocation) 1 else 0
} }
fun hasInvalidLocation(): Boolean { fun hasInvalidLocation(): Boolean = hasInvalidLocation == 1
return hasInvalidLocation == 1
}
companion object { companion object {
const val STATE_COMPLETED = -1 const val STATE_COMPLETED = -1
@ -107,7 +101,8 @@ data class Contribution constructor(
* @param uploadMediaDetails list of media Details * @param uploadMediaDetails list of media Details
*/ */
fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) = fun formatCaptions(uploadMediaDetails: List<UploadMediaDetail>) =
uploadMediaDetails.associate { it.languageCode!! to it.captionText } uploadMediaDetails
.associate { it.languageCode!! to it.captionText }
.filter { it.value.isNotBlank() } .filter { it.value.isNotBlank() }
/** /**
@ -117,19 +112,15 @@ data class Contribution constructor(
* @return a string with the pattern of {{en|1=descriptionText}} * @return a string with the pattern of {{en|1=descriptionText}}
*/ */
fun formatDescriptions(descriptions: List<UploadMediaDetail>) = fun formatDescriptions(descriptions: List<UploadMediaDetail>) =
descriptions.filter { it.descriptionText.isNotEmpty() } descriptions
.filter { it.descriptionText.isNotEmpty() }
.joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" } .joinToString(separator = "") { "{{${it.languageCode}|1=${it.descriptionText}}}" }
} }
val fileKey : String? get() = chunkInfo?.uploadResult?.filekey val fileKey: String? get() = chunkInfo?.uploadResult?.filekey
val localUriPath: File? get() = localUri?.path?.let { File(it) } val localUriPath: File? get() = localUri?.path?.let { File(it) }
fun isCompleted(): Boolean { fun isCompleted(): Boolean = chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
return chunkInfo != null && chunkInfo!!.totalChunks == chunkInfo!!.indexOfNextChunkToUpload
}
fun dateUploadStartedInMillis(): Long {
return dateUploadStarted!!.time
}
fun dateUploadStartedInMillis(): Long = dateUploadStarted!!.time
} }

View file

@ -14,16 +14,17 @@ import javax.inject.Named
* Class that extends PagedList.BoundaryCallback for contributions list It defines the action that * Class that extends PagedList.BoundaryCallback for contributions list It defines the action that
* is triggered for various boundary conditions in the list * is triggered for various boundary conditions in the list
*/ */
class ContributionBoundaryCallback @Inject constructor( class ContributionBoundaryCallback
@Inject
constructor(
private val repository: ContributionsRepository, private val repository: ContributionsRepository,
private val sessionManager: SessionManager, private val sessionManager: SessionManager,
private val mediaClient: MediaClient, private val mediaClient: MediaClient,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
) : BoundaryCallback<Contribution>() { ) : BoundaryCallback<Contribution>() {
private val compositeDisposable: CompositeDisposable = CompositeDisposable() private val compositeDisposable: CompositeDisposable = CompositeDisposable()
var userName: String? = null var userName: String? = null
/** /**
* It is triggered when the list has no items User's Contributions are then fetched from the * It is triggered when the list has no items User's Contributions are then fetched from the
* network * network
@ -39,7 +40,6 @@ class ContributionBoundaryCallback @Inject constructor(
* It is triggered when the user scrolls to the top of the list * It is triggered when the user scrolls to the top of the list
* */ * */
override fun onItemAtFrontLoaded(itemAtFront: Contribution) { override fun onItemAtFrontLoaded(itemAtFront: Contribution) {
} }
/** /**
@ -55,26 +55,27 @@ class ContributionBoundaryCallback @Inject constructor(
*/ */
private fun fetchContributions() { private fun fetchContributions() {
if (sessionManager.userName != null) { if (sessionManager.userName != null) {
userName?.let { userName -> userName
mediaClient.getMediaListForUser(userName) ?.let { userName ->
mediaClient
.getMediaListForUser(userName)
.map { mediaList -> .map { mediaList ->
mediaList.map { media -> mediaList.map { media ->
Contribution(media = media, state = Contribution.STATE_COMPLETED) Contribution(media = media, state = Contribution.STATE_COMPLETED)
} }
} }.subscribeOn(ioThreadScheduler)
.subscribeOn(ioThreadScheduler)
.subscribe(::saveContributionsToDB) { error: Throwable -> .subscribe(::saveContributionsToDB) { error: Throwable ->
Timber.e( Timber.e(
"Failed to fetch contributions: %s", "Failed to fetch contributions: %s",
error.message error.message,
) )
} }
}?.let { }?.let {
compositeDisposable.add( compositeDisposable.add(
it it,
) )
} }
}else { } else {
compositeDisposable.clear() compositeDisposable.clear()
} }
} }
@ -84,11 +85,12 @@ class ContributionBoundaryCallback @Inject constructor(
*/ */
private fun saveContributionsToDB(contributions: List<Contribution>) { private fun saveContributionsToDB(contributions: List<Contribution>) {
compositeDisposable.add( compositeDisposable.add(
repository.save(contributions) repository
.save(contributions)
.subscribeOn(ioThreadScheduler) .subscribeOn(ioThreadScheduler)
.subscribe { longs: List<Long?>? -> .subscribe { longs: List<Long?>? ->
repository["last_fetch_timestamp"] = System.currentTimeMillis() repository["last_fetch_timestamp"] = System.currentTimeMillis()
} },
) )
} }
@ -98,4 +100,4 @@ class ContributionBoundaryCallback @Inject constructor(
fun dispose() { fun dispose() {
compositeDisposable.dispose() compositeDisposable.dispose()
} }
} }

View file

@ -12,62 +12,61 @@ import javax.inject.Named
/** /**
* Data-Source which acts as mediator for contributions-data from the API * Data-Source which acts as mediator for contributions-data from the API
*/ */
class ContributionsRemoteDataSource @Inject constructor( class ContributionsRemoteDataSource
@Inject
constructor(
private val mediaClient: MediaClient, private val mediaClient: MediaClient,
@param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler @param:Named(CommonsApplicationModule.IO_THREAD) private val ioThreadScheduler: Scheduler,
) : ItemKeyedDataSource<Int, Contribution>() { ) : ItemKeyedDataSource<Int, Contribution>() {
private val compositeDisposable: CompositeDisposable = CompositeDisposable() private val compositeDisposable: CompositeDisposable = CompositeDisposable()
var userName: String? = null var userName: String? = null
override fun loadInitial( override fun loadInitial(
params: LoadInitialParams<Int>, params: LoadInitialParams<Int>,
callback: LoadInitialCallback<Contribution> callback: LoadInitialCallback<Contribution>,
) { ) {
fetchContributions(callback) fetchContributions(callback)
} }
override fun loadAfter( override fun loadAfter(
params: LoadParams<Int>, params: LoadParams<Int>,
callback: LoadCallback<Contribution> callback: LoadCallback<Contribution>,
) { ) {
fetchContributions(callback) fetchContributions(callback)
} }
override fun loadBefore( override fun loadBefore(
params: LoadParams<Int>, params: LoadParams<Int>,
callback: LoadCallback<Contribution> callback: LoadCallback<Contribution>,
) { ) {
} }
override fun getKey(item: Contribution): Int { override fun getKey(item: Contribution): Int = item.pageId.hashCode()
return item.pageId.hashCode()
}
/** /**
* Fetches contributions using the MediaWiki API * Fetches contributions using the MediaWiki API
*/ */
private fun fetchContributions(callback: LoadCallback<Contribution>) { private fun fetchContributions(callback: LoadCallback<Contribution>) {
compositeDisposable.add( compositeDisposable.add(
mediaClient.getMediaListForUser(userName!!) mediaClient
.getMediaListForUser(userName!!)
.map { mediaList -> .map { mediaList ->
mediaList.map { mediaList.map {
Contribution(media = it, state = Contribution.STATE_COMPLETED) Contribution(media = it, state = Contribution.STATE_COMPLETED)
} }
} }.subscribeOn(ioThreadScheduler)
.subscribeOn(ioThreadScheduler)
.subscribe({ .subscribe({
callback.onResult(it) callback.onResult(it)
}) { error: Throwable -> }) { error: Throwable ->
Timber.e( Timber.e(
"Failed to fetch contributions: %s", "Failed to fetch contributions: %s",
error.message error.message,
) )
} },
) )
} }
fun dispose() { fun dispose() {
compositeDisposable.dispose() compositeDisposable.dispose()
} }
} }

View file

@ -13,14 +13,15 @@ import fr.free.nrw.commons.databinding.DialogAddToWikipediaInstructionsBinding
* Dialog fragment for displaying instructions for editing wikipedia * Dialog fragment for displaying instructions for editing wikipedia
*/ */
class WikipediaInstructionsDialogFragment : DialogFragment() { class WikipediaInstructionsDialogFragment : DialogFragment() {
var callback: Callback? = null var callback: Callback? = null
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
) = DialogAddToWikipediaInstructionsBinding.inflate(inflater, container, false).apply { ) = DialogAddToWikipediaInstructionsBinding
.inflate(inflater, container, false)
.apply {
val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION) val contribution: Contribution? = arguments!!.getParcelable(ARG_CONTRIBUTION)
tvWikicode.setText(contribution?.media?.wikiCode) tvWikicode.setText(contribution?.media?.wikiCode)
instructionsCancel.setOnClickListener { dismiss() } instructionsCancel.setOnClickListener { dismiss() }
@ -29,10 +30,13 @@ class WikipediaInstructionsDialogFragment : DialogFragment() {
} }
}.root }.root
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
dialog!!.window?.setSoftInputMode( dialog!!.window?.setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN,
) )
} }
@ -40,14 +44,18 @@ class WikipediaInstructionsDialogFragment : DialogFragment() {
* Callback for handling confirm button clicked * Callback for handling confirm button clicked
*/ */
interface Callback { interface Callback {
fun onConfirmClicked(contribution: Contribution?, copyWikicode: Boolean) fun onConfirmClicked(
contribution: Contribution?,
copyWikicode: Boolean,
)
} }
companion object { companion object {
const val ARG_CONTRIBUTION = "contribution" const val ARG_CONTRIBUTION = "contribution"
@JvmStatic @JvmStatic
fun newInstance(contribution: Contribution) = WikipediaInstructionsDialogFragment().apply { fun newInstance(contribution: Contribution) =
WikipediaInstructionsDialogFragment().apply {
arguments = bundleOf(ARG_CONTRIBUTION to contribution) arguments = bundleOf(ARG_CONTRIBUTION to contribution)
} }
} }

View file

@ -2,17 +2,15 @@ package fr.free.nrw.commons.customselector.database
import androidx.room.* import androidx.room.*
/** /**
* Dao class for Not For Upload * Dao class for Not For Upload
*/ */
@Dao @Dao
abstract class NotForUploadStatusDao { abstract class NotForUploadStatusDao {
/** /**
* Insert into Not For Upload status. * Insert into Not For Upload status.
*/ */
@Insert( onConflict = OnConflictStrategy.REPLACE ) @Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(notForUploadStatus: NotForUploadStatus) abstract suspend fun insert(notForUploadStatus: NotForUploadStatus)
/** /**
@ -25,33 +23,27 @@ abstract class NotForUploadStatusDao {
* Query Not For Upload status with image sha1. * Query Not For Upload status with image sha1.
*/ */
@Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") @Query("SELECT * FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
abstract suspend fun getFromImageSHA1(imageSHA1 : String) : NotForUploadStatus? abstract suspend fun getFromImageSHA1(imageSHA1: String): NotForUploadStatus?
/** /**
* Asynchronous image sha1 query. * Asynchronous image sha1 query.
*/ */
suspend fun getNotForUploadFromImageSHA1(imageSHA1: String):NotForUploadStatus? { suspend fun getNotForUploadFromImageSHA1(imageSHA1: String): NotForUploadStatus? = getFromImageSHA1(imageSHA1)
return getFromImageSHA1(imageSHA1)
}
/** /**
* Deletion Not For Upload status with image sha1. * Deletion Not For Upload status with image sha1.
*/ */
@Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") @Query("DELETE FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
abstract suspend fun deleteWithImageSHA1(imageSHA1 : String) abstract suspend fun deleteWithImageSHA1(imageSHA1: String)
/** /**
* Asynchronous image sha1 deletion. * Asynchronous image sha1 deletion.
*/ */
suspend fun deleteNotForUploadWithImageSHA1(imageSHA1: String) { suspend fun deleteNotForUploadWithImageSHA1(imageSHA1: String) = deleteWithImageSHA1(imageSHA1)
return deleteWithImageSHA1(imageSHA1)
}
/** /**
* Check whether the imageSHA1 is present in database * Check whether the imageSHA1 is present in database
*/ */
@Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ") @Query("SELECT COUNT() FROM images_not_for_upload_table WHERE imageSHA1 = (:imageSHA1) ")
abstract suspend fun find(imageSHA1 : String): Int abstract suspend fun find(imageSHA1: String): Int
} }

View file

@ -7,10 +7,9 @@ import androidx.room.*
*/ */
@Entity(tableName = "images_not_for_upload_table") @Entity(tableName = "images_not_for_upload_table")
data class NotForUploadStatus( data class NotForUploadStatus(
/** /**
* Original image sha1. * Original image sha1.
*/ */
@PrimaryKey @PrimaryKey
val imageSHA1 : String val imageSHA1: String,
) )

View file

@ -8,11 +8,10 @@ import java.util.*
*/ */
@Dao @Dao
abstract class UploadedStatusDao { abstract class UploadedStatusDao {
/** /**
* Insert into uploaded status. * Insert into uploaded status.
*/ */
@Insert( onConflict = OnConflictStrategy.REPLACE ) @Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insert(uploadedStatus: UploadedStatus) abstract suspend fun insert(uploadedStatus: UploadedStatus)
/** /**
@ -31,13 +30,13 @@ abstract class UploadedStatusDao {
* Query uploaded status with image sha1. * Query uploaded status with image sha1.
*/ */
@Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ") @Query("SELECT * FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) ")
abstract suspend fun getFromImageSHA1(imageSHA1 : String) : UploadedStatus? abstract suspend fun getFromImageSHA1(imageSHA1: String): UploadedStatus?
/** /**
* Query uploaded status with modified image sha1. * Query uploaded status with modified image sha1.
*/ */
@Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ") @Query("SELECT * FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) ")
abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1 : String) : UploadedStatus? abstract suspend fun getFromModifiedImageSHA1(modifiedImageSHA1: String): UploadedStatus?
/** /**
* Asynchronous insert into uploaded status table. * Asynchronous insert into uploaded status table.
@ -51,20 +50,24 @@ abstract class UploadedStatusDao {
* Check whether the imageSHA1 is present in database * Check whether the imageSHA1 is present in database
*/ */
@Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ") @Query("SELECT COUNT() FROM uploaded_table WHERE imageSHA1 = (:imageSHA1) AND imageResult = (:imageResult) ")
abstract suspend fun findByImageSHA1(imageSHA1 : String, imageResult: Boolean): Int abstract suspend fun findByImageSHA1(
imageSHA1: String,
imageResult: Boolean,
): Int
/** /**
* Check whether the modifiedImageSHA1 is present in database * Check whether the modifiedImageSHA1 is present in database
*/ */
@Query("SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ") @Query(
abstract suspend fun findByModifiedImageSHA1(modifiedImageSHA1 : String, "SELECT COUNT() FROM uploaded_table WHERE modifiedImageSHA1 = (:modifiedImageSHA1) AND modifiedImageResult = (:modifiedImageResult) ",
modifiedImageResult: Boolean): Int )
abstract suspend fun findByModifiedImageSHA1(
modifiedImageSHA1: String,
modifiedImageResult: Boolean,
): Int
/** /**
* Asynchronous image sha1 query. * Asynchronous image sha1 query.
*/ */
suspend fun getUploadedFromImageSHA1(imageSHA1: String):UploadedStatus? { suspend fun getUploadedFromImageSHA1(imageSHA1: String): UploadedStatus? = getFromImageSHA1(imageSHA1)
return getFromImageSHA1(imageSHA1)
}
} }

View file

@ -10,30 +10,25 @@ import java.util.*
*/ */
@Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)]) @Entity(tableName = "uploaded_table", indices = [Index(value = ["modifiedImageSHA1"], unique = true)])
data class UploadedStatus( data class UploadedStatus(
/** /**
* Original image sha1. * Original image sha1.
*/ */
@PrimaryKey @PrimaryKey
val imageSHA1 : String, val imageSHA1: String,
/** /**
* Modified image sha1 (after exif changes). * Modified image sha1 (after exif changes).
*/ */
val modifiedImageSHA1 : String, val modifiedImageSHA1: String,
/** /**
* imageSHA1 query result from API. * imageSHA1 query result from API.
*/ */
var imageResult : Boolean, var imageResult: Boolean,
/** /**
* modifiedImageSHA1 query result from API. * modifiedImageSHA1 query result from API.
*/ */
var modifiedImageResult : Boolean, var modifiedImageResult: Boolean,
/** /**
* lastUpdated for data validation. * lastUpdated for data validation.
*/ */
var lastUpdated : Date? = null var lastUpdated: Date? = null,
) )

View file

@ -4,12 +4,10 @@ package fr.free.nrw.commons.customselector.helper
* Stores constants related to custom image selector * Stores constants related to custom image selector
*/ */
object CustomSelectorConstants { object CustomSelectorConstants {
const val BUCKET_ID = "bucket_id" const val BUCKET_ID = "bucket_id"
const val TOTAL_SELECTED_IMAGES = "total_selected_images" const val TOTAL_SELECTED_IMAGES = "total_selected_images"
const val PRESENT_POSITION = "present_position" const val PRESENT_POSITION = "present_position"
const val NEW_SELECTED_IMAGES = "new_selected_images" const val NEW_SELECTED_IMAGES = "new_selected_images"
const val SHOULD_REFRESH = "should_refresh" const val SHOULD_REFRESH = "should_refresh"
const val FULL_SCREEN_MODE_FIRST_LUNCH = "full_screen_mode_first_launch" const val FULL_SCREEN_MODE_FIRST_LUNCH = "full_screen_mode_first_launch"
} }

View file

@ -7,7 +7,6 @@ import fr.free.nrw.commons.customselector.model.Image
* Image Helper object, includes all the static functions and variables required by custom selector. * Image Helper object, includes all the static functions and variables required by custom selector.
*/ */
object ImageHelper { object ImageHelper {
/** /**
* Custom selector preference key * Custom selector preference key
*/ */
@ -39,7 +38,10 @@ object ImageHelper {
/** /**
* Filters the images based on the given bucketId (folder) * Filters the images based on the given bucketId (folder)
*/ */
fun filterImages(images: ArrayList<Image>, bukketId: Long?): ArrayList<Image> { fun filterImages(
images: ArrayList<Image>,
bukketId: Long?,
): ArrayList<Image> {
if (bukketId == null) return images if (bukketId == null) return images
val filteredImages = arrayListOf<Image>() val filteredImages = arrayListOf<Image>()
@ -54,30 +56,37 @@ object ImageHelper {
/** /**
* getIndex: Returns the index of image in given list. * getIndex: Returns the index of image in given list.
*/ */
fun getIndex(list: ArrayList<Image>, image: Image): Int { fun getIndex(
return list.indexOf(image) list: ArrayList<Image>,
} image: Image,
): Int = list.indexOf(image)
/** /**
* getIndex: Returns the index of image in given list. * getIndex: Returns the index of image in given list.
*/ */
fun getIndexFromId(list: ArrayList<Image>, imageId: Long): Int { fun getIndexFromId(
for(i in list){ list: ArrayList<Image>,
if(i.id == imageId) imageId: Long,
): Int {
for (i in list) {
if (i.id == imageId) {
return list.indexOf(i) return list.indexOf(i)
} }
return 0; }
return 0
} }
/** /**
* Gets the list of indices from the master list. * Gets the list of indices from the master list.
*/ */
fun getIndexList(list: ArrayList<Image>, masterList: ArrayList<Image>): ArrayList<Int> { fun getIndexList(
list: ArrayList<Image>,
masterList: ArrayList<Image>,
): ArrayList<Int> {
// Can be optimised as masterList is sorted by time. // Can be optimised as masterList is sorted by time.
val indexes = arrayListOf<Int>() val indexes = arrayListOf<Int>()
for(image in list) { for (image in list) {
val index = getIndex(masterList, image) val index = getIndex(masterList, image)
if (index == -1) { if (index == -1) {
continue continue

View file

@ -8,17 +8,19 @@ import kotlin.math.abs
/** /**
* Class for detecting swipe gestures * Class for detecting swipe gestures
*/ */
open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener { open class OnSwipeTouchListener(
context: Context?,
) : View.OnTouchListener {
private val gestureDetector: GestureDetector private val gestureDetector: GestureDetector
private val SWIPE_THRESHOLD_HEIGHT = (getScreenResolution(context!!)).second / 3 private val SWIPE_THRESHOLD_HEIGHT = (getScreenResolution(context!!)).second / 3
private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3 private val SWIPE_THRESHOLD_WIDTH = (getScreenResolution(context!!)).first / 3
private val SWIPE_VELOCITY_THRESHOLD = 1000 private val SWIPE_VELOCITY_THRESHOLD = 1000
override fun onTouch(view: View?, motionEvent: MotionEvent): Boolean { override fun onTouch(
return gestureDetector.onTouchEvent(motionEvent) view: View?,
} motionEvent: MotionEvent,
): Boolean = gestureDetector.onTouchEvent(motionEvent)
fun getScreenResolution(context: Context): Pair<Int, Int> { fun getScreenResolution(context: Context): Pair<Int, Int> {
val wm: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager val wm: WindowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
@ -31,10 +33,7 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
} }
inner class GestureListener : GestureDetector.SimpleOnGestureListener() { inner class GestureListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent): Boolean = true
override fun onDown(e: MotionEvent): Boolean {
return true
}
/** /**
* Detects the gestures * Detects the gestures
@ -43,14 +42,16 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
event1: MotionEvent?, event1: MotionEvent?,
event2: MotionEvent, event2: MotionEvent,
velocityX: Float, velocityX: Float,
velocityY: Float velocityY: Float,
): Boolean { ): Boolean {
try { try {
val diffY: Float = event2.y - (event1?.y ?: event2.y) val diffY: Float = event2.y - (event1?.y ?: event2.y)
val diffX: Float = event2.x - (event1?.x ?: event2.x) val diffX: Float = event2.x - (event1?.x ?: event2.x)
if (abs(diffX) > abs(diffY)) { if (abs(diffX) > abs(diffY)) {
if (abs(diffX) > SWIPE_THRESHOLD_WIDTH && abs(velocityX) > if (abs(diffX) > SWIPE_THRESHOLD_WIDTH &&
SWIPE_VELOCITY_THRESHOLD) { abs(velocityX) >
SWIPE_VELOCITY_THRESHOLD
) {
if (diffX > 0) { if (diffX > 0) {
onSwipeRight() onSwipeRight()
} else { } else {
@ -58,8 +59,10 @@ open class OnSwipeTouchListener(context: Context?) : View.OnTouchListener {
} }
} }
} else { } else {
if (abs(diffY) > SWIPE_THRESHOLD_HEIGHT && abs(velocityY) > if (abs(diffY) > SWIPE_THRESHOLD_HEIGHT &&
SWIPE_VELOCITY_THRESHOLD) { abs(velocityY) >
SWIPE_VELOCITY_THRESHOLD
) {
if (diffY > 0) { if (diffY > 0) {
onSwipeDown() onSwipeDown()
} else { } else {

View file

@ -4,12 +4,15 @@ package fr.free.nrw.commons.customselector.listeners
* Custom Selector Folder Click Listener * Custom Selector Folder Click Listener
*/ */
interface FolderClickListener { interface FolderClickListener {
/** /**
* onFolderClick * onFolderClick
* @param folderId : folder id of the folder. * @param folderId : folder id of the folder.
* @param folderName : folder name of the folder. * @param folderName : folder name of the folder.
* @param lastItemId : last scroll position in the folder. * @param lastItemId : last scroll position in the folder.
*/ */
fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) fun onFolderClick(
folderId: Long,
folderName: String,
lastItemId: Long,
)
} }

View file

@ -7,7 +7,6 @@ import fr.free.nrw.commons.customselector.model.Image
* responds to the device image query. * responds to the device image query.
*/ */
interface ImageLoaderListener { interface ImageLoaderListener {
/** /**
* On image loaded * On image loaded
* @param images : queried device images. * @param images : queried device images.

View file

@ -1,19 +1,20 @@
package fr.free.nrw.commons.customselector.listeners package fr.free.nrw.commons.customselector.listeners
import android.net.Uri
import fr.free.nrw.commons.customselector.model.Image import fr.free.nrw.commons.customselector.model.Image
/** /**
* Custom selector Image select listener * Custom selector Image select listener
*/ */
interface ImageSelectListener { interface ImageSelectListener {
/** /**
* onSelectedImagesChanged * onSelectedImagesChanged
* @param selectedImages : new selected images. * @param selectedImages : new selected images.
* @param selectedNotForUploadImages : number of selected not for upload images * @param selectedNotForUploadImages : number of selected not for upload images
*/ */
fun onSelectedImagesChanged(selectedImages: ArrayList<Image>, selectedNotForUploadImages: Int) fun onSelectedImagesChanged(
selectedImages: ArrayList<Image>,
selectedNotForUploadImages: Int,
)
/** /**
* onLongPress * onLongPress
@ -22,6 +23,6 @@ interface ImageSelectListener {
fun onLongPress( fun onLongPress(
position: Int, position: Int,
images: ArrayList<Image>, images: ArrayList<Image>,
selectedImages: ArrayList<Image> selectedImages: ArrayList<Image>,
) )
} }

View file

@ -6,5 +6,8 @@ import fr.free.nrw.commons.customselector.model.Image
* Interface to pass data between fragment and activity * Interface to pass data between fragment and activity
*/ */
interface PassDataListener { interface PassDataListener {
fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) fun passSelectedImages(
selectedImages: ArrayList<Image>,
shouldRefresh: Boolean,
)
} }

View file

@ -8,24 +8,19 @@ data class Folder(
bucketId : Unique directory id, eg 540528482 bucketId : Unique directory id, eg 540528482
*/ */
var bucketId: Long, var bucketId: Long,
/** /**
name : bucket/folder name, eg Camera name : bucket/folder name, eg Camera
*/ */
var name: String, var name: String,
/** /**
images : folder images, list of all images under this folder. images : folder images, list of all images under this folder.
*/ */
var images: ArrayList<Image> = arrayListOf<Image>() var images: ArrayList<Image> = arrayListOf<Image>(),
) { ) {
/** /**
* Indicates whether some other object is "equal to" this one. * Indicates whether some other object is "equal to" this one.
*/ */
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (javaClass != other?.javaClass) { if (javaClass != other?.javaClass) {
return false return false
} }

View file

@ -12,62 +12,57 @@ data class Image(
id : Unique image id, primary key of image in device, eg 104950 id : Unique image id, primary key of image in device, eg 104950
*/ */
var id: Long, var id: Long,
/** /**
name : Name of the image with extension, eg CommonsLogo.jpeg name : Name of the image with extension, eg CommonsLogo.jpeg
*/ */
var name: String, var name: String,
/** /**
uri : Uri of the image, points to image location or name, eg content://media/external/images/camera/10495 (Android 10) uri : Uri of the image, points to image location or name, eg content://media/external/images/camera/10495 (Android 10)
*/ */
var uri: Uri, var uri: Uri,
/** /**
path : System path of the image, eg storage/emulated/0/camera/CommonsLogo.jpeg path : System path of the image, eg storage/emulated/0/camera/CommonsLogo.jpeg
*/ */
var path: String, var path: String,
/** /**
bucketId : bucketId of folder, eg 540528482 bucketId : bucketId of folder, eg 540528482
*/ */
var bucketId: Long = 0, var bucketId: Long = 0,
/** /**
bucketName : name of folder, eg Camera bucketName : name of folder, eg Camera
*/ */
var bucketName: String = "", var bucketName: String = "",
/** /**
sha1 : sha1 of original image. sha1 : sha1 of original image.
*/ */
var sha1: String = "", var sha1: String = "",
/** /**
* date: Creation date of the image to show it inside the bubble during bubble scroll. * date: Creation date of the image to show it inside the bubble during bubble scroll.
*/ */
var date: String = "" var date: String = "",
) : Parcelable { ) : Parcelable {
/** /**
default parcelable constructor. default parcelable constructor.
*/ */
constructor(parcel: Parcel): constructor(parcel: Parcel) :
this(parcel.readLong(), this(
parcel.readLong(),
parcel.readString()!!, parcel.readString()!!,
parcel.readParcelable(Uri::class.java.classLoader)!!, parcel.readParcelable(Uri::class.java.classLoader)!!,
parcel.readString()!!, parcel.readString()!!,
parcel.readLong(), parcel.readLong(),
parcel.readString()!!, parcel.readString()!!,
parcel.readString()!!, parcel.readString()!!,
parcel.readString()!! parcel.readString()!!,
) )
/** /**
Write to parcel method. Write to parcel method.
*/ */
override fun writeToParcel(parcel: Parcel, flags: Int) { override fun writeToParcel(
parcel: Parcel,
flags: Int,
) {
parcel.writeLong(id) parcel.writeLong(id)
parcel.writeString(name) parcel.writeString(name)
parcel.writeParcelable(uri, flags) parcel.writeParcelable(uri, flags)
@ -81,41 +76,38 @@ data class Image(
/** /**
* Describe the kinds of special objects contained in this Parcelable * Describe the kinds of special objects contained in this Parcelable
*/ */
override fun describeContents(): Int { override fun describeContents(): Int = 0
return 0
}
/** /**
* Indicates whether some other object is "equal to" this one. * Indicates whether some other object is "equal to" this one.
*/ */
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (javaClass != other?.javaClass) {
if(javaClass != other?.javaClass) {
return false return false
} }
other as Image other as Image
if(id != other.id) { if (id != other.id) {
return false; return false
} }
if(name != other.name) { if (name != other.name) {
return false; return false
} }
if(uri != other.uri) { if (uri != other.uri) {
return false; return false
} }
if(path != other.path) { if (path != other.path) {
return false; return false
} }
if(bucketId != other.bucketId) { if (bucketId != other.bucketId) {
return false; return false
} }
if(bucketName != other.bucketName) { if (bucketName != other.bucketName) {
return false; return false
} }
if(sha1 != other.sha1) { if (sha1 != other.sha1) {
return false; return false
} }
return true return true
@ -125,12 +117,8 @@ data class Image(
* Parcelable companion object * Parcelable companion object
*/ */
companion object CREATOR : Parcelable.Creator<Image> { companion object CREATOR : Parcelable.Creator<Image> {
override fun createFromParcel(parcel: Parcel): Image { override fun createFromParcel(parcel: Parcel): Image = Image(parcel)
return Image(parcel)
}
override fun newArray(size: Int): Array<Image?> { override fun newArray(size: Int): Array<Image?> = arrayOfNulls(size)
return arrayOfNulls(size)
}
} }
} }

View file

@ -7,10 +7,9 @@ data class Result(
/** /**
* CallbackStatus : stores the result status * CallbackStatus : stores the result status
*/ */
val status:CallbackStatus, val status: CallbackStatus,
/** /**
* Images : images retrieved * Images : images retrieved
*/ */
val images: ArrayList<Image>) { val images: ArrayList<Image>,
} )

View file

@ -21,14 +21,11 @@ class FolderAdapter(
* Application context. * Application context.
*/ */
context: Context, context: Context,
/** /**
* Folder Click listener for click events. * Folder Click listener for click events.
*/ */
private val itemClickListener: FolderClickListener private val itemClickListener: FolderClickListener,
) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) { ) : RecyclerViewAdapter<FolderAdapter.FolderViewHolder?>(context) {
/** /**
* List of folders. * List of folders.
*/ */
@ -37,7 +34,10 @@ class FolderAdapter(
/** /**
* Create view holder, returns View holder item. * Create view holder, returns View holder item.
*/ */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder { override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): FolderViewHolder {
val itemView = inflater.inflate(R.layout.item_custom_selector_folder, parent, false) val itemView = inflater.inflate(R.layout.item_custom_selector_folder, parent, false)
return FolderViewHolder(itemView) return FolderViewHolder(itemView)
} }
@ -45,28 +45,31 @@ class FolderAdapter(
/** /**
* Bind view holder, setup the item view, title, count and click listener * Bind view holder, setup the item view, title, count and click listener
*/ */
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) { override fun onBindViewHolder(
holder: FolderViewHolder,
position: Int,
) {
val folder = folders[position] val folder = folders[position]
val toBeRemoved = ArrayList<Image>() val toBeRemoved = ArrayList<Image>()
for(image in folder.images) { for (image in folder.images) {
// Remove all the top images that do not exist anymore // Remove all the top images that do not exist anymore
if(context.contentResolver.getType(image.uri) == null){ if (context.contentResolver.getType(image.uri) == null) {
// File not found // File not found
toBeRemoved.add(image) toBeRemoved.add(image)
} else { } else {
break break
} }
} }
holder.image.setImageDrawable (null) holder.image.setImageDrawable(null)
folder.images.removeAll(toBeRemoved) folder.images.removeAll(toBeRemoved)
val count = folder.images.size val count = folder.images.size
if(count == 0 && folders.size > 0) { if (count == 0 && folders.size > 0) {
// Folder is empty, remove folder from the adapter. // Folder is empty, remove folder from the adapter.
holder.itemView.post{ holder.itemView.post {
val updatePosition = folders.indexOf(folder) val updatePosition = folders.indexOf(folder)
if(updatePosition != -1) { if (updatePosition != -1) {
folders.removeAt(updatePosition) folders.removeAt(updatePosition)
notifyItemRemoved(updatePosition) notifyItemRemoved(updatePosition)
notifyItemRangeChanged(updatePosition, folders.size) notifyItemRangeChanged(updatePosition, folders.size)
@ -89,8 +92,9 @@ class FolderAdapter(
fun init(newFolders: List<Folder>) { fun init(newFolders: List<Folder>) {
val oldFolderList: MutableList<Folder> = folders val oldFolderList: MutableList<Folder> = folders
val newFolderList = newFolders.toMutableList() val newFolderList = newFolders.toMutableList()
val diffResult = DiffUtil.calculateDiff( val diffResult =
FoldersDiffCallback(oldFolderList, newFolderList) DiffUtil.calculateDiff(
FoldersDiffCallback(oldFolderList, newFolderList),
) )
folders = newFolderList folders = newFolderList
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
@ -99,15 +103,14 @@ class FolderAdapter(
/** /**
* returns item count. * returns item count.
*/ */
override fun getItemCount(): Int { override fun getItemCount(): Int = folders.size
return folders.size
}
/** /**
* Folder view holder. * Folder view holder.
*/ */
class FolderViewHolder(itemView:View) : RecyclerView.ViewHolder(itemView) { class FolderViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
/** /**
* Folder thumbnail image view. * Folder thumbnail image view.
*/ */
@ -129,37 +132,33 @@ class FolderAdapter(
*/ */
class FoldersDiffCallback( class FoldersDiffCallback(
var oldFolders: MutableList<Folder>, var oldFolders: MutableList<Folder>,
var newFolders: MutableList<Folder> var newFolders: MutableList<Folder>,
) : DiffUtil.Callback() { ) : DiffUtil.Callback() {
/** /**
* Returns the size of the old list. * Returns the size of the old list.
*/ */
override fun getOldListSize(): Int { override fun getOldListSize(): Int = oldFolders.size
return oldFolders.size
}
/** /**
* Returns the size of the new list. * Returns the size of the new list.
*/ */
override fun getNewListSize(): Int { override fun getNewListSize(): Int = newFolders.size
return newFolders.size
}
/** /**
* Called by the DiffUtil to decide whether two object represent the same Item. * Called by the DiffUtil to decide whether two object represent the same Item.
*/ */
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(
return oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId oldItemPosition: Int,
} newItemPosition: Int,
): Boolean = oldFolders.get(oldItemPosition).bucketId == newFolders.get(newItemPosition).bucketId
/** /**
* Called by the DiffUtil when it wants to check whether two items have the same data. * Called by the DiffUtil when it wants to check whether two items have the same data.
* DiffUtil uses this information to detect if the contents of an item has changed. * DiffUtil uses this information to detect if the contents of an item has changed.
*/ */
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(
return oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition)) oldItemPosition: Int,
newItemPosition: Int,
): Boolean = oldFolders.get(oldItemPosition).equals(newFolders.get(newItemPosition))
} }
}
} }

View file

@ -5,7 +5,6 @@ import android.content.SharedPreferences
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.constraintlayout.widget.Group import androidx.constraintlayout.widget.Group
import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.DiffUtil
@ -32,20 +31,16 @@ class ImageAdapter(
* Application Context. * Application Context.
*/ */
context: Context, context: Context,
/** /**
* Image select listener for click events on image. * Image select listener for click events on image.
*/ */
private var imageSelectListener: ImageSelectListener, private var imageSelectListener: ImageSelectListener,
/** /**
* ImageLoader queries images. * ImageLoader queries images.
*/ */
private var imageLoader: ImageLoader private var imageLoader: ImageLoader,
): ) : RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context),
FastScrollRecyclerView.SectionedAdapter {
RecyclerViewAdapter<ImageAdapter.ImageViewHolder>(context), FastScrollRecyclerView.SectionedAdapter {
/** /**
* ImageSelectedOrUpdated payload class. * ImageSelectedOrUpdated payload class.
*/ */
@ -106,14 +101,17 @@ class ImageAdapter(
/** /**
* Coroutine Dispatchers and Scope. * Coroutine Dispatchers and Scope.
*/ */
private var defaultDispatcher : CoroutineDispatcher = Dispatchers.Default private var defaultDispatcher: CoroutineDispatcher = Dispatchers.Default
private var ioDispatcher : CoroutineDispatcher = Dispatchers.IO private var ioDispatcher: CoroutineDispatcher = Dispatchers.IO
private val scope : CoroutineScope = MainScope() private val scope: CoroutineScope = MainScope()
/** /**
* Create View holder. * Create View holder.
*/ */
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageViewHolder { override fun onCreateViewHolder(
parent: ViewGroup,
viewType: Int,
): ImageViewHolder {
val itemView = inflater.inflate(R.layout.item_custom_selector_image, parent, false) val itemView = inflater.inflate(R.layout.item_custom_selector_image, parent, false)
return ImageViewHolder(itemView) return ImageViewHolder(itemView)
} }
@ -121,10 +119,15 @@ class ImageAdapter(
/** /**
* Bind View holder, load image, selected view, click listeners. * Bind View holder, load image, selected view, click listeners.
*/ */
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) { override fun onBindViewHolder(
if(images.size == 0) { return } holder: ImageViewHolder,
var image=images[position] position: Int,
holder.image.setImageDrawable (null) ) {
if (images.size == 0) {
return
}
var image = images[position]
holder.image.setImageDrawable(null)
if (context.contentResolver.getType(image.uri) == null) { if (context.contentResolver.getType(image.uri) == null) {
// Image does not exist anymore, update adapter. // Image does not exist anymore, update adapter.
holder.itemView.post { holder.itemView.post {
@ -140,7 +143,8 @@ class ImageAdapter(
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
// Getting selected index when switch is on // Getting selected index when switch is on
val selectedIndex: Int = if (showAlreadyActionedImages) { val selectedIndex: Int =
if (showAlreadyActionedImages) {
ImageHelper.getIndex(selectedImages, image) ImageHelper.getIndex(selectedImages, image)
// Getting selected index when switch is off // Getting selected index when switch is off
@ -160,7 +164,11 @@ class ImageAdapter(
holder.itemUnselected() holder.itemUnselected()
} }
imageLoader.queryAndSetView( imageLoader.queryAndSetView(
holder, image, ioDispatcher, defaultDispatcher ,uploadingContributionList holder,
image,
ioDispatcher,
defaultDispatcher,
uploadingContributionList,
) )
scope.launch { scope.launch {
val sharedPreferences: SharedPreferences = val sharedPreferences: SharedPreferences =
@ -177,18 +185,24 @@ class ImageAdapter(
// inside map, so it will fetch the image from the map and load in the holder // inside map, so it will fetch the image from the map and load in the holder
} else { } else {
val actionableImages: List<Image> = ArrayList(actionableImagesMap.values) val actionableImages: List<Image> = ArrayList(actionableImagesMap.values)
if(actionableImages.size > position) { if (actionableImages.size > position) {
image = actionableImages[position] image = actionableImages[position]
Glide.with(holder.image).load(image.uri) Glide
.thumbnail(0.3f).into(holder.image) .with(holder.image)
.load(image.uri)
.thumbnail(0.3f)
.into(holder.image)
} }
} }
// If switch is turned off, it just fetches the image from all images without any // If switch is turned off, it just fetches the image from all images without any
// further operations // further operations
} else { } else {
Glide.with(holder.image).load(image.uri) Glide
.thumbnail(0.3f).into(holder.image) .with(holder.image)
.load(image.uri)
.thumbnail(0.3f)
.into(holder.image)
} }
} }
@ -210,11 +224,15 @@ class ImageAdapter(
suspend fun processThumbnailForActionedImage( suspend fun processThumbnailForActionedImage(
holder: ImageViewHolder, holder: ImageViewHolder,
position: Int, position: Int,
uploadingContributionList: List<Contribution> uploadingContributionList: List<Contribution>,
) { ) {
val next = imageLoader.nextActionableImage( val next =
allImages, ioDispatcher, defaultDispatcher, imageLoader.nextActionableImage(
nextImagePosition, uploadingContributionList allImages,
ioDispatcher,
defaultDispatcher,
nextImagePosition,
uploadingContributionList,
) )
// If next actionable image is found, saves it, as the the search for // If next actionable image is found, saves it, as the the search for
@ -229,8 +247,11 @@ class ImageAdapter(
actionableImagesMap[next] = allImages[next] actionableImagesMap[next] = allImages[next]
alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder) alreadyAddedPositions.add(imagePositionAsPerIncreasingOrder)
imagePositionAsPerIncreasingOrder++ imagePositionAsPerIncreasingOrder++
Glide.with(holder.image).load(allImages[next].uri) Glide
.thumbnail(0.3f).into(holder.image) .with(holder.image)
.load(allImages[next].uri)
.thumbnail(0.3f)
.into(holder.image)
notifyItemInserted(position) notifyItemInserted(position)
notifyItemRangeChanged(position, itemCount + 1) notifyItemRangeChanged(position, itemCount + 1)
} }
@ -248,7 +269,7 @@ class ImageAdapter(
*/ */
private fun onThumbnailClicked( private fun onThumbnailClicked(
position: Int, position: Int,
holder: ImageViewHolder holder: ImageViewHolder,
) { ) {
val sharedPreferences: SharedPreferences = val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0) context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
@ -269,7 +290,10 @@ class ImageAdapter(
/** /**
* Handle click event on an image, update counter on images. * Handle click event on an image, update counter on images.
*/ */
private fun selectOrRemoveImage(holder: ImageViewHolder, position: Int){ private fun selectOrRemoveImage(
holder: ImageViewHolder,
position: Int,
) {
val sharedPreferences: SharedPreferences = val sharedPreferences: SharedPreferences =
context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0) context.getSharedPreferences(CUSTOM_SELECTOR_PREFERENCE_KEY, 0)
val showAlreadyActionedImages = val showAlreadyActionedImages =
@ -277,7 +301,8 @@ class ImageAdapter(
// Getting clicked index from all images index when show_already_actioned_images // Getting clicked index from all images index when show_already_actioned_images
// switch is on // switch is on
val clickedIndex: Int = if(showAlreadyActionedImages) { val clickedIndex: Int =
if (showAlreadyActionedImages) {
ImageHelper.getIndex(selectedImages, images[position]) ImageHelper.getIndex(selectedImages, images[position])
// Getting clicked index from actionable images when show_already_actioned_images // Getting clicked index from actionable images when show_already_actioned_images
@ -294,7 +319,8 @@ class ImageAdapter(
notifyItemChanged(position, ImageUnselected()) notifyItemChanged(position, ImageUnselected())
// Getting index from all images index when switch is on // Getting index from all images index when switch is on
val indexes = if (showAlreadyActionedImages) { val indexes =
if (showAlreadyActionedImages) {
ImageHelper.getIndexList(selectedImages, images) ImageHelper.getIndexList(selectedImages, images)
// Getting index from actionable images when switch is off // Getting index from actionable images when switch is off
@ -313,7 +339,8 @@ class ImageAdapter(
} }
// Getting index from all images index when switch is on // Getting index from all images index when switch is on
val indexes: ArrayList<Int> = if (showAlreadyActionedImages) { val indexes: ArrayList<Int> =
if (showAlreadyActionedImages) {
selectedImages.add(images[position]) selectedImages.add(images[position])
ImageHelper.getIndexList(selectedImages, images) ImageHelper.getIndexList(selectedImages, images)
@ -334,10 +361,15 @@ class ImageAdapter(
/** /**
* Initialize the data set. * Initialize the data set.
*/ */
fun init(newImages: List<Image>, fixedImages: List<Image>, emptyMap: TreeMap<Int, Image>, uploadedImages: List<Contribution> = ArrayList()) { fun init(
newImages: List<Image>,
fixedImages: List<Image>,
emptyMap: TreeMap<Int, Image>,
uploadedImages: List<Contribution> = ArrayList(),
) {
allImages = fixedImages allImages = fixedImages
val oldImageList:ArrayList<Image> = images val oldImageList: ArrayList<Image> = images
val newImageList:ArrayList<Image> = ArrayList(newImages) val newImageList: ArrayList<Image> = ArrayList(newImages)
actionableImagesMap = emptyMap actionableImagesMap = emptyMap
alreadyAddedPositions = ArrayList() alreadyAddedPositions = ArrayList()
uploadingContributionList = uploadedImages uploadingContributionList = uploadedImages
@ -345,8 +377,9 @@ class ImageAdapter(
reachedEndOfFolder = false reachedEndOfFolder = false
selectedImages = ArrayList() selectedImages = ArrayList()
imagePositionAsPerIncreasingOrder = 0 imagePositionAsPerIncreasingOrder = 0
val diffResult = DiffUtil.calculateDiff( val diffResult =
ImagesDiffCallback(oldImageList, newImageList) DiffUtil.calculateDiff(
ImagesDiffCallback(oldImageList, newImageList),
) )
images = newImageList images = newImageList
diffResult.dispatchUpdatesTo(this) diffResult.dispatchUpdatesTo(this)
@ -355,31 +388,35 @@ class ImageAdapter(
/** /**
* Set new selected images * Set new selected images
*/ */
fun setSelectedImages(newSelectedImages: ArrayList<Image>){ fun setSelectedImages(newSelectedImages: ArrayList<Image>) {
selectedImages = ArrayList(newSelectedImages) selectedImages = ArrayList(newSelectedImages)
imageSelectListener.onSelectedImagesChanged(selectedImages, 0) imageSelectListener.onSelectedImagesChanged(selectedImages, 0)
} }
/** /**
* Refresh the data in the adapter * Refresh the data in the adapter
*/ */
fun refresh(newImages: List<Image>, fixedImages: List<Image>, uploadingImages: List<Contribution> = ArrayList()) { fun refresh(
newImages: List<Image>,
fixedImages: List<Image>,
uploadingImages: List<Contribution> = ArrayList(),
) {
numberOfSelectedImagesMarkedAsNotForUpload = 0 numberOfSelectedImagesMarkedAsNotForUpload = 0
images.clear() images.clear()
selectedImages = arrayListOf() selectedImages = arrayListOf()
init(newImages, fixedImages, TreeMap(),uploadingImages) init(newImages, fixedImages, TreeMap(), uploadingImages)
notifyDataSetChanged() notifyDataSetChanged()
} }
/** /**
* Clear selected images and empty the list. * Clear selected images and empty the list.
*/ */
fun clearSelectedImages(){ fun clearSelectedImages() {
numberOfSelectedImagesMarkedAsNotForUpload = 0 numberOfSelectedImagesMarkedAsNotForUpload = 0
selectedImages.clear() selectedImages.clear()
selectedImages = arrayListOf() selectedImages = arrayListOf()
} }
/** /**
* Remove image from actionable images map. * Remove image from actionable images map.
*/ */
@ -389,7 +426,7 @@ class ImageAdapter(
val showAlreadyActionedImages = val showAlreadyActionedImages =
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
if(showAlreadyActionedImages) { if (showAlreadyActionedImages) {
refresh(allImages, allImages, uploadingContributionList) refresh(allImages, allImages, uploadingContributionList)
} else { } else {
val iterator = actionableImagesMap.entries.iterator() val iterator = actionableImagesMap.entries.iterator()
@ -402,16 +439,14 @@ class ImageAdapter(
iterator.remove() iterator.remove()
alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1) alreadyAddedPositions.removeAt(alreadyAddedPositions.size - 1)
notifyItemRemoved(index) notifyItemRemoved(index)
notifyItemRangeChanged(index, itemCount ) notifyItemRangeChanged(index, itemCount)
break break
} }
index++ index++
} }
} }
} }
/** /**
* Returns the total number of items in the data set held by the adapter. * Returns the total number of items in the data set held by the adapter.
* *
@ -424,7 +459,7 @@ class ImageAdapter(
sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true) sharedPreferences.getBoolean(SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, true)
// While switch is on initializes the holder with all images size // While switch is on initializes the holder with all images size
return if(showAlreadyActionedImages) { return if (showAlreadyActionedImages) {
allImages.size allImages.size
// While switch is off and searching for next actionable has ended, initializes the holder // While switch is off and searching for next actionable has ended, initializes the holder
@ -439,9 +474,7 @@ class ImageAdapter(
} }
} }
fun getImageIdAt(position: Int): Long { fun getImageIdAt(position: Int): Long = images.get(position).id
return images.get(position).id
}
/** /**
* CleanUp function. * CleanUp function.
@ -453,7 +486,9 @@ class ImageAdapter(
/** /**
* Image view holder. * Image view holder.
*/ */
class ImageViewHolder(itemView: View): RecyclerView.ViewHolder(itemView) { class ImageViewHolder(
itemView: View,
) : RecyclerView.ViewHolder(itemView) {
val image: ImageView = itemView.findViewById(R.id.image_thumbnail) val image: ImageView = itemView.findViewById(R.id.image_thumbnail)
private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group) private val uploadedGroup: Group = itemView.findViewById(R.id.uploaded_group)
private val uploadingGroup: Group = itemView.findViewById(R.id.uploading_group) private val uploadingGroup: Group = itemView.findViewById(R.id.uploading_group)
@ -495,16 +530,12 @@ class ImageAdapter(
notForUploadGroup.visibility = View.VISIBLE notForUploadGroup.visibility = View.VISIBLE
} }
fun isItemUploaded():Boolean { fun isItemUploaded(): Boolean = uploadedGroup.visibility == View.VISIBLE
return uploadedGroup.visibility == View.VISIBLE
}
/** /**
* Item is not for upload * Item is not for upload
*/ */
fun isItemNotForUpload():Boolean { fun isItemNotForUpload(): Boolean = notForUploadGroup.visibility == View.VISIBLE
return notForUploadGroup.visibility == View.VISIBLE
}
/** /**
* Item is not uploading * Item is not uploading
@ -533,45 +564,38 @@ class ImageAdapter(
*/ */
class ImagesDiffCallback( class ImagesDiffCallback(
var oldImageList: ArrayList<Image>, var oldImageList: ArrayList<Image>,
var newImageList: ArrayList<Image> var newImageList: ArrayList<Image>,
) : DiffUtil.Callback(){ ) : DiffUtil.Callback() {
/** /**
* Returns the size of the old list. * Returns the size of the old list.
*/ */
override fun getOldListSize(): Int { override fun getOldListSize(): Int = oldImageList.size
return oldImageList.size
}
/** /**
* Returns the size of the new list. * Returns the size of the new list.
*/ */
override fun getNewListSize(): Int { override fun getNewListSize(): Int = newImageList.size
return newImageList.size
}
/** /**
* Called by the DiffUtil to decide whether two object represent the same Item. * Called by the DiffUtil to decide whether two object represent the same Item.
*/ */
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areItemsTheSame(
return newImageList[newItemPosition].id == oldImageList[oldItemPosition].id oldItemPosition: Int,
} newItemPosition: Int,
): Boolean = newImageList[newItemPosition].id == oldImageList[oldItemPosition].id
/** /**
* Called by the DiffUtil when it wants to check whether two items have the same data. * Called by the DiffUtil when it wants to check whether two items have the same data.
* DiffUtil uses this information to detect if the contents of an item has changed. * DiffUtil uses this information to detect if the contents of an item has changed.
*/ */
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { override fun areContentsTheSame(
return oldImageList[oldItemPosition].equals(newImageList[newItemPosition]) oldItemPosition: Int,
} newItemPosition: Int,
): Boolean = oldImageList[oldItemPosition].equals(newImageList[newItemPosition])
} }
/** /**
* Returns the text for showing inside the bubble during bubble scroll. * Returns the text for showing inside the bubble during bubble scroll.
*/ */
override fun getSectionName(position: Int): String { override fun getSectionName(position: Int): String = images[position].date
return images[position].date
}
} }

View file

@ -7,6 +7,8 @@ import androidx.recyclerview.widget.RecyclerView
/** /**
* Generic Recycler view adapter. * Generic Recycler view adapter.
*/ */
abstract class RecyclerViewAdapter<T : RecyclerView.ViewHolder?>(val context: Context): RecyclerView.Adapter<T>() { abstract class RecyclerViewAdapter<T : RecyclerView.ViewHolder?>(
val context: Context,
) : RecyclerView.Adapter<T>() {
val inflater: LayoutInflater = LayoutInflater.from(context) val inflater: LayoutInflater = LayoutInflater.from(context)
} }

View file

@ -8,20 +8,14 @@ import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.util.Log
import android.view.View import android.view.View
import android.view.Window import android.view.Window
import android.widget.Button import android.widget.Button
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.TextView import android.widget.TextView
import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -38,7 +32,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -57,23 +50,22 @@ import fr.free.nrw.commons.databinding.ActivityCustomSelectorBinding
import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding import fr.free.nrw.commons.databinding.CustomSelectorBottomLayoutBinding
import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding import fr.free.nrw.commons.databinding.CustomSelectorToolbarBinding
import fr.free.nrw.commons.filepicker.Constants import fr.free.nrw.commons.filepicker.Constants
import fr.free.nrw.commons.filepicker.FilePicker
import fr.free.nrw.commons.media.ZoomableActivity import fr.free.nrw.commons.media.ZoomableActivity
import fr.free.nrw.commons.theme.BaseActivity import fr.free.nrw.commons.theme.BaseActivity
import fr.free.nrw.commons.upload.FileUtilsWrapper import fr.free.nrw.commons.upload.FileUtilsWrapper
import fr.free.nrw.commons.utils.CustomSelectorUtils import fr.free.nrw.commons.utils.CustomSelectorUtils
import fr.free.nrw.commons.utils.PermissionUtils
import kotlinx.coroutines.* import kotlinx.coroutines.*
import java.io.File import java.io.File
import java.lang.Integer.max import java.lang.Integer.max
import javax.inject.Inject import javax.inject.Inject
/** /**
* Custom Selector Activity. * Custom Selector Activity.
*/ */
class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectListener { class CustomSelectorActivity :
BaseActivity(),
FolderClickListener,
ImageSelectListener {
/** /**
* ViewBindings * ViewBindings
*/ */
@ -147,7 +139,7 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
*/ */
var imageFragment: ImageFragment? = null var imageFragment: ImageFragment? = null
private var progressDialogText:String="" private var progressDialogText: String = ""
private var showPartialAccessIndicator by mutableStateOf(false) private var showPartialAccessIndicator by mutableStateOf(false)
@ -158,7 +150,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE &&
ContextCompat.checkSelfPermission( ContextCompat.checkSelfPermission(
this, Manifest.permission.READ_MEDIA_IMAGES this,
Manifest.permission.READ_MEDIA_IMAGES,
) == PackageManager.PERMISSION_DENIED ) == PackageManager.PERMISSION_DENIED
) { ) {
showPartialAccessIndicator = true showPartialAccessIndicator = true
@ -171,21 +164,23 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
PartialStorageAccessIndicator( PartialStorageAccessIndicator(
isVisible = showPartialAccessIndicator, isVisible = showPartialAccessIndicator,
onManage = { onManage = {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
requestPermissions(arrayOf(Manifest.permission.READ_MEDIA_IMAGES), 1) requestPermissions(arrayOf(Manifest.permission.READ_MEDIA_IMAGES), 1)
} }
}, },
modifier = Modifier modifier =
Modifier
.padding(vertical = 8.dp, horizontal = 4.dp) .padding(vertical = 8.dp, horizontal = 4.dp)
.fillMaxWidth() .fillMaxWidth(),
) )
} }
val view = binding.root val view = binding.root
setContentView(view) setContentView(view)
prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE) prefs = applicationContext.getSharedPreferences("CustomSelector", MODE_PRIVATE)
viewModel = ViewModelProvider(this, customSelectorViewModelFactory).get( viewModel =
CustomSelectorViewModel::class.java ViewModelProvider(this, customSelectorViewModelFactory).get(
CustomSelectorViewModel::class.java,
) )
setupViews() setupViews()
@ -208,11 +203,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(
requestCode: Int, requestCode: Int,
permissions: Array<out String>, permissions: Array<out String>,
grantResults: IntArray grantResults: IntArray,
) { ) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if(requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { if (requestCode == 1 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
if(grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showPartialAccessIndicator = false showPartialAccessIndicator = false
} }
} }
@ -226,7 +221,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
/** /**
* When data will be send from full screen mode, it will be passed to fragment * When data will be send from full screen mode, it will be passed to fragment
*/ */
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?,
) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE && if (requestCode == Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE &&
resultCode == Activity.RESULT_OK resultCode == Activity.RESULT_OK
@ -254,7 +253,8 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
* Set up view, default folder view. * Set up view, default folder view.
*/ */
private fun setupViews() { private fun setupViews() {
supportFragmentManager.beginTransaction() supportFragmentManager
.beginTransaction()
.replace(R.id.fragment_container, FolderFragment.newInstance()) .replace(R.id.fragment_container, FolderFragment.newInstance())
.commit() .commit()
setUpToolbar() setUpToolbar()
@ -322,11 +322,12 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
var allImagesAlreadyNotForUpload = true var allImagesAlreadyNotForUpload = true
images.forEach { image -> images.forEach { image ->
val imageSHA1 = CustomSelectorUtils.getImageSHA1( val imageSHA1 =
CustomSelectorUtils.getImageSHA1(
image.uri, image.uri,
ioDispatcher, ioDispatcher,
fileUtilsWrapper, fileUtilsWrapper,
contentResolver contentResolver,
) )
val exists = notForUploadStatusDao.find(imageSHA1) val exists = notForUploadStatusDao.find(imageSHA1)
if (exists < 1) { if (exists < 1) {
@ -337,11 +338,12 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
if (!allImagesAlreadyNotForUpload) { if (!allImagesAlreadyNotForUpload) {
// Insert or delete images as necessary, but the UI updates should be posted back to the main thread // Insert or delete images as necessary, but the UI updates should be posted back to the main thread
images.forEach { image -> images.forEach { image ->
val imageSHA1 = CustomSelectorUtils.getImageSHA1( val imageSHA1 =
CustomSelectorUtils.getImageSHA1(
image.uri, image.uri,
ioDispatcher, ioDispatcher,
fileUtilsWrapper, fileUtilsWrapper,
contentResolver contentResolver,
) )
notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1)) notForUploadStatusDao.insert(NotForUploadStatus(imageSHA1))
} }
@ -353,11 +355,12 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
} }
} else { } else {
images.forEach { image -> images.forEach { image ->
val imageSHA1 = CustomSelectorUtils.getImageSHA1( val imageSHA1 =
CustomSelectorUtils.getImageSHA1(
image.uri, image.uri,
ioDispatcher, ioDispatcher,
fileUtilsWrapper, fileUtilsWrapper,
contentResolver contentResolver,
) )
notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1) notForUploadStatusDao.deleteNotForUploadWithImageSHA1(imageSHA1)
} }
@ -386,13 +389,19 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
/** /**
* Change the title of the toolbar. * Change the title of the toolbar.
*/ */
private fun changeTitle(title: String, selectedImageCount:Int) { private fun changeTitle(
if (title.isNotEmpty()){ title: String,
selectedImageCount: Int,
) {
if (title.isNotEmpty()) {
val titleText = findViewById<TextView>(R.id.title) val titleText = findViewById<TextView>(R.id.title)
var titleWithAppendedImageCount = title var titleWithAppendedImageCount = title
if (selectedImageCount > 0) { if (selectedImageCount > 0) {
titleWithAppendedImageCount += " (${resources.getQuantityString(R.plurals.custom_picker_images_selected_title_appendix, titleWithAppendedImageCount += " (${resources.getQuantityString(
selectedImageCount, selectedImageCount)})" R.plurals.custom_picker_images_selected_title_appendix,
selectedImageCount,
selectedImageCount,
)})"
} }
if (titleText != null) { if (titleText != null) {
titleText.text = titleWithAppendedImageCount titleText.text = titleWithAppendedImageCount
@ -415,8 +424,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
/** /**
* override on folder click, change the toolbar title on folder click. * override on folder click, change the toolbar title on folder click.
*/ */
override fun onFolderClick(folderId: Long, folderName: String, lastItemId: Long) { override fun onFolderClick(
supportFragmentManager.beginTransaction() folderId: Long,
folderName: String,
lastItemId: Long,
) {
supportFragmentManager
.beginTransaction()
.add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId)) .add(R.id.fragment_container, ImageFragment.newInstance(folderId, lastItemId))
.addToBackStack(null) .addToBackStack(null)
.commit() .commit()
@ -433,18 +447,21 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
*/ */
override fun onSelectedImagesChanged( override fun onSelectedImagesChanged(
selectedImages: ArrayList<Image>, selectedImages: ArrayList<Image>,
selectedNotForUploadImages: Int selectedNotForUploadImages: Int,
) { ) {
viewModel.selectedImages.value = selectedImages viewModel.selectedImages.value = selectedImages
changeTitle(bucketName, selectedImages.size) changeTitle(bucketName, selectedImages.size)
uploadLimitExceeded = selectedImages.size > uploadLimit uploadLimitExceeded = selectedImages.size > uploadLimit
uploadLimitExceededBy = max(selectedImages.size - uploadLimit,0) uploadLimitExceededBy = max(selectedImages.size - uploadLimit, 0)
if (uploadLimitExceeded && selectedNotForUploadImages == 0) { if (uploadLimitExceeded && selectedNotForUploadImages == 0) {
toolbarBinding.imageLimitError.visibility = View.VISIBLE toolbarBinding.imageLimitError.visibility = View.VISIBLE
bottomSheetBinding.upload.text = resources.getString( bottomSheetBinding.upload.text =
R.string.custom_selector_button_limit_text, uploadLimit) resources.getString(
R.string.custom_selector_button_limit_text,
uploadLimit,
)
} else { } else {
toolbarBinding.imageLimitError.visibility = View.INVISIBLE toolbarBinding.imageLimitError.visibility = View.INVISIBLE
bottomSheetBinding.upload.text = resources.getString(R.string.upload) bottomSheetBinding.upload.text = resources.getString(R.string.upload)
@ -461,11 +478,11 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
bottomSheetBinding.notForUpload.text = bottomSheetBinding.notForUpload.text =
when (selectedImages.size == selectedNotForUploadImages) { when (selectedImages.size == selectedNotForUploadImages) {
true -> { true -> {
progressDialogText=getString(R.string.unmarking_as_not_for_upload) progressDialogText = getString(R.string.unmarking_as_not_for_upload)
getString(R.string.unmark_as_not_for_upload) getString(R.string.unmark_as_not_for_upload)
} }
else -> { else -> {
progressDialogText=getString(R.string.marking_as_not_for_upload) progressDialogText = getString(R.string.marking_as_not_for_upload)
getString(R.string.mark_as_not_for_upload) getString(R.string.mark_as_not_for_upload)
} }
} }
@ -481,13 +498,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
override fun onLongPress( override fun onLongPress(
position: Int, position: Int,
images: ArrayList<Image>, images: ArrayList<Image>,
selectedImages: ArrayList<Image> selectedImages: ArrayList<Image>,
) { ) {
val intent = Intent(this, ZoomableActivity::class.java) val intent = Intent(this, ZoomableActivity::class.java)
intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position) intent.putExtra(CustomSelectorConstants.PRESENT_POSITION, position)
intent.putParcelableArrayListExtra( intent.putParcelableArrayListExtra(
CustomSelectorConstants.TOTAL_SELECTED_IMAGES, CustomSelectorConstants.TOTAL_SELECTED_IMAGES,
selectedImages selectedImages,
) )
intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId) intent.putExtra(CustomSelectorConstants.BUCKET_ID, bucketId)
startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE) startActivityForResult(intent, Constants.RequestCodes.RECEIVE_DATA_FROM_FULL_SCREEN_MODE)
@ -547,10 +564,13 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
val dialog = Dialog(this) val dialog = Dialog(this)
dialog.requestWindowFeature(Window.FEATURE_NO_TITLE) dialog.requestWindowFeature(Window.FEATURE_NO_TITLE)
dialog.setContentView(R.layout.custom_selector_limit_dialog) dialog.setContentView(R.layout.custom_selector_limit_dialog)
(dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener() (dialog.findViewById(R.id.btn_dismiss_limit_warning) as Button).setOnClickListener { dialog.dismiss() }
{ dialog.dismiss() } (dialog.findViewById(R.id.upload_limit_warning) as TextView).text =
(dialog.findViewById(R.id.upload_limit_warning) as TextView).text = resources.getString( resources.getString(
R.string.custom_selector_over_limit_warning, uploadLimit, uploadLimitExceededBy) R.string.custom_selector_over_limit_warning,
uploadLimit,
uploadLimitExceededBy,
)
dialog.show() dialog.show()
} }
@ -560,9 +580,17 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
*/ */
override fun onDestroy() { override fun onDestroy() {
if (isImageFragmentOpen) { if (isImageFragmentOpen) {
prefs.edit().putLong(FOLDER_ID, bucketId).putString(FOLDER_NAME, bucketName).apply() prefs
.edit()
.putLong(FOLDER_ID, bucketId)
.putString(FOLDER_NAME, bucketName)
.apply()
} else { } else {
prefs.edit().remove(FOLDER_ID).remove(FOLDER_NAME).apply() prefs
.edit()
.remove(FOLDER_ID)
.remove(FOLDER_NAME)
.apply()
} }
super.onDestroy() super.onDestroy()
} }
@ -573,38 +601,41 @@ class CustomSelectorActivity : BaseActivity(), FolderClickListener, ImageSelectL
const val ITEM_ID: String = "ItemId" const val ITEM_ID: String = "ItemId"
} }
} }
@Composable @Composable
fun PartialStorageAccessIndicator( fun PartialStorageAccessIndicator(
isVisible: Boolean, isVisible: Boolean,
onManage: ()-> Unit, onManage: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier,
) { ) {
if(isVisible) { if (isVisible) {
OutlinedCard( OutlinedCard(
modifier = modifier, modifier = modifier,
colors = CardDefaults.cardColors( colors =
containerColor = colorResource(R.color.primarySuperLightColor) CardDefaults.cardColors(
containerColor = colorResource(R.color.primarySuperLightColor),
), ),
border = BorderStroke(0.5.dp, color = colorResource(R.color.primaryColor)), border = BorderStroke(0.5.dp, color = colorResource(R.color.primaryColor)),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp),
) { ) {
Row(modifier = Modifier.padding(16.dp).fillMaxWidth()) { Row(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
Text( Text(
text = "You've given access to a select number of photos", text = "You've given access to a select number of photos",
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f),
) )
TextButton( TextButton(
onClick = onManage, onClick = onManage,
modifier = Modifier.align(Alignment.Bottom), modifier = Modifier.align(Alignment.Bottom),
colors = ButtonDefaults.buttonColors( colors =
containerColor = colorResource(R.color.primaryColor) ButtonDefaults.buttonColors(
containerColor = colorResource(R.color.primaryColor),
), ),
shape = RoundedCornerShape(8.dp) shape = RoundedCornerShape(8.dp),
) { ) {
Text( Text(
text = "Manage", text = "Manage",
style = MaterialTheme.typography.labelMedium, style = MaterialTheme.typography.labelMedium,
color = colorResource(R.color.primaryTextColor) color = colorResource(R.color.primaryTextColor),
) )
} }
} }
@ -616,9 +647,13 @@ fun PartialStorageAccessIndicator(
@Composable @Composable
fun PartialStorageAccessIndicatorPreview() { fun PartialStorageAccessIndicatorPreview() {
Surface { Surface {
PartialStorageAccessIndicator(isVisible = true, onManage = {}, modifier = Modifier PartialStorageAccessIndicator(
isVisible = true,
onManage = {},
modifier =
Modifier
.padding(vertical = 8.dp, horizontal = 4.dp) .padding(vertical = 8.dp, horizontal = 4.dp)
.fillMaxWidth() .fillMaxWidth(),
) )
} }
} }

View file

@ -14,8 +14,10 @@ import kotlinx.coroutines.cancel
/** /**
* Custom Selector view model. * Custom Selector view model.
*/ */
class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFileLoader) : ViewModel() { class CustomSelectorViewModel(
var context: Context,
var imageFileLoader: ImageFileLoader,
) : ViewModel() {
/** /**
* Scope for coroutine task (image fetch). * Scope for coroutine task (image fetch).
*/ */
@ -37,7 +39,8 @@ class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFil
fun fetchImages() { fun fetchImages() {
result.postValue(Result(CallbackStatus.FETCHING, arrayListOf())) result.postValue(Result(CallbackStatus.FETCHING, arrayListOf()))
scope.cancel() scope.cancel()
imageFileLoader.loadDeviceImages(object: ImageLoaderListener { imageFileLoader.loadDeviceImages(
object : ImageLoaderListener {
override fun onImageLoaded(images: ArrayList<Image>) { override fun onImageLoaded(images: ArrayList<Image>) {
result.postValue(Result(CallbackStatus.SUCCESS, images)) result.postValue(Result(CallbackStatus.SUCCESS, images))
} }
@ -45,7 +48,8 @@ class CustomSelectorViewModel(var context: Context,var imageFileLoader: ImageFil
override fun onFailed(throwable: Throwable) { override fun onFailed(throwable: Throwable) {
result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf())) result.postValue(Result(CallbackStatus.SUCCESS, arrayListOf()))
} }
}) },
)
} }
/** /**

View file

@ -8,10 +8,12 @@ import javax.inject.Inject
/** /**
* View Model Factory. * View Model Factory.
*/ */
class CustomSelectorViewModelFactory @Inject constructor(val context: Context,val imageFileLoader: ImageFileLoader) : ViewModelProvider.Factory { class CustomSelectorViewModelFactory
@Inject
override fun<CustomSelectorViewModel: ViewModel> create(modelClass: Class<CustomSelectorViewModel>) : CustomSelectorViewModel { constructor(
return CustomSelectorViewModel(context,imageFileLoader) as CustomSelectorViewModel val context: Context,
val imageFileLoader: ImageFileLoader,
) : ViewModelProvider.Factory {
override fun <CustomSelectorViewModel : ViewModel> create(modelClass: Class<CustomSelectorViewModel>): CustomSelectorViewModel =
CustomSelectorViewModel(context, imageFileLoader) as CustomSelectorViewModel
} }
}

View file

@ -9,10 +9,10 @@ import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.customselector.helper.ImageHelper import fr.free.nrw.commons.customselector.helper.ImageHelper
import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.listeners.FolderClickListener import fr.free.nrw.commons.customselector.listeners.FolderClickListener
import fr.free.nrw.commons.customselector.model.CallbackStatus import fr.free.nrw.commons.customselector.model.CallbackStatus
import fr.free.nrw.commons.customselector.model.Folder import fr.free.nrw.commons.customselector.model.Folder
import fr.free.nrw.commons.customselector.model.Result
import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter import fr.free.nrw.commons.customselector.ui.adapter.FolderAdapter
import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding import fr.free.nrw.commons.databinding.FragmentCustomSelectorBinding
import fr.free.nrw.commons.di.CommonsDaggerSupportFragment import fr.free.nrw.commons.di.CommonsDaggerSupportFragment
@ -24,7 +24,6 @@ import javax.inject.Inject
* Custom selector folder fragment. * Custom selector folder fragment.
*/ */
class FolderFragment : CommonsDaggerSupportFragment() { class FolderFragment : CommonsDaggerSupportFragment() {
/** /**
* ViewBinding * ViewBinding
*/ */
@ -53,6 +52,7 @@ class FolderFragment : CommonsDaggerSupportFragment() {
var mediaClient: MediaClient? = null var mediaClient: MediaClient? = null
@Inject set @Inject set
/** /**
* Folder Adapter. * Folder Adapter.
*/ */
@ -66,15 +66,13 @@ class FolderFragment : CommonsDaggerSupportFragment() {
/** /**
* Folder List. * Folder List.
*/ */
private lateinit var folders : ArrayList<Folder> private lateinit var folders: ArrayList<Folder>
/** /**
* Companion newInstance. * Companion newInstance.
*/ */
companion object{ companion object {
fun newInstance(): FolderFragment { fun newInstance(): FolderFragment = FolderFragment()
return FolderFragment()
}
} }
/** /**
@ -83,21 +81,24 @@ class FolderFragment : CommonsDaggerSupportFragment() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel = ViewModelProvider(requireActivity(),customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java) viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory!!).get(CustomSelectorViewModel::class.java)
} }
/** /**
* OnCreateView. * OnCreateView.
* Inflate Layout, init adapter, init gridLayoutManager, setUp recycler view, observe the view model for result. * Inflate Layout, init adapter, init gridLayoutManager, setUp recycler view, observe the view model for result.
*/ */
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?,
): View? {
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener) folderAdapter = FolderAdapter(requireActivity(), activity as FolderClickListener)
gridLayoutManager = GridLayoutManager(context, columnCount()) gridLayoutManager = GridLayoutManager(context, columnCount())
selectorRV = binding?.selectorRv selectorRV = binding?.selectorRv
loader = binding?.loader loader = binding?.loader
with(binding?.selectorRv){ with(binding?.selectorRv) {
this?.layoutManager = gridLayoutManager this?.layoutManager = gridLayoutManager
this?.setHasFixedSize(true) this?.setHasFixedSize(true)
this?.adapter = folderAdapter this?.adapter = folderAdapter
@ -114,9 +115,9 @@ class FolderFragment : CommonsDaggerSupportFragment() {
* Load adapter. * Load adapter.
*/ */
private fun handleResult(result: Result) { private fun handleResult(result: Result) {
if(result.status is CallbackStatus.SUCCESS){ if (result.status is CallbackStatus.SUCCESS) {
val images = result.images val images = result.images
if(images.isEmpty()){ if (images.isEmpty()) {
binding?.emptyText?.let { binding?.emptyText?.let {
it.visibility = View.VISIBLE it.visibility = View.VISIBLE
} }

View file

@ -20,8 +20,9 @@ import kotlin.coroutines.CoroutineContext
* Custom Selector Image File Loader. * Custom Selector Image File Loader.
* Loads device images. * Loads device images.
*/ */
class ImageFileLoader(val context: Context) : CoroutineScope{ class ImageFileLoader(
val context: Context,
) : CoroutineScope {
/** /**
* Coroutine context for fetching images. * Coroutine context for fetching images.
*/ */
@ -30,13 +31,14 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
/** /**
* Media paramerters required. * Media paramerters required.
*/ */
private val projection = arrayOf( private val projection =
arrayOf(
MediaStore.Images.Media._ID, MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME, MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATA, MediaStore.Images.Media.DATA,
MediaStore.Images.Media.BUCKET_ID, MediaStore.Images.Media.BUCKET_ID,
MediaStore.Images.Media.BUCKET_DISPLAY_NAME, MediaStore.Images.Media.BUCKET_DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED MediaStore.Images.Media.DATE_ADDED,
) )
/** /**
@ -50,12 +52,18 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
} }
} }
/** /**
* Load Device images using cursor * Load Device images using cursor
*/ */
private fun getImages(listener:ImageLoaderListener) { private fun getImages(listener: ImageLoaderListener) {
val cursor = context.contentResolver.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, null, null, MediaStore.Images.Media.DATE_ADDED + " DESC") val cursor =
context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
null,
null,
MediaStore.Images.Media.DATE_ADDED + " DESC",
)
if (cursor == null) { if (cursor == null) {
listener.onFailed(NullPointerException()) listener.onFailed(NullPointerException())
return return
@ -85,11 +93,13 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
val file = val file =
if (path == null || path.isEmpty()) { if (path == null || path.isEmpty()) {
null null
} else try { } else {
try {
File(path) File(path)
} catch (ignored: Exception) { } catch (ignored: Exception) {
null null
} }
}
if (file != null && file.exists() && name != null && path != null && bucketName != null) { if (file != null && file.exists() && name != null && path != null && bucketName != null) {
val extension = path.substringAfterLast(".", "") val extension = path.substringAfterLast(".", "")
@ -106,30 +116,29 @@ class ImageFileLoader(val context: Context) : CoroutineScope{
val dateFormat = DateFormat.getMediumDateFormat(context) val dateFormat = DateFormat.getMediumDateFormat(context)
val formattedDate = dateFormat.format(date) val formattedDate = dateFormat.format(date)
val image = Image( val image =
Image(
id, id,
name, name,
uri, uri,
path, path,
bucketId, bucketId,
bucketName, bucketName,
date = (formattedDate) date = (formattedDate),
) )
images.add(image) images.add(image)
} }
} while (cursor.moveToNext()) } while (cursor.moveToNext())
} }
cursor.close() cursor.close()
listener.onImageLoaded(images) listener.onImageLoaded(images)
} }
/** /**
* Abort loading images. * Abort loading images.
*/ */
fun abortLoadImage(){ fun abortLoadImage() {
//todo Abort loading images. // todo Abort loading images.
} }
/* /*

View file

@ -44,8 +44,10 @@ import kotlin.collections.ArrayList
/** /**
* Custom Selector Image Fragment. * Custom Selector Image Fragment.
*/ */
class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDataListener { class ImageFragment :
CommonsDaggerSupportFragment(),
RefreshUIListener,
PassDataListener {
private var _binding: FragmentCustomSelectorBinding? = null private var _binding: FragmentCustomSelectorBinding? = null
private val binding get() = _binding private val binding get() = _binding
@ -107,7 +109,6 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
private lateinit var progressDialog: AlertDialog private lateinit var progressDialog: AlertDialog
private lateinit var progressDialogLayout: ProgressDialogBinding private lateinit var progressDialogLayout: ProgressDialogBinding
/** /**
* NotForUploadStatus Dao class for database operations * NotForUploadStatus Dao class for database operations
*/ */
@ -142,7 +143,6 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
lateinit var contributionDao: ContributionDao lateinit var contributionDao: ContributionDao
companion object { companion object {
/** /**
* Switch state * Switch state
*/ */
@ -157,7 +157,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
/** /**
* newInstance from bucketId. * newInstance from bucketId.
*/ */
fun newInstance(bucketId: Long, lastItemId: Long): ImageFragment { fun newInstance(
bucketId: Long,
lastItemId: Long,
): ImageFragment {
val fragment = ImageFragment() val fragment = ImageFragment()
val args = Bundle() val args = Bundle()
args.putLong(BUCKET_ID, bucketId) args.putLong(BUCKET_ID, bucketId)
@ -175,8 +178,9 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
bucketId = arguments?.getLong(BUCKET_ID) bucketId = arguments?.getLong(BUCKET_ID)
lastItemId = arguments?.getLong(LAST_ITEM_ID, 0) lastItemId = arguments?.getLong(LAST_ITEM_ID, 0)
viewModel = ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get( viewModel =
CustomSelectorViewModel::class.java ViewModelProvider(requireActivity(), customSelectorViewModelFactory).get(
CustomSelectorViewModel::class.java,
) )
} }
@ -188,7 +192,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
override fun onCreateView( override fun onCreateView(
inflater: LayoutInflater, inflater: LayoutInflater,
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?,
): View? { ): View? {
_binding = FragmentCustomSelectorBinding.inflate(inflater, container, false) _binding = FragmentCustomSelectorBinding.inflate(inflater, container, false)
imageAdapter = imageAdapter =
@ -200,9 +204,12 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
this?.adapter = imageAdapter this?.adapter = imageAdapter
} }
viewModel?.result?.observe(viewLifecycleOwner, Observer { viewModel?.result?.observe(
viewLifecycleOwner,
Observer {
handleResult(it) handleResult(it)
}) },
)
switch = binding?.switchWidget switch = binding?.switchWidget
switch?.visibility = View.VISIBLE switch?.visibility = View.VISIBLE
@ -323,15 +330,17 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
override fun onDestroy() { override fun onDestroy() {
imageAdapter.cleanUp() imageAdapter.cleanUp()
val position = (selectorRV?.layoutManager as GridLayoutManager) val position =
(selectorRV?.layoutManager as GridLayoutManager)
.findFirstVisibleItemPosition() .findFirstVisibleItemPosition()
// Check for empty RecyclerView. // Check for empty RecyclerView.
if (position != -1 && filteredImages.size > 0) { if (position != -1 && filteredImages.size > 0) {
context?.let { context -> context?.let { context ->
context.getSharedPreferences( context
.getSharedPreferences(
"CustomSelector", "CustomSelector",
BaseActivity.MODE_PRIVATE BaseActivity.MODE_PRIVATE,
)?.let { prefs -> )?.let { prefs ->
prefs.edit()?.let { editor -> prefs.edit()?.let { editor ->
editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply() editor.putLong("ItemId", imageAdapter.getImageIdAt(position))?.apply()
@ -354,7 +363,7 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
/** /**
* Removes the image from the actionable image map * Removes the image from the actionable image map
*/ */
fun removeImage(image : Image){ fun removeImage(image: Image) {
imageAdapter.removeImageFromActionableImageMap(image) imageAdapter.removeImageFromActionableImageMap(image)
} }
@ -364,11 +373,15 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
fun clearSelectedImages() { fun clearSelectedImages() {
imageAdapter.clearSelectedImages() imageAdapter.clearSelectedImages()
} }
/** /**
* Passes selected images and other information from Activity to Fragment and connects it with * Passes selected images and other information from Activity to Fragment and connects it with
* the adapter * the adapter
*/ */
override fun passSelectedImages(selectedImages: ArrayList<Image>, shouldRefresh: Boolean) { override fun passSelectedImages(
selectedImages: ArrayList<Image>,
shouldRefresh: Boolean,
) {
imageAdapter.setSelectedImages(selectedImages) imageAdapter.setSelectedImages(selectedImages)
val uploadingContributions = getUploadingContributions() val uploadingContributions = getUploadingContributions()
@ -398,11 +411,10 @@ class ImageFragment : CommonsDaggerSupportFragment(), RefreshUIListener, PassDat
} }
} }
private fun getUploadingContributions(): List<Contribution> { private fun getUploadingContributions(): List<Contribution> =
contributionDao
return contributionDao.getContribution( .getContribution(
listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED) listOf(Contribution.STATE_IN_PROGRESS, Contribution.STATE_FAILED, Contribution.STATE_QUEUED, Contribution.STATE_PAUSED),
)?.subscribeOn(Schedulers.io())?.blockingGet() ?: emptyList() )?.subscribeOn(Schedulers.io())
} ?.blockingGet() ?: emptyList()
} }

View file

@ -23,51 +23,46 @@ import javax.inject.Inject
/** /**
* Image Loader class, loads images, depending on API results. * Image Loader class, loads images, depending on API results.
*/ */
class ImageLoader @Inject constructor( class ImageLoader
@Inject
constructor(
/** /**
* MediaClient for SHA1 query. * MediaClient for SHA1 query.
*/ */
var mediaClient: MediaClient, var mediaClient: MediaClient,
/** /**
* FileProcessor to pre-process the file. * FileProcessor to pre-process the file.
*/ */
var fileProcessor: FileProcessor, var fileProcessor: FileProcessor,
/** /**
* File Utils Wrapper for SHA1 * File Utils Wrapper for SHA1
*/ */
var fileUtilsWrapper: FileUtilsWrapper, var fileUtilsWrapper: FileUtilsWrapper,
/** /**
* UploadedStatusDao for cache query. * UploadedStatusDao for cache query.
*/ */
var uploadedStatusDao: UploadedStatusDao, var uploadedStatusDao: UploadedStatusDao,
/** /**
* NotForUploadDao for database operations * NotForUploadDao for database operations
*/ */
var notForUploadStatusDao: NotForUploadStatusDao, var notForUploadStatusDao: NotForUploadStatusDao,
/** /**
* Context for coroutine. * Context for coroutine.
*/ */
val context: Context val context: Context,
) { ) {
/** /**
* Maps to facilitate image query. * Maps to facilitate image query.
*/ */
private var mapModifiedImageSHA1: HashMap<Image, String> = HashMap() private var mapModifiedImageSHA1: HashMap<Image, String> = HashMap()
private var mapHolderImage : HashMap<ImageViewHolder, Image> = HashMap() private var mapHolderImage: HashMap<ImageViewHolder, Image> = HashMap()
private var mapResult: HashMap<String, Result> = HashMap() private var mapResult: HashMap<String, Result> = HashMap()
private var mapImageSHA1: HashMap<Uri, String> = HashMap() private var mapImageSHA1: HashMap<Uri, String> = HashMap()
/** /**
* Coroutine Scope. * Coroutine Scope.
*/ */
private val scope : CoroutineScope = MainScope() private val scope: CoroutineScope = MainScope()
/** /**
* Query image and setUp the view. * Query image and setUp the view.
@ -77,9 +72,8 @@ class ImageLoader @Inject constructor(
image: Image, image: Image,
ioDispatcher: CoroutineDispatcher, ioDispatcher: CoroutineDispatcher,
defaultDispatcher: CoroutineDispatcher, defaultDispatcher: CoroutineDispatcher,
uploadedContributionsList : List<Contribution> uploadedContributionsList: List<Contribution>,
) { ) {
/** /**
* Recycler view uses same view holder, so we can identify the latest query image from holder. * Recycler view uses same view holder, so we can identify the latest query image from holder.
*/ */
@ -95,13 +89,15 @@ class ImageLoader @Inject constructor(
return@launch return@launch
} }
val imageSHA1: String = when (mapImageSHA1[image.uri] != null) { val imageSHA1: String =
when (mapImageSHA1[image.uri] != null) {
true -> mapImageSHA1[image.uri]!! true -> mapImageSHA1[image.uri]!!
else -> CustomSelectorUtils.getImageSHA1( else ->
CustomSelectorUtils.getImageSHA1(
image.uri, image.uri,
ioDispatcher, ioDispatcher,
fileUtilsWrapper, fileUtilsWrapper,
context.contentResolver context.contentResolver,
) )
} }
mapImageSHA1[image.uri] = imageSHA1 mapImageSHA1[image.uri] = imageSHA1
@ -111,7 +107,8 @@ class ImageLoader @Inject constructor(
} }
val uploadedStatus = getFromUploaded(imageSHA1) val uploadedStatus = getFromUploaded(imageSHA1)
val sha1 = uploadedStatus?.let { val sha1 =
uploadedStatus?.let {
result = getResultFromUploadedStatus(uploadedStatus) result = getResultFromUploadedStatus(uploadedStatus)
uploadedStatus.modifiedImageSHA1 uploadedStatus.modifiedImageSHA1
} ?: run { } ?: run {
@ -132,10 +129,11 @@ class ImageLoader @Inject constructor(
when { when {
mapResult[imageSHA1] == null -> { mapResult[imageSHA1] == null -> {
// Query original image. // Query original image.
result = checkWhetherFileExistsOnCommonsUsingSHA1( result =
checkWhetherFileExistsOnCommonsUsingSHA1(
imageSHA1, imageSHA1,
ioDispatcher, ioDispatcher,
mediaClient mediaClient,
) )
when (result) { when (result) {
is Result.TRUE -> { is Result.TRUE -> {
@ -166,10 +164,11 @@ class ImageLoader @Inject constructor(
when { when {
mapResult[sha1] == null -> { mapResult[sha1] == null -> {
// Original image not found, query modified image. // Original image not found, query modified image.
result = checkWhetherFileExistsOnCommonsUsingSHA1( result =
checkWhetherFileExistsOnCommonsUsingSHA1(
sha1, sha1,
ioDispatcher, ioDispatcher,
mediaClient mediaClient,
) )
when (result) { when (result) {
is Result.TRUE -> { is Result.TRUE -> {
@ -205,21 +204,25 @@ class ImageLoader @Inject constructor(
val showAlreadyActionedImages = val showAlreadyActionedImages =
sharedPreferences.getBoolean( sharedPreferences.getBoolean(
ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY, ImageHelper.SHOW_ALREADY_ACTIONED_IMAGES_PREFERENCE_KEY,
true true,
) )
if (mapHolderImage[holder] == image) { if (mapHolderImage[holder] == image) {
if ((result is Result.TRUE) && showAlreadyActionedImages) { if ((result is Result.TRUE) && showAlreadyActionedImages) {
holder.itemUploaded() holder.itemUploaded()
} else holder.itemNotUploaded() } else {
holder.itemNotUploaded()
}
if ((existsInNotForUploadTable > 0) && showAlreadyActionedImages) { if ((existsInNotForUploadTable > 0) && showAlreadyActionedImages) {
holder.itemNotForUpload() holder.itemNotForUpload()
} else holder.itemForUpload() } else {
holder.itemForUpload()
}
} }
if (uploadedContributionsList.isNotEmpty()) { if (uploadedContributionsList.isNotEmpty()) {
for (contribution in uploadedContributionsList ) { for (contribution in uploadedContributionsList) {
if (contribution.contentUri == image.uri && showAlreadyActionedImages) { if (contribution.contentUri == image.uri && showAlreadyActionedImages) {
holder.itemUploading() holder.itemUploading()
break break
@ -235,34 +238,37 @@ class ImageLoader @Inject constructor(
* Finds out the next actionable image position * Finds out the next actionable image position
*/ */
suspend fun nextActionableImage( suspend fun nextActionableImage(
allImages: List<Image>, ioDispatcher: CoroutineDispatcher, allImages: List<Image>,
ioDispatcher: CoroutineDispatcher,
defaultDispatcher: CoroutineDispatcher, defaultDispatcher: CoroutineDispatcher,
nextImagePosition: Int, nextImagePosition: Int,
currentlyUploadingImages: List<Contribution> currentlyUploadingImages: List<Contribution>,
): Int { ): Int {
var next: Int var next: Int
// Traversing from given position to the end // Traversing from given position to the end
for (i in nextImagePosition until allImages.size){ for (i in nextImagePosition until allImages.size) {
val currentImage = allImages[i] val currentImage = allImages[i]
if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) { if (currentlyUploadingImages.any { it.contentUri == currentImage.uri }) {
continue // Skip this image as it's currently being uploaded continue // Skip this image as it's currently being uploaded
} }
val imageSHA1: String = when (mapImageSHA1[currentImage.uri] != null) { val imageSHA1: String =
when (mapImageSHA1[currentImage.uri] != null) {
true -> mapImageSHA1[currentImage.uri]!! true -> mapImageSHA1[currentImage.uri]!!
else -> CustomSelectorUtils.getImageSHA1( else ->
CustomSelectorUtils.getImageSHA1(
currentImage.uri, currentImage.uri,
ioDispatcher, ioDispatcher,
fileUtilsWrapper, fileUtilsWrapper,
context.contentResolver context.contentResolver,
) )
} }
next = notForUploadStatusDao.find(imageSHA1) next = notForUploadStatusDao.find(imageSHA1)
// After checking the image in the not for upload table, if the image is present then // After checking the image in the not for upload table, if the image is present then
// skips the image and moves to next image for checking // skips the image and moves to next image for checking
if(next > 0){ if (next > 0) {
continue continue
// Otherwise checks in already uploaded table // Otherwise checks in already uploaded table
@ -273,9 +279,10 @@ class ImageLoader @Inject constructor(
// modified SHA1 in already uploaded table // modified SHA1 in already uploaded table
if (next <= 0) { if (next <= 0) {
val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher) val modifiedImageSha1 = getSHA1(currentImage, defaultDispatcher)
next = uploadedStatusDao.findByModifiedImageSHA1( next =
uploadedStatusDao.findByModifiedImageSHA1(
modifiedImageSha1, modifiedImageSha1,
true true,
) )
// If the modified image SHA1 is not present in the already uploaded table, // If the modified image SHA1 is not present in the already uploaded table,
@ -304,39 +311,47 @@ class ImageLoader @Inject constructor(
* *
* @return sha1 of the image * @return sha1 of the image
*/ */
suspend fun getSHA1(image: Image, defaultDispatcher: CoroutineDispatcher): String { suspend fun getSHA1(
mapModifiedImageSHA1[image]?.let{ image: Image,
defaultDispatcher: CoroutineDispatcher,
): String {
mapModifiedImageSHA1[image]?.let {
return it return it
} }
val sha1 = CustomSelectorUtils val sha1 =
.generateModifiedSHA1(image, CustomSelectorUtils
.generateModifiedSHA1(
image,
defaultDispatcher, defaultDispatcher,
context, context,
fileProcessor, fileProcessor,
fileUtilsWrapper fileUtilsWrapper,
) )
mapModifiedImageSHA1[image] = sha1; mapModifiedImageSHA1[image] = sha1
return sha1; return sha1
} }
/** /**
* Get the uploaded status entry from the database. * Get the uploaded status entry from the database.
*/ */
suspend fun getFromUploaded(imageSha1:String): UploadedStatus? { suspend fun getFromUploaded(imageSha1: String): UploadedStatus? = uploadedStatusDao.getUploadedFromImageSHA1(imageSha1)
return uploadedStatusDao.getUploadedFromImageSHA1(imageSha1)
}
/** /**
* Insert into uploaded status table. * Insert into uploaded status table.
*/ */
suspend fun insertIntoUploaded(imageSha1:String, modifiedImageSha1:String, imageResult:Boolean, modifiedImageResult: Boolean){ suspend fun insertIntoUploaded(
imageSha1: String,
modifiedImageSha1: String,
imageResult: Boolean,
modifiedImageResult: Boolean,
) {
uploadedStatusDao.insertUploaded( uploadedStatusDao.insertUploaded(
UploadedStatus( UploadedStatus(
imageSha1, imageSha1,
modifiedImageSha1, modifiedImageSha1,
imageResult, imageResult,
modifiedImageResult modifiedImageResult,
) ),
) )
} }
@ -362,9 +377,13 @@ class ImageLoader @Inject constructor(
*/ */
sealed class Result { sealed class Result {
object TRUE : Result() object TRUE : Result()
object FALSE : Result() object FALSE : Result()
object INVALID : Result() object INVALID : Result()
object NOTFOUND : Result() object NOTFOUND : Result()
object ERROR : Result() object ERROR : Result()
} }
@ -378,5 +397,4 @@ class ImageLoader @Inject constructor(
*/ */
const val INVALIDATE_DAY_COUNT: Long = 7 const val INVALIDATE_DAY_COUNT: Long = 7
} }
}
}

View file

@ -17,13 +17,22 @@ import fr.free.nrw.commons.upload.depicts.DepictsDao
* The database for accessing the respective DAOs * The database for accessing the respective DAOs
* *
*/ */
@Database(entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class], version = 18, exportSchema = false) @Database(
entities = [Contribution::class, Depicts::class, UploadedStatus::class, NotForUploadStatus::class, ReviewEntity::class, Place::class],
version = 18,
exportSchema = false,
)
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun contributionDao(): ContributionDao abstract fun contributionDao(): ContributionDao
abstract fun PlaceDao(): PlaceDao abstract fun PlaceDao(): PlaceDao
abstract fun DepictsDao(): DepictsDao;
abstract fun UploadedStatusDao(): UploadedStatusDao; abstract fun DepictsDao(): DepictsDao
abstract fun UploadedStatusDao(): UploadedStatusDao
abstract fun NotForUploadStatusDao(): NotForUploadStatusDao abstract fun NotForUploadStatusDao(): NotForUploadStatusDao
abstract fun ReviewDao(): ReviewDao abstract fun ReviewDao(): ReviewDao
} }

View file

@ -1,6 +1,5 @@
package fr.free.nrw.commons.description package fr.free.nrw.commons.description
import android.app.ProgressDialog import android.app.ProgressDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
@ -29,11 +28,12 @@ import io.reactivex.schedulers.Schedulers
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
/** /**
* Activity for populating and editing existing description and caption * Activity for populating and editing existing description and caption
*/ */
class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventListener { class DescriptionEditActivity :
BaseActivity(),
UploadMediaDetailAdapter.EventListener {
/** /**
* Adapter for showing UploadMediaDetail in the activity * Adapter for showing UploadMediaDetail in the activity
*/ */
@ -78,7 +78,6 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
@Inject lateinit var sessionManager: SessionManager @Inject lateinit var sessionManager: SessionManager
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -110,12 +109,17 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
* @param descriptionAndCaptions list of description and caption * @param descriptionAndCaptions list of description and caption
*/ */
private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) { private fun initRecyclerView(descriptionAndCaptions: ArrayList<UploadMediaDetail>?) {
uploadMediaDetailAdapter = UploadMediaDetailAdapter(this, uploadMediaDetailAdapter =
savedLanguageValue, descriptionAndCaptions, recentLanguagesDao) UploadMediaDetailAdapter(
this,
savedLanguageValue,
descriptionAndCaptions,
recentLanguagesDao,
)
uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int -> uploadMediaDetailAdapter.setCallback { titleStringID: Int, messageStringId: Int ->
showInfoAlert( showInfoAlert(
titleStringID, titleStringID,
messageStringId messageStringId,
) )
} }
uploadMediaDetailAdapter.setEventListener(this) uploadMediaDetailAdapter.setEventListener(this)
@ -129,11 +133,17 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
* @param titleStringID Title ID * @param titleStringID Title ID
* @param messageStringId Message ID * @param messageStringId Message ID
*/ */
private fun showInfoAlert(titleStringID: Int, messageStringId: Int) { private fun showInfoAlert(
titleStringID: Int,
messageStringId: Int,
) {
showAlertDialog( showAlertDialog(
this, getString(titleStringID), this,
getString(messageStringId), getString(android.R.string.ok), getString(titleStringID),
null, true getString(messageStringId),
getString(android.R.string.ok),
null,
true,
) )
} }
@ -144,7 +154,7 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
*/ */
override fun addLanguage() { override fun addLanguage() {
val uploadMediaDetail = UploadMediaDetail() val uploadMediaDetail = UploadMediaDetail()
uploadMediaDetail.isManuallyAdded = true //This was manually added by the user uploadMediaDetail.isManuallyAdded = true // This was manually added by the user
uploadMediaDetailAdapter.addDescription(uploadMediaDetail) uploadMediaDetailAdapter.addDescription(uploadMediaDetail)
rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1) rvDescriptions!!.smoothScrollToPosition(uploadMediaDetailAdapter.itemCount - 1)
} }
@ -174,9 +184,10 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
val descriptionStart = wikiText!!.substring(0, descriptionIndex + 12) val descriptionStart = wikiText!!.substring(0, descriptionIndex + 12)
val descriptionToEnd = wikiText!!.substring(descriptionIndex + 12) val descriptionToEnd = wikiText!!.substring(descriptionIndex + 12)
val descriptionEndIndex = descriptionToEnd.indexOf("\n") val descriptionEndIndex = descriptionToEnd.indexOf("\n")
val descriptionEnd = wikiText!!.substring( val descriptionEnd =
descriptionStart.length wikiText!!.substring(
+ descriptionEndIndex descriptionStart.length +
descriptionEndIndex,
) )
buffer.append(descriptionStart) buffer.append(descriptionStart)
for (i in uploadMediaDetails.indices) { for (i in uploadMediaDetails.indices) {
@ -203,65 +214,72 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
* @param updatedWikiText updated wiki text * @param updatedWikiText updated wiki text
* @param uploadMediaDetails descriptions and captions * @param uploadMediaDetails descriptions and captions
*/ */
private fun editDescription(media : Media, updatedWikiText : String, uploadMediaDetails : ArrayList<UploadMediaDetail>){ private fun editDescription(
media: Media,
updatedWikiText: String,
uploadMediaDetails: ArrayList<UploadMediaDetail>,
) {
try { try {
descriptionEditHelper?.addDescription( descriptionEditHelper
applicationContext, media, ?.addDescription(
updatedWikiText applicationContext,
) media,
?.subscribeOn(Schedulers.io()) updatedWikiText,
)?.subscribeOn(Schedulers.io())
?.observeOn(AndroidSchedulers.mainThread()) ?.observeOn(AndroidSchedulers.mainThread())
?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })?.let { ?.subscribe(Consumer<Boolean> { s: Boolean? -> Timber.d("Descriptions are added.") })
?.let {
compositeDisposable.add( compositeDisposable.add(
it it,
) )
} }
} catch (e : InvalidLoginTokenException) { } catch (e: InvalidLoginTokenException) {
val username: String? = sessionManager?.userName val username: String? = sessionManager?.userName
val logoutListener = CommonsApplication.BaseLogoutListener( val logoutListener =
CommonsApplication.BaseLogoutListener(
this, this,
getString(R.string.invalid_login_message), getString(R.string.invalid_login_message),
username username,
) )
val commonsApplication = CommonsApplication.getInstance() val commonsApplication = CommonsApplication.getInstance()
if (commonsApplication != null ){ if (commonsApplication != null) {
commonsApplication.clearApplicationData(this,logoutListener) commonsApplication.clearApplicationData(this, logoutListener)
} }
} }
val updatedCaptions = LinkedHashMap<String, String>() val updatedCaptions = LinkedHashMap<String, String>()
for (mediaDetail in uploadMediaDetails) { for (mediaDetail in uploadMediaDetails) {
try { try {
compositeDisposable.add( compositeDisposable.add(
descriptionEditHelper!!.addCaption( descriptionEditHelper!!
applicationContext, media, .addCaption(
mediaDetail.languageCode, mediaDetail.captionText applicationContext,
) media,
.subscribeOn(Schedulers.io()) mediaDetail.languageCode,
mediaDetail.captionText,
).subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe { s: Boolean? -> .subscribe { s: Boolean? ->
updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText updatedCaptions[mediaDetail.languageCode!!] = mediaDetail.captionText
media.captions = updatedCaptions media.captions = updatedCaptions
Timber.d("Caption is added.") Timber.d("Caption is added.")
}) },
} )
catch (e : InvalidLoginTokenException) { } catch (e: InvalidLoginTokenException) {
val username = sessionManager.userName val username = sessionManager.userName
val logoutListener = CommonsApplication.BaseLogoutListener( val logoutListener =
CommonsApplication.BaseLogoutListener(
this, this,
getString(R.string.invalid_login_message), getString(R.string.invalid_login_message),
username username,
) )
val commonsApplication = CommonsApplication.getInstance() val commonsApplication = CommonsApplication.getInstance()
if (commonsApplication != null ){ if (commonsApplication != null) {
commonsApplication.clearApplicationData(this,logoutListener) commonsApplication.clearApplicationData(this, logoutListener)
} }
} }
} }
} }
@ -274,23 +292,29 @@ class DescriptionEditActivity : BaseActivity(), UploadMediaDetailAdapter.EventLi
progressDialog!!.show() progressDialog!!.show()
} }
override override fun onActivityResult(
fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { requestCode: Int,
super.onActivityResult(requestCode, resultCode, data); resultCode: Int,
data: Intent?,
) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) { if (requestCode == REQUEST_CODE_FOR_VOICE_INPUT) {
if (resultCode == RESULT_OK && data != null) { if (resultCode == RESULT_OK && data != null) {
val result = data.getStringArrayListExtra( RecognizerIntent.EXTRA_RESULTS ) val result = data.getStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS)
uploadMediaDetailAdapter.handleSpeechResult(result!![0]) } uploadMediaDetailAdapter.handleSpeechResult(result!![0])
else { Timber.e("Error %s", resultCode) } } else {
Timber.e("Error %s", resultCode)
} }
} }
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>) outState.putParcelableArrayList(LIST_OF_DESCRIPTION_AND_CAPTION, uploadMediaDetailAdapter.items as ArrayList<out Parcelable?>)
outState.putString(WIKITEXT, wikiText) outState.putString(WIKITEXT, wikiText)
outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue) outState.putString(Prefs.DESCRIPTION_LANGUAGE, savedLanguageValue)
//save Media // save Media
outState.putParcelable("media", media) outState.putParcelable("media", media)
} }
} }

View file

@ -6,5 +6,5 @@ package fr.free.nrw.commons.description
object EditDescriptionConstants { object EditDescriptionConstants {
const val LIST_OF_DESCRIPTION_AND_CAPTION = "description.descriptionAndCaption" const val LIST_OF_DESCRIPTION_AND_CAPTION = "description.descriptionAndCaption"
const val WIKITEXT = "description.wikiText" const val WIKITEXT = "description.wikiText"
const val UPDATED_WIKITEXT = "description.updatedWikiText"; const val UPDATED_WIKITEXT = "description.updatedWikiText"
} }

View file

@ -6,8 +6,7 @@ import dagger.Provides
import fr.free.nrw.commons.explore.map.ExploreMapFragment import fr.free.nrw.commons.explore.map.ExploreMapFragment
@Module @Module
class ExploreMapFragmentModule{ class ExploreMapFragmentModule {
@Provides @Provides
fun ExploreMapFragment.providesActivity(): Activity = activity!! fun ExploreMapFragment.providesActivity(): Activity = activity!!
} }

View file

@ -6,8 +6,7 @@ import dagger.Provides
import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment import fr.free.nrw.commons.nearby.fragments.NearbyParentFragment
@Module @Module
class NearbyParentFragmentModule{ class NearbyParentFragmentModule {
@Provides @Provides
fun NearbyParentFragment.providesActivity(): Activity = activity!! fun NearbyParentFragment.providesActivity(): Activity = activity!!
} }

View file

@ -44,7 +44,8 @@ class EditActivity : AppCompatActivity() {
imageUri = intent.getStringExtra("image") ?: "" imageUri = intent.getStringExtra("image") ?: ""
vm = ViewModelProvider(this).get(EditViewModel::class.java) vm = ViewModelProvider(this).get(EditViewModel::class.java)
val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) } val sourceExif = imageUri.toUri().path?.let { ExifInterface(it) }
val exifTags = arrayOf( val exifTags =
arrayOf(
ExifInterface.TAG_APERTURE, ExifInterface.TAG_APERTURE,
ExifInterface.TAG_DATETIME, ExifInterface.TAG_DATETIME,
ExifInterface.TAG_EXPOSURE_TIME, ExifInterface.TAG_EXPOSURE_TIME,
@ -67,7 +68,7 @@ class EditActivity : AppCompatActivity() {
ExifInterface.TAG_ORIENTATION, ExifInterface.TAG_ORIENTATION,
ExifInterface.TAG_WHITE_BALANCE, ExifInterface.TAG_WHITE_BALANCE,
ExifInterface.WHITEBALANCE_AUTO, ExifInterface.WHITEBALANCE_AUTO,
ExifInterface.WHITEBALANCE_MANUAL ExifInterface.WHITEBALANCE_MANUAL,
) )
for (tag in exifTags) { for (tag in exifTags) {
val attribute = sourceExif?.getAttribute(tag.toString()) val attribute = sourceExif?.getAttribute(tag.toString())
@ -87,7 +88,8 @@ class EditActivity : AppCompatActivity() {
private fun init() { private fun init() {
binding.iv.adjustViewBounds = true binding.iv.adjustViewBounds = true
binding.iv.scaleType = ImageView.ScaleType.MATRIX binding.iv.scaleType = ImageView.ScaleType.MATRIX
binding.iv.post(Runnable { binding.iv.post(
Runnable {
val options = BitmapFactory.Options() val options = BitmapFactory.Options()
options.inJustDecodeBounds = true options.inJustDecodeBounds = true
BitmapFactory.decodeFile(imageUri, options) BitmapFactory.decodeFile(imageUri, options)
@ -108,7 +110,6 @@ class EditActivity : AppCompatActivity() {
binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt() binding.iv.layoutParams.height = (scale * scaledBitmap.height).toInt()
binding.iv.imageMatrix = scaleMatrix(scale, scale) binding.iv.imageMatrix = scaleMatrix(scale, scale)
} else { } else {
options.inJustDecodeBounds = false options.inJustDecodeBounds = false
val bitmap = BitmapFactory.decodeFile(imageUri, options) val bitmap = BitmapFactory.decodeFile(imageUri, options)
binding.iv.setImageBitmap(bitmap) binding.iv.setImageBitmap(bitmap)
@ -117,7 +118,8 @@ class EditActivity : AppCompatActivity() {
binding.iv.layoutParams.height = (scale * bitmapHeight).toInt() binding.iv.layoutParams.height = (scale * bitmapHeight).toInt()
binding.iv.imageMatrix = scaleMatrix(scale, scale) binding.iv.imageMatrix = scaleMatrix(scale, scale)
} }
}) },
)
binding.rotateBtn.setOnClickListener { binding.rotateBtn.setOnClickListener {
animateImageHeight() animateImageHeight()
} }
@ -138,8 +140,16 @@ class EditActivity : AppCompatActivity() {
* further rotation actions. * further rotation actions.
*/ */
private fun animateImageHeight() { private fun animateImageHeight() {
val drawableWidth: Float = binding.iv.getDrawable().getIntrinsicWidth().toFloat() val drawableWidth: Float =
val drawableHeight: Float = binding.iv.getDrawable().getIntrinsicHeight().toFloat() binding.iv
.getDrawable()
.getIntrinsicWidth()
.toFloat()
val drawableHeight: Float =
binding.iv
.getDrawable()
.getIntrinsicHeight()
.toFloat()
val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat() val viewWidth: Float = binding.iv.getMeasuredWidth().toFloat()
val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat() val viewHeight: Float = binding.iv.getMeasuredHeight().toFloat()
val rotation = imageRotation % 360 val rotation = imageRotation % 360
@ -152,7 +162,6 @@ class EditActivity : AppCompatActivity() {
Timber.d("Rotation $rotation") Timber.d("Rotation $rotation")
Timber.d("new Rotation $newRotation") Timber.d("new Rotation $newRotation")
if (rotation == 0 || rotation == 180) { if (rotation == 0 || rotation == 180) {
imageScale = viewWidth / drawableWidth imageScale = viewWidth / drawableWidth
newImageScale = viewWidth / drawableHeight newImageScale = viewWidth / drawableHeight
@ -169,7 +178,8 @@ class EditActivity : AppCompatActivity() {
animator.interpolator = AccelerateDecelerateInterpolator() animator.interpolator = AccelerateDecelerateInterpolator()
animator.addListener(object : AnimatorListener { animator.addListener(
object : AnimatorListener {
override fun onAnimationStart(animation: Animator) { override fun onAnimationStart(animation: Animator) {
binding.rotateBtn.setEnabled(false) binding.rotateBtn.setEnabled(false)
} }
@ -184,8 +194,8 @@ class EditActivity : AppCompatActivity() {
override fun onAnimationRepeat(animation: Animator) { override fun onAnimationRepeat(animation: Animator) {
} }
},
}) )
animator.addUpdateListener { animation -> animator.addUpdateListener { animation ->
val animVal = animation.animatedValue as Float val animVal = animation.animatedValue as Float
@ -195,20 +205,21 @@ class EditActivity : AppCompatActivity() {
val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale val animatedScale = complementaryAnimVal * imageScale + animVal * newImageScale
val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation val animatedRotation = complementaryAnimVal * rotation + animVal * newRotation
binding.iv.getLayoutParams().height = animatedHeight binding.iv.getLayoutParams().height = animatedHeight
val matrix: Matrix = rotationMatrix( val matrix: Matrix =
rotationMatrix(
animatedRotation, animatedRotation,
drawableWidth / 2, drawableWidth / 2,
drawableHeight / 2 drawableHeight / 2,
) )
matrix.postScale( matrix.postScale(
animatedScale, animatedScale,
animatedScale, animatedScale,
drawableWidth / 2, drawableWidth / 2,
drawableHeight / 2 drawableHeight / 2,
) )
matrix.postTranslate( matrix.postTranslate(
-(drawableWidth - binding.iv.getMeasuredWidth()) / 2, -(drawableWidth - binding.iv.getMeasuredWidth()) / 2,
-(drawableHeight - binding.iv.getMeasuredHeight()) / 2 -(drawableHeight - binding.iv.getMeasuredHeight()) / 2,
) )
binding.iv.setImageMatrix(matrix) binding.iv.setImageMatrix(matrix)
binding.iv.requestLayout() binding.iv.requestLayout()
@ -228,11 +239,9 @@ class EditActivity : AppCompatActivity() {
* as a result, and finishes the current activity. * as a result, and finishes the current activity.
*/ */
fun getRotatedImage() { fun getRotatedImage() {
val filePath = imageUri.toUri().path val filePath = imageUri.toUri().path
val file = filePath?.let { File(it) } val file = filePath?.let { File(it) }
val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) } val rotatedImage = file?.let { vm.rotateImage(imageRotation, it) }
if (rotatedImage == null) { if (rotatedImage == null) {
Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show() Toast.makeText(this, "Failed to rotate to image", Toast.LENGTH_LONG).show()
@ -243,9 +252,9 @@ class EditActivity : AppCompatActivity() {
copyExifData(editedImageExif) copyExifData(editedImageExif)
} }
val resultIntent = Intent() val resultIntent = Intent()
resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error"); resultIntent.putExtra("editedImageFilePath", rotatedImage?.toUri()?.path ?: "Error")
setResult(RESULT_OK, resultIntent); setResult(RESULT_OK, resultIntent)
finish(); finish()
} }
/** /**
@ -257,7 +266,6 @@ class EditActivity : AppCompatActivity() {
* @param editedImageExif The ExifInterface object for the edited image. * @param editedImageExif The ExifInterface object for the edited image.
*/ */
private fun copyExifData(editedImageExif: ExifInterface?) { private fun copyExifData(editedImageExif: ExifInterface?) {
for (attr in sourceExifAttributeList) { for (attr in sourceExifAttributeList) {
Log.d("Tag is ${attr.first}", "Value is ${attr.second}") Log.d("Tag is ${attr.first}", "Value is ${attr.second}")
editedImageExif!!.setAttribute(attr.first, attr.second) editedImageExif!!.setAttribute(attr.first, attr.second)
@ -282,7 +290,11 @@ class EditActivity : AppCompatActivity() {
* The scale factor ensures that the scaled bitmap will fit within the maximum size * The scale factor ensures that the scaled bitmap will fit within the maximum size
* while maintaining aspect ratio. * while maintaining aspect ratio.
*/ */
private fun calculateScaleFactor(originalWidth: Int, originalHeight: Int, maxSize: Int): Int { private fun calculateScaleFactor(
originalWidth: Int,
originalHeight: Int,
maxSize: Int,
): Int {
var scaleFactor = 1 var scaleFactor = 1
if (originalWidth > maxSize || originalHeight > maxSize) { if (originalWidth > maxSize || originalHeight > maxSize) {
@ -295,7 +307,4 @@ class EditActivity : AppCompatActivity() {
return scaleFactor return scaleFactor
} }
} }

View file

@ -9,8 +9,7 @@ import java.io.File
* This ViewModel class is responsible for managing image editing operations, such as * This ViewModel class is responsible for managing image editing operations, such as
* rotating images. It utilizes a TransformImage implementation to perform image transformations. * rotating images. It utilizes a TransformImage implementation to perform image transformations.
*/ */
class EditViewModel() : ViewModel() { class EditViewModel : ViewModel() {
// Ideally should be injected using DI // Ideally should be injected using DI
private val transformImage: TransformImage = TransformImageImpl() private val transformImage: TransformImage = TransformImageImpl()
@ -21,7 +20,8 @@ class EditViewModel() : ViewModel() {
* @param imageFile The File representing the image to be rotated. * @param imageFile The File representing the image to be rotated.
* @return The rotated image File, or null if the rotation operation fails. * @return The rotated image File, or null if the rotation operation fails.
*/ */
fun rotateImage(degree: Int, imageFile: File): File? { fun rotateImage(
return transformImage.rotateImage(imageFile, degree) degree: Int,
} imageFile: File,
): File? = transformImage.rotateImage(imageFile, degree)
} }

View file

@ -9,7 +9,6 @@ import java.io.File
* implementations to provide specific functionality for tasks like rotating images. * implementations to provide specific functionality for tasks like rotating images.
*/ */
interface TransformImage { interface TransformImage {
/** /**
* Rotates the specified image file by the given degree. * Rotates the specified image file by the given degree.
* *
@ -17,5 +16,8 @@ interface TransformImage {
* @param degree The degree by which to rotate the image. * @param degree The degree by which to rotate the image.
* @return The rotated image File, or null if the rotation operation fails. * @return The rotated image File, or null if the rotation operation fails.
*/ */
fun rotateImage(imageFile: File, degree : Int ):File? fun rotateImage(
imageFile: File,
degree: Int,
): File?
} }

View file

@ -15,8 +15,7 @@ import java.io.FileOutputStream
* function for rotating images by a specified degree using the LLJTran library. Right now it reads * function for rotating images by a specified degree using the LLJTran library. Right now it reads
* the input image file, performs the rotation, and saves the rotated image to a new file. * the input image file, performs the rotation, and saves the rotated image to a new file.
*/ */
class TransformImageImpl() : TransformImage { class TransformImageImpl : TransformImage {
/** /**
* Rotates the specified image file by the given degree. * Rotates the specified image file by the given degree.
* *
@ -24,12 +23,15 @@ class TransformImageImpl() : TransformImage {
* @param degree The degree by which to rotate the image. * @param degree The degree by which to rotate the image.
* @return The rotated image File, or null if the rotation operation fails. * @return The rotated image File, or null if the rotation operation fails.
*/ */
override fun rotateImage(imageFile: File, degree : Int): File? { override fun rotateImage(
imageFile: File,
degree: Int,
): File? {
Timber.tag("Trying to rotate image").d("Starting") Timber.tag("Trying to rotate image").d("Starting")
val path = Environment.getExternalStoragePublicDirectory( val path =
Environment.DIRECTORY_DOWNLOADS Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS,
) )
val imagePath = System.currentTimeMillis() val imagePath = System.currentTimeMillis()
@ -37,14 +39,15 @@ class TransformImageImpl() : TransformImage {
val output = file val output = file
val rotated = try { val rotated =
try {
val lljTran = LLJTran(imageFile) val lljTran = LLJTran(imageFile)
lljTran.read( lljTran.read(
LLJTran.READ_ALL, LLJTran.READ_ALL,
false, false,
) // This could throw an LLJTranException. I am not catching it for now... Let's see. ) // This could throw an LLJTranException. I am not catching it for now... Let's see.
lljTran.transform( lljTran.transform(
when(degree){ when (degree) {
90 -> LLJTran.ROT_90 90 -> LLJTran.ROT_90
180 -> LLJTran.ROT_180 180 -> LLJTran.ROT_180
270 -> LLJTran.ROT_270 270 -> LLJTran.ROT_270
@ -52,10 +55,10 @@ class TransformImageImpl() : TransformImage {
LLJTran.ROT_90 LLJTran.ROT_90
} }
}, },
LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION LLJTran.OPT_DEFAULTS or LLJTran.OPT_XFORM_ORIENTATION,
) )
BufferedOutputStream(FileOutputStream(output)).use { writer -> BufferedOutputStream(FileOutputStream(output)).use { writer ->
lljTran.save(writer, LLJTran.OPT_WRITE_ALL ) lljTran.save(writer, LLJTran.OPT_WRITE_ALL)
} }
lljTran.freeMemory() lljTran.freeMemory()
true true

View file

@ -15,14 +15,11 @@ import fr.free.nrw.commons.explore.media.SearchMediaFragmentPresenterImpl
@Module @Module
abstract class SearchModule { abstract class SearchModule {
@Binds @Binds
abstract fun SearchDepictionsFragmentPresenterImpl.bindsSearchDepictionsFragmentPresenter() abstract fun SearchDepictionsFragmentPresenterImpl.bindsSearchDepictionsFragmentPresenter(): SearchDepictionsFragmentPresenter
: SearchDepictionsFragmentPresenter
@Binds @Binds
abstract fun SearchCategoriesFragmentPresenterImpl.bindsSearchCategoriesFragmentPresenter() abstract fun SearchCategoriesFragmentPresenterImpl.bindsSearchCategoriesFragmentPresenter(): SearchCategoriesFragmentPresenter
: SearchCategoriesFragmentPresenter
@Binds @Binds
abstract fun SearchMediaFragmentPresenterImpl.bindsSearchMediaFragmentPresenter() abstract fun SearchMediaFragmentPresenterImpl.bindsSearchMediaFragmentPresenter(): SearchMediaFragmentPresenter
: SearchMediaFragmentPresenter
} }

View file

@ -9,19 +9,14 @@ import fr.free.nrw.commons.explore.categories.parent.ParentCategoriesPresenterIm
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenter import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenter
import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenterImpl import fr.free.nrw.commons.explore.categories.sub.SubCategoriesPresenterImpl
@Module @Module
abstract class CategoriesModule { abstract class CategoriesModule {
@Binds
abstract fun CategoryMediaPresenterImpl.bindsCategoryMediaPresenter(): CategoryMediaPresenter
@Binds @Binds
abstract fun CategoryMediaPresenterImpl.bindsCategoryMediaPresenter() abstract fun SubCategoriesPresenterImpl.bindsSubCategoriesPresenter(): SubCategoriesPresenter
: CategoryMediaPresenter
@Binds @Binds
abstract fun SubCategoriesPresenterImpl.bindsSubCategoriesPresenter() abstract fun ParentCategoriesPresenterImpl.bindsParentCategoriesPresenter(): ParentCategoriesPresenter
: SubCategoriesPresenter
@Binds
abstract fun ParentCategoriesPresenterImpl.bindsParentCategoriesPresenter()
: ParentCategoriesPresenter
} }

View file

@ -4,7 +4,6 @@ import fr.free.nrw.commons.R
import fr.free.nrw.commons.category.CategoryDetailsActivity import fr.free.nrw.commons.category.CategoryDetailsActivity
import fr.free.nrw.commons.explore.paging.BasePagingFragment import fr.free.nrw.commons.explore.paging.BasePagingFragment
abstract class PageableCategoryFragment : BasePagingFragment<String>() { abstract class PageableCategoryFragment : BasePagingFragment<String>() {
override val errorTextId: Int = R.string.error_loading_categories override val errorTextId: Int = R.string.error_loading_categories
override val pagedListAdapter by lazy { override val pagedListAdapter by lazy {

View file

@ -8,31 +8,44 @@ import androidx.recyclerview.widget.RecyclerView
import fr.free.nrw.commons.category.CATEGORY_PREFIX import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.databinding.ItemRecentSearchesBinding import fr.free.nrw.commons.databinding.ItemRecentSearchesBinding
class PagedSearchCategoriesAdapter(private val onCategoryClicked: (String) -> Unit) : class PagedSearchCategoriesAdapter(
PagedListAdapter<String, CategoryItemViewHolder>(PagedSearchCategoriesDiffUtilCallback) { private val onCategoryClicked: (String) -> Unit,
) : PagedListAdapter<String, CategoryItemViewHolder>(PagedSearchCategoriesDiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = CategoryItemViewHolder( override fun onCreateViewHolder(
ItemRecentSearchesBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int,
) = CategoryItemViewHolder(
ItemRecentSearchesBinding.inflate(LayoutInflater.from(parent.context), parent, false),
) )
override fun onBindViewHolder(holder: CategoryItemViewHolder, position: Int) { override fun onBindViewHolder(
holder: CategoryItemViewHolder,
position: Int,
) {
holder.bind(getItem(position)!!, onCategoryClicked) holder.bind(getItem(position)!!, onCategoryClicked)
} }
} }
class CategoryItemViewHolder( class CategoryItemViewHolder(
private val binding: ItemRecentSearchesBinding private val binding: ItemRecentSearchesBinding,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: String, onCategoryClicked: (String) -> Unit) = with(binding) { fun bind(
item: String,
onCategoryClicked: (String) -> Unit,
) = with(binding) {
root.setOnClickListener { onCategoryClicked(item) } root.setOnClickListener { onCategoryClicked(item) }
textView1.text = item.substringAfter(CATEGORY_PREFIX) textView1.text = item.substringAfter(CATEGORY_PREFIX)
} }
} }
private object PagedSearchCategoriesDiffUtilCallback : DiffUtil.ItemCallback<String>() { private object PagedSearchCategoriesDiffUtilCallback : DiffUtil.ItemCallback<String>() {
override fun areItemsTheSame(oldItem: String, newItem: String) = override fun areItemsTheSame(
oldItem == newItem oldItem: String,
newItem: String,
) = oldItem == newItem
override fun areContentsTheSame(oldItem: String, newItem: String) = override fun areContentsTheSame(
oldItem == newItem oldItem: String,
newItem: String,
) = oldItem == newItem
} }

View file

@ -6,16 +6,17 @@ import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.explore.media.PageableMediaFragment import fr.free.nrw.commons.explore.media.PageableMediaFragment
import javax.inject.Inject import javax.inject.Inject
class CategoriesMediaFragment : PageableMediaFragment() { class CategoriesMediaFragment : PageableMediaFragment() {
@Inject @Inject
lateinit var presenter: CategoryMediaPresenter lateinit var presenter: CategoryMediaPresenter
override val injectedPresenter override val injectedPresenter
get() = presenter get() = presenter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
} }

View file

@ -13,8 +13,10 @@ interface CategoryMediaPresenter : PagingContract.Presenter<Media>
/** /**
* Presenter for DepictedImagesFragment * Presenter for DepictedImagesFragment
*/ */
class CategoryMediaPresenterImpl @Inject constructor( class CategoryMediaPresenterImpl
@Inject
constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler, @Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableCategoriesMediaDataSource dataSourceFactory: PageableCategoriesMediaDataSource,
) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory), ) : BasePagingPresenter<Media>(mainThreadScheduler, dataSourceFactory),
CategoryMediaPresenter CategoryMediaPresenter

View file

@ -7,14 +7,16 @@ import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import fr.free.nrw.commons.media.MediaClient import fr.free.nrw.commons.media.MediaClient
import javax.inject.Inject import javax.inject.Inject
class PageableCategoriesMediaDataSource @Inject constructor( class PageableCategoriesMediaDataSource
@Inject
constructor(
liveDataConverter: LiveDataConverter, liveDataConverter: LiveDataConverter,
private val mediaClient: MediaClient private val mediaClient: MediaClient,
) : PageableBaseDataSource<Media>(liveDataConverter) { ) : PageableBaseDataSource<Media>(liveDataConverter) {
override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int -> override val loadFunction: LoadFunction<Media> = { loadSize: Int, startPosition: Int ->
if(startPosition == 0){ if (startPosition == 0) {
mediaClient.resetCategoryContinuation(query) mediaClient.resetCategoryContinuation(query)
} }
mediaClient.getMediaListFromCategory(query).blockingGet() mediaClient.getMediaListFromCategory(query).blockingGet()
} }
} }

View file

@ -5,15 +5,16 @@ import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import javax.inject.Inject import javax.inject.Inject
class PageableParentCategoriesDataSource @Inject constructor( class PageableParentCategoriesDataSource
@Inject
constructor(
liveDataConverter: LiveDataConverter, liveDataConverter: LiveDataConverter,
val categoryClient: CategoryClient val categoryClient: CategoryClient,
) : PageableBaseDataSource<String>(liveDataConverter) { ) : PageableBaseDataSource<String>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int -> override val loadFunction = { loadSize: Int, startPosition: Int ->
if (startPosition == 0) { if (startPosition == 0) {
categoryClient.resetParentCategoryContinuation(query) categoryClient.resetParentCategoryContinuation(query)
} }
categoryClient.getParentCategoryList(query).blockingGet().map { it.name } categoryClient.getParentCategoryList(query).blockingGet().map { it.name }
} }
} }

View file

@ -7,9 +7,7 @@ import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.explore.categories.PageableCategoryFragment import fr.free.nrw.commons.explore.categories.PageableCategoryFragment
import javax.inject.Inject import javax.inject.Inject
class ParentCategoriesFragment : PageableCategoryFragment() { class ParentCategoriesFragment : PageableCategoryFragment() {
@Inject @Inject
lateinit var presenter: ParentCategoriesPresenter lateinit var presenter: ParentCategoriesPresenter
@ -18,9 +16,11 @@ class ParentCategoriesFragment : PageableCategoryFragment() {
override fun getEmptyText(query: String) = getString(R.string.no_parentcategory_found) override fun getEmptyText(query: String) = getString(R.string.no_parentcategory_found)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
} }
} }

View file

@ -7,11 +7,12 @@ import io.reactivex.Scheduler
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Named import javax.inject.Named
interface ParentCategoriesPresenter : PagingContract.Presenter<String> interface ParentCategoriesPresenter : PagingContract.Presenter<String>
class ParentCategoriesPresenterImpl @Inject constructor( class ParentCategoriesPresenterImpl
@Inject
constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler, @Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableParentCategoriesDataSource dataSourceFactory: PageableParentCategoriesDataSource,
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory), ) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
ParentCategoriesPresenter ParentCategoriesPresenter

View file

@ -5,13 +5,16 @@ import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import javax.inject.Inject import javax.inject.Inject
class PageableSearchCategoriesDataSource @Inject constructor( class PageableSearchCategoriesDataSource
@Inject
constructor(
liveDataConverter: LiveDataConverter, liveDataConverter: LiveDataConverter,
val categoryClient: CategoryClient val categoryClient: CategoryClient,
) : PageableBaseDataSource<String>(liveDataConverter) { ) : PageableBaseDataSource<String>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int -> override val loadFunction = { loadSize: Int, startPosition: Int ->
categoryClient.searchCategories(query, loadSize, startPosition).blockingGet() categoryClient
.searchCategories(query, loadSize, startPosition)
.blockingGet()
.map { it.name } .map { it.name }
} }
} }

View file

@ -9,8 +9,10 @@ import javax.inject.Named
interface SearchCategoriesFragmentPresenter : PagingContract.Presenter<String> interface SearchCategoriesFragmentPresenter : PagingContract.Presenter<String>
class SearchCategoriesFragmentPresenterImpl @Inject constructor( class SearchCategoriesFragmentPresenterImpl
@Inject
constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler, @Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableSearchCategoriesDataSource dataSourceFactory: PageableSearchCategoriesDataSource,
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory), ) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
SearchCategoriesFragmentPresenter SearchCategoriesFragmentPresenter

View file

@ -5,15 +5,16 @@ import fr.free.nrw.commons.explore.paging.LiveDataConverter
import fr.free.nrw.commons.explore.paging.PageableBaseDataSource import fr.free.nrw.commons.explore.paging.PageableBaseDataSource
import javax.inject.Inject import javax.inject.Inject
class PageableSubCategoriesDataSource @Inject constructor( class PageableSubCategoriesDataSource
@Inject
constructor(
liveDataConverter: LiveDataConverter, liveDataConverter: LiveDataConverter,
val categoryClient: CategoryClient val categoryClient: CategoryClient,
) : PageableBaseDataSource<String>(liveDataConverter) { ) : PageableBaseDataSource<String>(liveDataConverter) {
override val loadFunction = { loadSize: Int, startPosition: Int -> override val loadFunction = { loadSize: Int, startPosition: Int ->
if (startPosition == 0) { if (startPosition == 0) {
categoryClient.resetSubCategoryContinuation(query) categoryClient.resetSubCategoryContinuation(query)
} }
categoryClient.getSubCategoryList(query).blockingGet().map { it.name } categoryClient.getSubCategoryList(query).blockingGet().map { it.name }
} }
} }

View file

@ -7,9 +7,7 @@ import fr.free.nrw.commons.category.CATEGORY_PREFIX
import fr.free.nrw.commons.explore.categories.PageableCategoryFragment import fr.free.nrw.commons.explore.categories.PageableCategoryFragment
import javax.inject.Inject import javax.inject.Inject
class SubCategoriesFragment : PageableCategoryFragment() { class SubCategoriesFragment : PageableCategoryFragment() {
@Inject lateinit var presenter: SubCategoriesPresenter @Inject lateinit var presenter: SubCategoriesPresenter
override val injectedPresenter override val injectedPresenter
@ -17,7 +15,10 @@ class SubCategoriesFragment : PageableCategoryFragment() {
override fun getEmptyText(query: String) = getString(R.string.no_subcategory_found) override fun getEmptyText(query: String) = getString(R.string.no_subcategory_found)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}") onQueryUpdated("$CATEGORY_PREFIX${arguments!!.getString("categoryName")!!}")
} }

View file

@ -9,8 +9,10 @@ import javax.inject.Named
interface SubCategoriesPresenter : PagingContract.Presenter<String> interface SubCategoriesPresenter : PagingContract.Presenter<String>
class SubCategoriesPresenterImpl @Inject constructor( class SubCategoriesPresenterImpl
@Inject
constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler, @Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableSubCategoriesDataSource dataSourceFactory: PageableSubCategoriesDataSource,
) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory), ) : BasePagingPresenter<String>(mainThreadScheduler, dataSourceFactory),
SubCategoriesPresenter SubCategoriesPresenter

View file

@ -9,22 +9,31 @@ import fr.free.nrw.commons.R
import fr.free.nrw.commons.databinding.ItemDepictionsBinding import fr.free.nrw.commons.databinding.ItemDepictionsBinding
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
class DepictionAdapter(private val onDepictionClicked: (DepictedItem) -> Unit) : class DepictionAdapter(
PagedListAdapter<DepictedItem, DepictedItemViewHolder>(DepictionDiffUtilCallback) { private val onDepictionClicked: (DepictedItem) -> Unit,
) : PagedListAdapter<DepictedItem, DepictedItemViewHolder>(DepictionDiffUtilCallback) {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DepictedItemViewHolder( override fun onCreateViewHolder(
ItemDepictionsBinding.inflate(LayoutInflater.from(parent.context), parent, false) parent: ViewGroup,
viewType: Int,
) = DepictedItemViewHolder(
ItemDepictionsBinding.inflate(LayoutInflater.from(parent.context), parent, false),
) )
override fun onBindViewHolder(holder: DepictedItemViewHolder, position: Int) { override fun onBindViewHolder(
holder: DepictedItemViewHolder,
position: Int,
) {
holder.bind(getItem(position)!!, onDepictionClicked) holder.bind(getItem(position)!!, onDepictionClicked)
} }
} }
class DepictedItemViewHolder( class DepictedItemViewHolder(
private val binding: ItemDepictionsBinding private val binding: ItemDepictionsBinding,
) : RecyclerView.ViewHolder(binding.root) { ) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: DepictedItem, onDepictionClicked: (DepictedItem) -> Unit) = with(binding) { fun bind(
item: DepictedItem,
onDepictionClicked: (DepictedItem) -> Unit,
) = with(binding) {
root.setOnClickListener { onDepictionClicked(item) } root.setOnClickListener { onDepictionClicked(item) }
depictsLabel.text = item.name depictsLabel.text = item.name
description.text = item.description description.text = item.description
@ -37,9 +46,13 @@ class DepictedItemViewHolder(
} }
private object DepictionDiffUtilCallback : DiffUtil.ItemCallback<DepictedItem>() { private object DepictionDiffUtilCallback : DiffUtil.ItemCallback<DepictedItem>() {
override fun areItemsTheSame(oldItem: DepictedItem, newItem: DepictedItem) = override fun areItemsTheSame(
oldItem.id == newItem.id oldItem: DepictedItem,
newItem: DepictedItem,
) = oldItem.id == newItem.id
override fun areContentsTheSame(oldItem: DepictedItem, newItem: DepictedItem) = override fun areContentsTheSame(
oldItem == newItem oldItem: DepictedItem,
newItem: DepictedItem,
) = oldItem == newItem
} }

View file

@ -14,16 +14,12 @@ import fr.free.nrw.commons.explore.depictions.parent.ParentDepictionsPresenterIm
*/ */
@Module @Module
abstract class DepictionModule { abstract class DepictionModule {
@Binds
abstract fun ParentDepictionsPresenterImpl.bindsParentDepictionPresenter(): ParentDepictionsPresenter
@Binds @Binds
abstract fun ParentDepictionsPresenterImpl.bindsParentDepictionPresenter() abstract fun ChildDepictionsPresenterImpl.bindsChildDepictionPresenter(): ChildDepictionsPresenter
: ParentDepictionsPresenter
@Binds @Binds
abstract fun ChildDepictionsPresenterImpl.bindsChildDepictionPresenter() abstract fun DepictedImagesPresenterImpl.bindsDepictedImagesContractPresenter(): DepictedImagesPresenter
: ChildDepictionsPresenter
@Binds
abstract fun DepictedImagesPresenterImpl.bindsDepictedImagesContractPresenter()
: DepictedImagesPresenter
} }

View file

@ -20,28 +20,34 @@ import javax.inject.Singleton
* Depicts Client to handle custom calls to Commons Wikibase APIs * Depicts Client to handle custom calls to Commons Wikibase APIs
*/ */
@Singleton @Singleton
class DepictsClient @Inject constructor(private val depictsInterface: DepictsInterface) { class DepictsClient
@Inject
constructor(
private val depictsInterface: DepictsInterface,
) {
/** /**
* Search for depictions using the search item * Search for depictions using the search item
* @return list of depicted items * @return list of depicted items
*/ */
fun searchForDepictions(query: String?, limit: Int, offset: Int): Single<List<DepictedItem>> { fun searchForDepictions(
query: String?,
limit: Int,
offset: Int,
): Single<List<DepictedItem>> {
val language = Locale.getDefault().language val language = Locale.getDefault().language
return depictsInterface.searchForDepicts(query, "$limit", language, language, "$offset") return depictsInterface
.searchForDepicts(query, "$limit", language, language, "$offset")
.map { it.search.joinToString("|", transform = DepictSearchItem::id) } .map { it.search.joinToString("|", transform = DepictSearchItem::id) }
.mapToDepictions() .mapToDepictions()
} }
fun getEntities(ids: String): Single<Entities> { fun getEntities(ids: String): Single<Entities> = depictsInterface.getEntities(ids)
return depictsInterface.getEntities(ids)
}
fun toDepictions(sparqlResponse: Single<SparqlResponse>): Single<List<DepictedItem>> { fun toDepictions(sparqlResponse: Single<SparqlResponse>): Single<List<DepictedItem>> =
return sparqlResponse.map { sparqlResponse
.map {
it.results.bindings.joinToString("|", transform = Binding::id) it.results.bindings.joinToString("|", transform = Binding::id)
}.mapToDepictions() }.mapToDepictions()
}
/** /**
* Fetches Entities from ids ex. "Q1233|Q546" and converts them into DepictedItem * Fetches Entities from ids ex. "Q1233|Q546" and converts them into DepictedItem
@ -58,34 +64,39 @@ class DepictsClient @Inject constructor(private val depictsInterface: DepictsInt
/** /**
* Convert different entities into DepictedItem * Convert different entities into DepictedItem
*/ */
private fun mapToDepictItem(entity: Entities.Entity): DepictedItem { private fun mapToDepictItem(entity: Entities.Entity): DepictedItem =
return if (entity.descriptions().byLanguageOrFirstOrEmpty() == "") { if (entity.descriptions().byLanguageOrFirstOrEmpty() == "") {
val instanceOfIDs = entity[WikidataProperties.INSTANCE_OF] val instanceOfIDs =
entity[WikidataProperties.INSTANCE_OF]
.toIds() .toIds()
if (instanceOfIDs.isNotEmpty()) { if (instanceOfIDs.isNotEmpty()) {
val entities: Entities = getEntities(instanceOfIDs[0]).blockingGet() val entities: Entities = getEntities(instanceOfIDs[0]).blockingGet()
val nameAsDescription = entities.entities().values.first().labels() val nameAsDescription =
entities
.entities()
.values
.first()
.labels()
.byLanguageOrFirstOrEmpty() .byLanguageOrFirstOrEmpty()
DepictedItem( DepictedItem(
entity, entity,
entity.labels().byLanguageOrFirstOrEmpty(), entity.labels().byLanguageOrFirstOrEmpty(),
nameAsDescription nameAsDescription,
) )
} else { } else {
DepictedItem( DepictedItem(
entity, entity,
entity.labels().byLanguageOrFirstOrEmpty(), entity.labels().byLanguageOrFirstOrEmpty(),
"" "",
) )
} }
} else { } else {
DepictedItem( DepictedItem(
entity, entity,
entity.labels().byLanguageOrFirstOrEmpty(), entity.labels().byLanguageOrFirstOrEmpty(),
entity.descriptions().byLanguageOrFirstOrEmpty() entity.descriptions().byLanguageOrFirstOrEmpty(),
) )
} }
}
/** /**
* Tries to get Entities.Label by default language from the map. * Tries to get Entities.Label by default language from the map.
@ -94,15 +105,16 @@ class DepictsClient @Inject constructor(private val depictsInterface: DepictsInt
*/ */
private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() = private fun Map<String, Entities.Label>.byLanguageOrFirstOrEmpty() =
let { let {
it[Locale.getDefault().language] ?: it.values.firstOrNull() }?.value() ?: "" it[Locale.getDefault().language] ?: it.values.firstOrNull()
}?.value() ?: ""
/** /**
* returns list of id ex. "Q2323" from Statement_partial * returns list of id ex. "Q2323" from Statement_partial
*/ */
private fun List<Statement_partial>?.toIds(): List<String> { private fun List<Statement_partial>?.toIds(): List<String> =
return this?.map { it.mainSnak.dataValue } this
?.map { it.mainSnak.dataValue }
?.filterIsInstance<DataValue.EntityId>() ?.filterIsInstance<DataValue.EntityId>()
?.map { it.value.id } ?.map { it.value.id }
?: emptyList() ?: emptyList()
} }
}

View file

@ -4,7 +4,6 @@ import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.paging.BasePagingFragment import fr.free.nrw.commons.explore.paging.BasePagingFragment
import fr.free.nrw.commons.upload.structure.depictions.DepictedItem import fr.free.nrw.commons.upload.structure.depictions.DepictedItem
abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() { abstract class PageableDepictionsFragment : BasePagingFragment<DepictedItem>() {
override val errorTextId: Int = R.string.error_loading_depictions override val errorTextId: Int = R.string.error_loading_depictions
override val pagedListAdapter by lazy { override val pagedListAdapter by lazy {

View file

@ -6,18 +6,19 @@ import fr.free.nrw.commons.R
import fr.free.nrw.commons.explore.depictions.PageableDepictionsFragment import fr.free.nrw.commons.explore.depictions.PageableDepictionsFragment
import javax.inject.Inject import javax.inject.Inject
class ChildDepictionsFragment : PageableDepictionsFragment() {
class ChildDepictionsFragment: PageableDepictionsFragment() {
@Inject @Inject
lateinit var presenter: ChildDepictionsPresenter lateinit var presenter: ChildDepictionsPresenter
override val injectedPresenter override val injectedPresenter
get() = presenter get() = presenter
override fun getEmptyText(query: String) = override fun getEmptyText(query: String) = getString(R.string.no_child_classes, arguments!!.getString("wikidataItemName")!!)
getString(R.string.no_child_classes, arguments!!.getString("wikidataItemName")!!)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(
view: View,
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
onQueryUpdated(arguments!!.getString("entityId")!!) onQueryUpdated(arguments!!.getString("entityId")!!)
} }

View file

@ -10,8 +10,10 @@ import javax.inject.Named
interface ChildDepictionsPresenter : PagingContract.Presenter<DepictedItem> interface ChildDepictionsPresenter : PagingContract.Presenter<DepictedItem>
class ChildDepictionsPresenterImpl @Inject constructor( class ChildDepictionsPresenterImpl
@Inject
constructor(
@Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler, @Named(CommonsApplicationModule.MAIN_THREAD) mainThreadScheduler: Scheduler,
dataSourceFactory: PageableChildDepictionsDataSource dataSourceFactory: PageableChildDepictionsDataSource,
) : BasePagingPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory), ) : BasePagingPresenter<DepictedItem>(mainThreadScheduler, dataSourceFactory),
ChildDepictionsPresenter ChildDepictionsPresenter

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