mirror of
https://github.com/commons-app/apps-android-commons.git
synced 2025-10-26 20:33:53 +01:00
Feature: Show where file is being used on Commons & Other wikis (#6006)
* add url to build config Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * add network call functions Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * return response asynchronously Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * inject page size in the request Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * rename from Commons..Response.kt to ..Response.kt Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * convert to .kt Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * ui setup working Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * fix merge conflict Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * cleanup Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * fix CI Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * use suspend function for network calls Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> * doc * doc * doc * doc * doc --------- Signed-off-by: parneet-guraya <gurayaparneet@gmail.com> Co-authored-by: Nicolas Raoul <nicolas.raoul@gmail.com>
This commit is contained in:
parent
04a07ed655
commit
85d9aef2f3
13 changed files with 2587 additions and 1499 deletions
|
|
@ -52,13 +52,14 @@ dependencies {
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
|
|
||||||
// Jetpack Compose
|
// Jetpack Compose
|
||||||
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
|
def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
|
||||||
|
|
||||||
implementation "androidx.activity:activity-compose:1.9.1"
|
implementation "androidx.activity:activity-compose:1.9.3"
|
||||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
|
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
|
||||||
implementation (composeBom)
|
implementation (composeBom)
|
||||||
implementation "androidx.compose.runtime:runtime"
|
implementation "androidx.compose.runtime:runtime"
|
||||||
implementation "androidx.compose.ui:ui"
|
implementation "androidx.compose.ui:ui"
|
||||||
|
implementation "androidx.compose.ui:ui-viewbinding"
|
||||||
implementation "androidx.compose.ui:ui-graphics"
|
implementation "androidx.compose.ui:ui-graphics"
|
||||||
implementation "androidx.compose.ui:ui-tooling"
|
implementation "androidx.compose.ui:ui-tooling"
|
||||||
implementation "androidx.compose.foundation:foundation"
|
implementation "androidx.compose.foundation:foundation"
|
||||||
|
|
@ -313,6 +314,7 @@ android {
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
|
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.org/wiki/Special:PasswordReset\""
|
||||||
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
||||||
|
buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\""
|
||||||
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\""
|
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons\""
|
||||||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\""
|
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.contributions.contentprovider\""
|
||||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
|
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.modifications.contentprovider\""
|
||||||
|
|
@ -348,6 +350,7 @@ android {
|
||||||
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
buildConfigField "String", "SIGNUP_SUCCESS_REDIRECTION_URL", "\"https://commons.m.wikimedia.beta.wmflabs.org/w/index.php?title=Main_Page&welcome=yes\""
|
||||||
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""
|
buildConfigField "String", "FORGOT_PASSWORD_URL", "\"https://commons.wikimedia.beta.wmflabs.org/wiki/Special:PasswordReset\""
|
||||||
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
buildConfigField "String", "PRIVACY_POLICY_URL", "\"https://github.com/commons-app/commons-app-documentation/blob/master/android/Privacy-policy.md\""
|
||||||
|
buildConfigField "String", "FILE_USAGES_BASE_URL", "\"https://commons.wikimedia.org/w/api.php?action=query&format=json&formatversion=2\""
|
||||||
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\""
|
buildConfigField "String", "ACCOUNT_TYPE", "\"fr.free.nrw.commons.beta\""
|
||||||
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\""
|
buildConfigField "String", "CONTRIBUTION_AUTHORITY", "\"fr.free.nrw.commons.beta.contributions.contentprovider\""
|
||||||
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
|
buildConfigField "String", "MODIFICATION_AUTHORITY", "\"fr.free.nrw.commons.beta.modifications.contentprovider\""
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
package fr.free.nrw.commons.fileusages
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show where file is being used on Commons and oher wikis.
|
||||||
|
*/
|
||||||
|
data class FileUsagesResponse(
|
||||||
|
@SerializedName("continue") val continueResponse: CommonsContinue?,
|
||||||
|
@SerializedName("batchcomplete") val batchComplete: Boolean,
|
||||||
|
@SerializedName("query") val query: Query,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CommonsContinue(
|
||||||
|
@SerializedName("fucontinue") val fuContinue: String,
|
||||||
|
@SerializedName("continue") val continueKey: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Query(
|
||||||
|
@SerializedName("pages") val pages: List<Page>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class Page(
|
||||||
|
@SerializedName("pageid") val pageId: Int,
|
||||||
|
@SerializedName("ns") val nameSpace: Int,
|
||||||
|
@SerializedName("title") val title: String,
|
||||||
|
@SerializedName("fileusage") val fileUsage: List<FileUsage>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class FileUsage(
|
||||||
|
@SerializedName("pageid") val pageId: Int,
|
||||||
|
@SerializedName("ns") val nameSpace: Int,
|
||||||
|
@SerializedName("title") val title: String,
|
||||||
|
@SerializedName("redirect") val redirect: Boolean
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package fr.free.nrw.commons.fileusages
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show where file is being used on Commons and oher wikis.
|
||||||
|
*/
|
||||||
|
data class FileUsagesUiModel(
|
||||||
|
val title: String,
|
||||||
|
val link: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
fun FileUsage.toUiModel(): FileUsagesUiModel {
|
||||||
|
return FileUsagesUiModel(title = title, link = "https://commons.wikimedia.org/wiki/$title")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun GlobalFileUsage.toUiModel(): FileUsagesUiModel {
|
||||||
|
// link is associated with sub items under wiki group (which is not used ATM)
|
||||||
|
return FileUsagesUiModel(title = wiki, link = null)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package fr.free.nrw.commons.fileusages
|
||||||
|
|
||||||
|
import com.google.gson.annotations.SerializedName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show where file is being used on Commons and oher wikis.
|
||||||
|
*/
|
||||||
|
data class GlobalFileUsagesResponse(
|
||||||
|
@SerializedName("continue") val continueResponse: GlobalContinue?,
|
||||||
|
@SerializedName("batchcomplete") val batchComplete: Boolean,
|
||||||
|
@SerializedName("query") val query: GlobalQuery,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GlobalContinue(
|
||||||
|
@SerializedName("gucontinue") val guContinue: String,
|
||||||
|
@SerializedName("continue") val continueKey: String
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GlobalQuery(
|
||||||
|
@SerializedName("pages") val pages: List<GlobalPage>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GlobalPage(
|
||||||
|
@SerializedName("pageid") val pageId: Int,
|
||||||
|
@SerializedName("ns") val nameSpace: Int,
|
||||||
|
@SerializedName("title") val title: String,
|
||||||
|
@SerializedName("globalusage") val fileUsage: List<GlobalFileUsage>
|
||||||
|
)
|
||||||
|
|
||||||
|
data class GlobalFileUsage(
|
||||||
|
@SerializedName("title") val title: String,
|
||||||
|
@SerializedName("wiki") val wiki: String,
|
||||||
|
@SerializedName("url") val url: String
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load diff
2233
app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
Normal file
2233
app/src/main/java/fr/free/nrw/commons/media/MediaDetailFragment.kt
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,116 @@
|
||||||
|
package fr.free.nrw.commons.media
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import fr.free.nrw.commons.R
|
||||||
|
import fr.free.nrw.commons.fileusages.FileUsagesUiModel
|
||||||
|
import fr.free.nrw.commons.fileusages.toUiModel
|
||||||
|
import fr.free.nrw.commons.mwapi.OkHttpJsonApiClient
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show where file is being used on Commons and oher wikis.
|
||||||
|
*/
|
||||||
|
class MediaDetailViewModel(
|
||||||
|
private val applicationContext: Context,
|
||||||
|
private val okHttpJsonApiClient: OkHttpJsonApiClient
|
||||||
|
) :
|
||||||
|
ViewModel() {
|
||||||
|
|
||||||
|
private val _commonsContainerState =
|
||||||
|
MutableStateFlow<FileUsagesContainerState>(FileUsagesContainerState.Initial)
|
||||||
|
val commonsContainerState = _commonsContainerState.asStateFlow()
|
||||||
|
|
||||||
|
private val _globalContainerState =
|
||||||
|
MutableStateFlow<FileUsagesContainerState>(FileUsagesContainerState.Initial)
|
||||||
|
val globalContainerState = _globalContainerState.asStateFlow()
|
||||||
|
|
||||||
|
fun loadFileUsagesCommons(fileName: String) {
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
_commonsContainerState.update { FileUsagesContainerState.Loading }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result =
|
||||||
|
okHttpJsonApiClient.getFileUsagesOnCommons(fileName, 10)
|
||||||
|
|
||||||
|
val data = result?.query?.pages?.first()?.fileUsage?.map { it.toUiModel() }
|
||||||
|
|
||||||
|
_commonsContainerState.update { FileUsagesContainerState.Success(data = data) }
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
|
||||||
|
_commonsContainerState.update {
|
||||||
|
FileUsagesContainerState.Error(
|
||||||
|
errorMessage = applicationContext.getString(
|
||||||
|
R.string.error_while_loading
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.e(e, javaClass.simpleName)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadGlobalFileUsages(fileName: String) {
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
|
||||||
|
_globalContainerState.update { FileUsagesContainerState.Loading }
|
||||||
|
|
||||||
|
try {
|
||||||
|
val result = okHttpJsonApiClient.getGlobalFileUsages(fileName, 10)
|
||||||
|
|
||||||
|
val data = result?.query?.pages?.first()?.fileUsage?.map { it.toUiModel() }
|
||||||
|
|
||||||
|
_globalContainerState.update { FileUsagesContainerState.Success(data = data) }
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_globalContainerState.update {
|
||||||
|
FileUsagesContainerState.Error(
|
||||||
|
errorMessage = applicationContext.getString(
|
||||||
|
R.string.error_while_loading
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Timber.e(e, javaClass.simpleName)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class FileUsagesContainerState {
|
||||||
|
object Initial : FileUsagesContainerState()
|
||||||
|
object Loading : FileUsagesContainerState()
|
||||||
|
data class Success(val data: List<FileUsagesUiModel>?) : FileUsagesContainerState()
|
||||||
|
data class Error(val errorMessage: String) : FileUsagesContainerState()
|
||||||
|
}
|
||||||
|
|
||||||
|
class MediaDetailViewModelProviderFactory
|
||||||
|
@Inject constructor(
|
||||||
|
private val okHttpJsonApiClient: OkHttpJsonApiClient,
|
||||||
|
private val applicationContext: Context
|
||||||
|
) :
|
||||||
|
ViewModelProvider.Factory {
|
||||||
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
|
if (modelClass.isAssignableFrom(MediaDetailViewModel::class.java)) {
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
return MediaDetailViewModel(applicationContext, okHttpJsonApiClient) as T
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Unknown ViewModel class")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,8 +2,11 @@ package fr.free.nrw.commons.mwapi
|
||||||
|
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import com.google.gson.Gson
|
import com.google.gson.Gson
|
||||||
|
import fr.free.nrw.commons.BuildConfig
|
||||||
import fr.free.nrw.commons.campaigns.CampaignResponseDTO
|
import fr.free.nrw.commons.campaigns.CampaignResponseDTO
|
||||||
import fr.free.nrw.commons.explore.depictions.DepictsClient
|
import fr.free.nrw.commons.explore.depictions.DepictsClient
|
||||||
|
import fr.free.nrw.commons.fileusages.FileUsagesResponse
|
||||||
|
import fr.free.nrw.commons.fileusages.GlobalFileUsagesResponse
|
||||||
import fr.free.nrw.commons.location.LatLng
|
import fr.free.nrw.commons.location.LatLng
|
||||||
import fr.free.nrw.commons.nearby.Place
|
import fr.free.nrw.commons.nearby.Place
|
||||||
import fr.free.nrw.commons.nearby.model.ItemsClass
|
import fr.free.nrw.commons.nearby.model.ItemsClass
|
||||||
|
|
@ -20,6 +23,8 @@ import fr.free.nrw.commons.utils.ConfigUtils.isBetaFlavour
|
||||||
import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse
|
import fr.free.nrw.commons.wikidata.model.GetWikidataEditCountResponse
|
||||||
import io.reactivex.Observable
|
import io.reactivex.Observable
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import okhttp3.HttpUrl
|
import okhttp3.HttpUrl
|
||||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
|
@ -50,8 +55,10 @@ class OkHttpJsonApiClient @Inject constructor(
|
||||||
): Observable<LeaderboardResponse> {
|
): Observable<LeaderboardResponse> {
|
||||||
val fetchLeaderboardUrlTemplate =
|
val fetchLeaderboardUrlTemplate =
|
||||||
wikiMediaToolforgeUrl.toString() + LeaderboardConstants.LEADERBOARD_END_POINT
|
wikiMediaToolforgeUrl.toString() + LeaderboardConstants.LEADERBOARD_END_POINT
|
||||||
val url = String.format(Locale.ENGLISH,
|
val url = String.format(
|
||||||
fetchLeaderboardUrlTemplate, userName, duration, category, limit, offset)
|
Locale.ENGLISH,
|
||||||
|
fetchLeaderboardUrlTemplate, userName, duration, category, limit, offset
|
||||||
|
)
|
||||||
val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder()
|
val urlBuilder: HttpUrl.Builder = url.toHttpUrlOrNull()!!.newBuilder()
|
||||||
.addQueryParameter("user", userName)
|
.addQueryParameter("user", userName)
|
||||||
.addQueryParameter("duration", duration)
|
.addQueryParameter("duration", duration)
|
||||||
|
|
@ -80,6 +87,80 @@ class OkHttpJsonApiClient @Inject constructor(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show where file is being used on Commons.
|
||||||
|
*/
|
||||||
|
suspend fun getFileUsagesOnCommons(
|
||||||
|
fileName: String?,
|
||||||
|
pageSize: Int
|
||||||
|
): FileUsagesResponse? {
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
return@withContext try {
|
||||||
|
|
||||||
|
val urlBuilder = BuildConfig.FILE_USAGES_BASE_URL.toHttpUrlOrNull()!!.newBuilder()
|
||||||
|
urlBuilder.addQueryParameter("prop", "fileusage")
|
||||||
|
urlBuilder.addQueryParameter("titles", fileName)
|
||||||
|
urlBuilder.addQueryParameter("fulimit", pageSize.toString())
|
||||||
|
|
||||||
|
Timber.i("Url %s", urlBuilder.toString())
|
||||||
|
val request: Request = Request.Builder()
|
||||||
|
.url(urlBuilder.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response: Response = okHttpClient.newCall(request).execute()
|
||||||
|
if (response.body != null && response.isSuccessful) {
|
||||||
|
val json: String = response.body!!.string()
|
||||||
|
gson.fromJson<FileUsagesResponse>(
|
||||||
|
json,
|
||||||
|
FileUsagesResponse::class.java
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show where file is being used on non-Commons wikis, typically the Wikipedias in various languages.
|
||||||
|
*/
|
||||||
|
suspend fun getGlobalFileUsages(
|
||||||
|
fileName: String?,
|
||||||
|
pageSize: Int
|
||||||
|
): GlobalFileUsagesResponse? {
|
||||||
|
|
||||||
|
return withContext(Dispatchers.IO) {
|
||||||
|
|
||||||
|
return@withContext try {
|
||||||
|
|
||||||
|
val urlBuilder = BuildConfig.FILE_USAGES_BASE_URL.toHttpUrlOrNull()!!.newBuilder()
|
||||||
|
urlBuilder.addQueryParameter("prop", "globalusage")
|
||||||
|
urlBuilder.addQueryParameter("titles", fileName)
|
||||||
|
urlBuilder.addQueryParameter("gulimit", pageSize.toString())
|
||||||
|
|
||||||
|
Timber.i("Url %s", urlBuilder.toString())
|
||||||
|
val request: Request = Request.Builder()
|
||||||
|
.url(urlBuilder.toString())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val response: Response = okHttpClient.newCall(request).execute()
|
||||||
|
if (response.body != null && response.isSuccessful) {
|
||||||
|
val json: String = response.body!!.string()
|
||||||
|
|
||||||
|
gson.fromJson<GlobalFileUsagesResponse>(
|
||||||
|
json,
|
||||||
|
GlobalFileUsagesResponse::class.java
|
||||||
|
)
|
||||||
|
} else null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setAvatar(username: String?, avatar: String?): Single<UpdateAvatarResponse?> {
|
fun setAvatar(username: String?, avatar: String?): Single<UpdateAvatarResponse?> {
|
||||||
val urlTemplate = wikiMediaToolforgeUrl
|
val urlTemplate = wikiMediaToolforgeUrl
|
||||||
.toString() + LeaderboardConstants.UPDATE_AVATAR_END_POINT
|
.toString() + LeaderboardConstants.UPDATE_AVATAR_END_POINT
|
||||||
|
|
|
||||||
|
|
@ -456,6 +456,11 @@
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<androidx.compose.ui.platform.ComposeView
|
||||||
|
android:id="@+id/file_usages_compose_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/copyWikicode"
|
android:id="@+id/copyWikicode"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
|
||||||
|
|
@ -849,4 +849,12 @@ Upload your first media by tapping on the add button.</string>
|
||||||
<string name="red_pin">This place has no picture yet, go take one!</string>
|
<string name="red_pin">This place has no picture yet, go take one!</string>
|
||||||
<string name="green_pin">This place has a picture already.</string>
|
<string name="green_pin">This place has a picture already.</string>
|
||||||
<string name="grey_pin">Now checking whether this place has a picture.</string>
|
<string name="grey_pin">Now checking whether this place has a picture.</string>
|
||||||
|
|
||||||
|
<string name="error_while_loading">Error while loading</string>
|
||||||
|
<string name="no_usages_found">No usages found</string>
|
||||||
|
<string name="usages_on_commons_heading">Commons</string>
|
||||||
|
<string name="usages_on_other_wikis_heading">Other wikis</string>
|
||||||
|
<string name="bullet_point">•</string>
|
||||||
|
<string name="file_usages_container_heading">File usages</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ import fr.free.nrw.commons.customselector.ui.selector.ImageLoader
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
|
|
@ -34,7 +34,6 @@ import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import java.lang.reflect.Field
|
import java.lang.reflect.Field
|
||||||
import java.util.TreeMap
|
import java.util.TreeMap
|
||||||
import kotlin.collections.ArrayList
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Selector image adapter test.
|
* Custom Selector image adapter test.
|
||||||
|
|
@ -65,7 +64,7 @@ class ImageAdapterTest {
|
||||||
private lateinit var selectedImageField: Field
|
private lateinit var selectedImageField: Field
|
||||||
private var uri: Uri = Mockito.mock(Uri::class.java)
|
private var uri: Uri = Mockito.mock(Uri::class.java)
|
||||||
private lateinit var image: Image
|
private lateinit var image: Image
|
||||||
private val testDispatcher = TestCoroutineDispatcher()
|
private val testDispatcher = StandardTestDispatcher()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set up variables.
|
* Set up variables.
|
||||||
|
|
@ -91,7 +90,6 @@ class ImageAdapterTest {
|
||||||
@After
|
@After
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
testDispatcher.cleanupTestCoroutines()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -21,9 +21,10 @@ import fr.free.nrw.commons.upload.FileUtilsWrapper
|
||||||
import io.reactivex.Single
|
import io.reactivex.Single
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
import kotlinx.coroutines.test.TestCoroutineDispatcher
|
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||||
|
import kotlinx.coroutines.test.TestScope
|
||||||
import kotlinx.coroutines.test.resetMain
|
import kotlinx.coroutines.test.resetMain
|
||||||
import kotlinx.coroutines.test.runBlockingTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import kotlinx.coroutines.test.setMain
|
import kotlinx.coroutines.test.setMain
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
import org.junit.Assert
|
import org.junit.Assert
|
||||||
|
|
@ -45,7 +46,6 @@ import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import kotlin.collections.HashMap
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Image Loader Test.
|
* Image Loader Test.
|
||||||
|
|
@ -92,7 +92,7 @@ class ImageLoaderTest {
|
||||||
private lateinit var contentResolver: ContentResolver
|
private lateinit var contentResolver: ContentResolver
|
||||||
|
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
private val testDispacher = TestCoroutineDispatcher()
|
private val testDispacher = StandardTestDispatcher()
|
||||||
|
|
||||||
private lateinit var imageLoader: ImageLoader
|
private lateinit var imageLoader: ImageLoader
|
||||||
private var mapImageSHA1: HashMap<Uri, String> = HashMap()
|
private var mapImageSHA1: HashMap<Uri, String> = HashMap()
|
||||||
|
|
@ -153,7 +153,6 @@ class ImageLoaderTest {
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun tearDown() {
|
fun tearDown() {
|
||||||
Dispatchers.resetMain()
|
Dispatchers.resetMain()
|
||||||
testDispacher.cleanupTestCoroutines()
|
|
||||||
mockedPickedFiles.close()
|
mockedPickedFiles.close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,7 +161,7 @@ class ImageLoaderTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testQueryAndSetViewUploadedStatusNull() =
|
fun testQueryAndSetViewUploadedStatusNull() =
|
||||||
testDispacher.runBlockingTest {
|
TestScope(testDispacher).runTest {
|
||||||
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null)
|
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(null)
|
||||||
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
||||||
mapModifiedImageSHA1[image] = "testSha1"
|
mapModifiedImageSHA1[image] = "testSha1"
|
||||||
|
|
@ -182,7 +181,7 @@ class ImageLoaderTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testQueryAndSetViewUploadedStatusNotNull() =
|
fun testQueryAndSetViewUploadedStatusNotNull() =
|
||||||
testDispacher.runBlockingTest {
|
TestScope(testDispacher).runTest {
|
||||||
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus)
|
whenever(uploadedStatusDao.getUploadedFromImageSHA1(any())).thenReturn(uploadedStatus)
|
||||||
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
||||||
whenever(context.getSharedPreferences("custom_selector", 0))
|
whenever(context.getSharedPreferences("custom_selector", 0))
|
||||||
|
|
@ -195,7 +194,7 @@ class ImageLoaderTest {
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testNextActionableImage() =
|
fun testNextActionableImage() =
|
||||||
testDispacher.runBlockingTest {
|
TestScope(testDispacher).runTest {
|
||||||
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
whenever(notForUploadStatusDao.find(any())).thenReturn(0)
|
||||||
whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(0)
|
whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(0)
|
||||||
whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(0)
|
whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(0)
|
||||||
|
|
@ -208,16 +207,40 @@ class ImageLoaderTest {
|
||||||
whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
|
whenever(PickedFiles.pickedExistingPicture(context, Uri.parse("test"))).thenReturn(
|
||||||
uploadableFile,
|
uploadableFile,
|
||||||
)
|
)
|
||||||
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0, emptyList())
|
imageLoader.nextActionableImage(
|
||||||
|
listOf(image),
|
||||||
|
testDispacher,
|
||||||
|
testDispacher,
|
||||||
|
0,
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
whenever(notForUploadStatusDao.find(any())).thenReturn(1)
|
whenever(notForUploadStatusDao.find(any())).thenReturn(1)
|
||||||
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0, emptyList())
|
imageLoader.nextActionableImage(
|
||||||
|
listOf(image),
|
||||||
|
testDispacher,
|
||||||
|
testDispacher,
|
||||||
|
0,
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(2)
|
whenever(uploadedStatusDao.findByImageSHA1(any(), any())).thenReturn(2)
|
||||||
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0, emptyList())
|
imageLoader.nextActionableImage(
|
||||||
|
listOf(image),
|
||||||
|
testDispacher,
|
||||||
|
testDispacher,
|
||||||
|
0,
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(2)
|
whenever(uploadedStatusDao.findByModifiedImageSHA1(any(), any())).thenReturn(2)
|
||||||
imageLoader.nextActionableImage(listOf(image), testDispacher, testDispacher, 0, emptyList())
|
imageLoader.nextActionableImage(
|
||||||
|
listOf(image),
|
||||||
|
testDispacher,
|
||||||
|
testDispacher,
|
||||||
|
0,
|
||||||
|
emptyList()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -226,7 +249,7 @@ class ImageLoaderTest {
|
||||||
@Test
|
@Test
|
||||||
@ExperimentalCoroutinesApi
|
@ExperimentalCoroutinesApi
|
||||||
fun testGetSha1() =
|
fun testGetSha1() =
|
||||||
testDispacher.runBlockingTest {
|
TestScope(testDispacher).runTest {
|
||||||
BDDMockito
|
BDDMockito
|
||||||
.given(PickedFiles.pickedExistingPicture(context, image.uri))
|
.given(PickedFiles.pickedExistingPicture(context, image.uri))
|
||||||
.willReturn(UploadableFile(uri, File("ABC")))
|
.willReturn(UploadableFile(uri, File("ABC")))
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ class MediaDetailFragmentUnitTests {
|
||||||
@Mock
|
@Mock
|
||||||
private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor
|
private lateinit var mockSharedPreferencesEditor: SharedPreferences.Editor
|
||||||
|
|
||||||
private lateinit var binding: FragmentMediaDetailBinding
|
private lateinit var _binding: FragmentMediaDetailBinding
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setUp() {
|
fun setUp() {
|
||||||
|
|
@ -185,12 +185,12 @@ class MediaDetailFragmentUnitTests {
|
||||||
|
|
||||||
layoutInflater = LayoutInflater.from(activity)
|
layoutInflater = LayoutInflater.from(activity)
|
||||||
|
|
||||||
binding = FragmentMediaDetailBinding.inflate(layoutInflater)
|
_binding = FragmentMediaDetailBinding.inflate(layoutInflater)
|
||||||
|
|
||||||
scrollView = binding.mediaDetailScrollView
|
scrollView = _binding.mediaDetailScrollView
|
||||||
|
|
||||||
progressBarDeletion = binding.progressBarDeletion
|
progressBarDeletion = _binding.progressBarDeletion
|
||||||
delete = binding.nominateDeletion
|
delete = _binding.nominateDeletion
|
||||||
|
|
||||||
Whitebox.setInternalState(fragment, "media", media)
|
Whitebox.setInternalState(fragment, "media", media)
|
||||||
Whitebox.setInternalState(fragment, "isDeleted", isDeleted)
|
Whitebox.setInternalState(fragment, "isDeleted", isDeleted)
|
||||||
|
|
@ -198,13 +198,13 @@ class MediaDetailFragmentUnitTests {
|
||||||
Whitebox.setInternalState(fragment, "reasonListEnglishMappings", reasonListEnglishMappings)
|
Whitebox.setInternalState(fragment, "reasonListEnglishMappings", reasonListEnglishMappings)
|
||||||
Whitebox.setInternalState(fragment, "reasonBuilder", reasonBuilder)
|
Whitebox.setInternalState(fragment, "reasonBuilder", reasonBuilder)
|
||||||
Whitebox.setInternalState(fragment, "deleteHelper", deleteHelper)
|
Whitebox.setInternalState(fragment, "deleteHelper", deleteHelper)
|
||||||
Whitebox.setInternalState(fragment, "binding", binding)
|
Whitebox.setInternalState(fragment, "_binding", _binding)
|
||||||
Whitebox.setInternalState(fragment, "detailProvider", detailProvider)
|
Whitebox.setInternalState(fragment, "detailProvider", detailProvider)
|
||||||
Whitebox.setInternalState(binding, "mediaDetailImageView", simpleDraweeView)
|
Whitebox.setInternalState(_binding, "mediaDetailImageView", simpleDraweeView)
|
||||||
Whitebox.setInternalState(binding, "mediaDetailTitle", textView)
|
Whitebox.setInternalState(_binding, "mediaDetailTitle", textView)
|
||||||
Whitebox.setInternalState(binding, "mediaDetailDepictionContainer", linearLayout)
|
Whitebox.setInternalState(_binding, "mediaDetailDepictionContainer", linearLayout)
|
||||||
Whitebox.setInternalState(binding, "dummyCaptionDescriptionContainer", linearLayout)
|
Whitebox.setInternalState(_binding, "dummyCaptionDescriptionContainer", linearLayout)
|
||||||
Whitebox.setInternalState(binding, "depictionsEditButton", button)
|
Whitebox.setInternalState(_binding, "depictionsEditButton", button)
|
||||||
Whitebox.setInternalState(fragment, "locationManager", locationManager)
|
Whitebox.setInternalState(fragment, "locationManager", locationManager)
|
||||||
|
|
||||||
`when`(simpleDraweeView.hierarchy).thenReturn(genericDraweeHierarchy)
|
`when`(simpleDraweeView.hierarchy).thenReturn(genericDraweeHierarchy)
|
||||||
|
|
@ -241,7 +241,7 @@ class MediaDetailFragmentUnitTests {
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun testLaunchZoomActivity() {
|
fun testLaunchZoomActivity() {
|
||||||
`when`(media.imageUrl).thenReturn("")
|
`when`(media.imageUrl).thenReturn("")
|
||||||
fragment.launchZoomActivity(binding.root)
|
fragment.launchZoomActivity(_binding.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue