Merge branch 'main' into main

This commit is contained in:
Akshay Komar 2025-01-10 23:19:30 +05:30 committed by GitHub
commit 83bef3a6b3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 556 additions and 220 deletions

View file

@ -0,0 +1,181 @@
package fr.free.nrw.commons.activity
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.webkit.ConsoleMessage
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.AndroidView
import fr.free.nrw.commons.R
import timber.log.Timber
/**
* SingleWebViewActivity is a reusable activity webView based on a given url(initial url) and
* closes itself when a specified success URL is reached to success url.
*/
class SingleWebViewActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val url = intent.getStringExtra(VANISH_ACCOUNT_URL)
val successUrl = intent.getStringExtra(VANISH_ACCOUNT_SUCCESS_URL)
if (url == null || successUrl == null) {
finish()
return
}
enableEdgeToEdge()
setContent {
Scaffold(
topBar = {
TopAppBar(
modifier = Modifier,
title = { Text(getString(R.string.vanish_account)) },
navigationIcon = {
IconButton(
onClick = {
// Close the WebView Activity if the user taps the back button
finish()
},
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
// TODO("Add contentDescription)
contentDescription = ""
)
}
}
)
},
content = {
WebViewComponent(
url = url,
successUrl = successUrl,
onSuccess = {
// TODO Redirect the user to login screen like we do when the user logout's
finish()
},
modifier = Modifier
.fillMaxSize()
.padding(it)
)
}
)
}
}
/**
* @param url The initial URL which we are loading in the WebView.
* @param successUrl The URL that, when reached, triggers the `onSuccess` callback.
* @param onSuccess A callback that is invoked when the current url of webView is successUrl.
* This is used when we want to close when the webView once a success url is hit.
* @param modifier An optional [Modifier] to customize the layout or appearance of the WebView.
*/
@SuppressLint("SetJavaScriptEnabled")
@Composable
private fun WebViewComponent(
url: String,
successUrl: String,
onSuccess: () -> Unit,
modifier: Modifier = Modifier
) {
val webView = remember { mutableStateOf<WebView?>(null) }
AndroidView(
modifier = modifier,
factory = {
WebView(it).apply {
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
javaScriptCanOpenWindowsAutomatically = true
}
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
view: WebView?,
request: WebResourceRequest?
): Boolean {
request?.url?.let { url ->
Timber.d("URL Loading: $url")
if (url.toString() == successUrl) {
Timber.d("Success URL detected. Closing WebView.")
onSuccess() // Close the activity
return true
}
return false
}
return false
}
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
}
}
webChromeClient = object : WebChromeClient() {
override fun onConsoleMessage(message: ConsoleMessage): Boolean {
Timber.d("Console: ${message.message()} -- From line ${message.lineNumber()} of ${message.sourceId()}")
return true
}
}
loadUrl(url)
}
},
update = {
webView.value = it
}
)
}
companion object {
private const val VANISH_ACCOUNT_URL = "VanishAccountUrl"
private const val VANISH_ACCOUNT_SUCCESS_URL = "vanishAccountSuccessUrl"
/**
* Launch the WebViewActivity with the specified URL and success URL.
* @param context The context from which the activity is launched.
* @param url The initial URL to load in the WebView.
* @param successUrl The URL that triggers the WebView to close when matched.
*/
fun showWebView(
context: Context,
url: String,
successUrl: String
) {
val intent = Intent(
context,
SingleWebViewActivity::class.java
).apply {
putExtra(VANISH_ACCOUNT_URL, url)
putExtra(VANISH_ACCOUNT_SUCCESS_URL, successUrl)
}
context.startActivity(intent)
}
}
}

View file

@ -1,23 +0,0 @@
package fr.free.nrw.commons.concurrency;
import androidx.annotation.NonNull;
import fr.free.nrw.commons.BuildConfig;
public class BackgroundPoolExceptionHandler implements ExceptionHandler {
/**
* If an exception occurs on a background thread, this handler will crash for debug builds
* but fail silently for release builds.
* @param t
*/
@Override
public void onException(@NonNull final Throwable t) {
//Crash for debug build
if (BuildConfig.DEBUG) {
Thread thread = new Thread(() -> {
throw new RuntimeException(t);
});
thread.start();
}
}
}

View file

@ -0,0 +1,21 @@
package fr.free.nrw.commons.concurrency
import fr.free.nrw.commons.BuildConfig
class BackgroundPoolExceptionHandler : ExceptionHandler {
/**
* If an exception occurs on a background thread, this handler will crash for debug builds
* but fail silently for release builds.
* @param t
*/
override fun onException(t: Throwable) {
// Crash for debug build
if (BuildConfig.DEBUG) {
val thread = Thread {
throw RuntimeException(t)
}
thread.start()
}
}
}

View file

@ -1,39 +0,0 @@
package fr.free.nrw.commons.concurrency;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
class ExceptionAwareThreadPoolExecutor extends ScheduledThreadPoolExecutor {
private final ExceptionHandler exceptionHandler;
public ExceptionAwareThreadPoolExecutor(int corePoolSize, ThreadFactory threadFactory,
ExceptionHandler exceptionHandler) {
super(corePoolSize, threadFactory);
this.exceptionHandler = exceptionHandler;
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
Future<?> future = (Future<?>) r;
if (future.isDone()) future.get();
} catch (CancellationException | InterruptedException e) {
//ignore
} catch (ExecutionException e) {
t = e.getCause() != null ? e.getCause() : e;
} catch (Exception e) {
t = e;
}
}
if (t != null) {
exceptionHandler.onException(t);
}
}
}

View file

@ -0,0 +1,40 @@
package fr.free.nrw.commons.concurrency
import java.util.concurrent.CancellationException
import java.util.concurrent.ExecutionException
import java.util.concurrent.Future
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.ThreadFactory
class ExceptionAwareThreadPoolExecutor(
corePoolSize: Int,
threadFactory: ThreadFactory,
private val exceptionHandler: ExceptionHandler?
) : ScheduledThreadPoolExecutor(corePoolSize, threadFactory) {
override fun afterExecute(r: Runnable, t: Throwable?) {
super.afterExecute(r, t)
var throwable = t
if (throwable == null && r is Future<*>) {
try {
if (r.isDone) {
r.get()
}
} catch (e: CancellationException) {
// ignore
} catch (e: InterruptedException) {
// ignore
} catch (e: ExecutionException) {
throwable = e.cause ?: e
} catch (e: Exception) {
throwable = e
}
}
throwable?.let {
exceptionHandler?.onException(it)
}
}
}

View file

@ -1,7 +0,0 @@
package fr.free.nrw.commons.concurrency;
import androidx.annotation.NonNull;
public interface ExceptionHandler {
void onException(@NonNull Throwable t);
}

View file

@ -0,0 +1,7 @@
package fr.free.nrw.commons.concurrency
interface ExceptionHandler {
fun onException(t: Throwable)
}

View file

@ -1,124 +0,0 @@
package fr.free.nrw.commons.concurrency;
import androidx.annotation.NonNull;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
/**
* This class is a thread pool which provides some additional features:
* - it sets the thread priority to a value lower than foreground priority by default, or you can
* supply your own priority
* - it gives you a way to handle exceptions thrown in the thread pool
*/
public class ThreadPoolService implements Executor {
private final ScheduledThreadPoolExecutor backgroundPool;
private ThreadPoolService(final Builder b) {
backgroundPool = new ExceptionAwareThreadPoolExecutor(b.poolSize,
new ThreadFactory() {
int count = 0;
@Override
public Thread newThread(@NonNull Runnable r) {
count++;
Thread t = new Thread(r, String.format("%s-%s", b.name, count));
//If the priority is specified out of range, we set the thread priority to Thread.MIN_PRIORITY
//It's done prevent IllegalArgumentException and to prevent setting of improper high priority for a less priority task
t.setPriority(b.priority > Thread.MAX_PRIORITY || b.priority < Thread.MIN_PRIORITY ?
Thread.MIN_PRIORITY : b.priority);
return t;
}
}, b.exceptionHandler);
}
public <V> ScheduledFuture<V> schedule(Callable<V> callable, long time, TimeUnit timeUnit) {
return backgroundPool.schedule(callable, time, timeUnit);
}
public ScheduledFuture<?> schedule(Runnable runnable) {
return schedule(runnable, 0, TimeUnit.SECONDS);
}
public ScheduledFuture<?> schedule(Runnable runnable, long time, TimeUnit timeUnit) {
return backgroundPool.schedule(runnable, time, timeUnit);
}
public ScheduledFuture<?> scheduleAtFixedRate(final Runnable task, long initialDelay,
long period, final TimeUnit timeUnit) {
return backgroundPool.scheduleAtFixedRate(task, initialDelay, period, timeUnit);
}
public ScheduledThreadPoolExecutor executor() {
return backgroundPool;
}
public void shutdown(){
backgroundPool.shutdown();
}
@Override
public void execute(Runnable command) {
backgroundPool.execute(command);
}
/**
* Builder class for {@link ThreadPoolService}
*/
public static class Builder {
//Required
private final String name;
//Optional
private int poolSize = 1;
private int priority = Thread.MIN_PRIORITY;
private ExceptionHandler exceptionHandler = null;
/**
* @param name the name of the threads in the service. if there are N threads,
* the thread names will be like name-1, name-2, name-3,...,name-N
*/
public Builder(@NonNull String name) {
this.name = name;
}
/**
* @param poolSize the number of threads to keep in the pool
* @throws IllegalArgumentException if size of pool <=0
*/
public Builder setPoolSize(int poolSize) throws IllegalArgumentException {
if (poolSize <= 0) {
throw new IllegalArgumentException("Pool size must be grater than 0");
}
this.poolSize = poolSize;
return this;
}
/**
* @param priority Priority of the threads in the service. You can supply a constant from
* {@link java.lang.Thread} or
* specify your own priority in the range 1(MIN_PRIORITY) to 10(MAX_PRIORITY)
* By default, the priority is set to {@link java.lang.Thread#MIN_PRIORITY}
*/
public Builder setPriority(int priority) {
this.priority = priority;
return this;
}
/**
* @param handler The handler to use to handle exceptions in the service
*/
public Builder setExceptionHandler(ExceptionHandler handler) {
this.exceptionHandler = handler;
return this;
}
public ThreadPoolService build() {
return new ThreadPoolService(this);
}
}
}

View file

@ -0,0 +1,122 @@
package fr.free.nrw.commons.concurrency
import java.util.concurrent.Callable
import java.util.concurrent.Executor
import java.util.concurrent.ScheduledFuture
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.ThreadFactory
import java.util.concurrent.TimeUnit
/**
* This class is a thread pool which provides some additional features:
* - it sets the thread priority to a value lower than foreground priority by default, or you can
* supply your own priority
* - it gives you a way to handle exceptions thrown in the thread pool
*/
class ThreadPoolService private constructor(builder: Builder) : Executor {
private val backgroundPool: ScheduledThreadPoolExecutor = ExceptionAwareThreadPoolExecutor(
builder.poolSize,
object : ThreadFactory {
private var count = 0
override fun newThread(r: Runnable): Thread {
count++
val t = Thread(r, "${builder.name}-$count")
// If the priority is specified out of range, we set the thread priority to
// Thread.MIN_PRIORITY
// It's done to prevent IllegalArgumentException and to prevent setting of
// improper high priority for a less priority task
t.priority =
if (
builder.priority > Thread.MAX_PRIORITY
||
builder.priority < Thread.MIN_PRIORITY
) {
Thread.MIN_PRIORITY
} else {
builder.priority
}
return t
}
},
builder.exceptionHandler
)
fun <V> schedule(callable: Callable<V>, time: Long, timeUnit: TimeUnit): ScheduledFuture<V> {
return backgroundPool.schedule(callable, time, timeUnit)
}
fun schedule(runnable: Runnable): ScheduledFuture<*> {
return schedule(runnable, 0, TimeUnit.SECONDS)
}
fun schedule(runnable: Runnable, time: Long, timeUnit: TimeUnit): ScheduledFuture<*> {
return backgroundPool.schedule(runnable, time, timeUnit)
}
fun scheduleAtFixedRate(
task: Runnable,
initialDelay: Long,
period: Long,
timeUnit: TimeUnit
): ScheduledFuture<*> {
return backgroundPool.scheduleWithFixedDelay(task, initialDelay, period, timeUnit)
}
fun executor(): ScheduledThreadPoolExecutor {
return backgroundPool
}
fun shutdown() {
backgroundPool.shutdown()
}
override fun execute(command: Runnable) {
backgroundPool.execute(command)
}
/**
* Builder class for [ThreadPoolService]
*/
class Builder(val name: String) {
var poolSize: Int = 1
var priority: Int = Thread.MIN_PRIORITY
var exceptionHandler: ExceptionHandler? = null
/**
* @param poolSize the number of threads to keep in the pool
* @throws IllegalArgumentException if size of pool <= 0
*/
fun setPoolSize(poolSize: Int): Builder {
if (poolSize <= 0) {
throw IllegalArgumentException("Pool size must be greater than 0")
}
this.poolSize = poolSize
return this
}
/**
* @param priority Priority of the threads in the service. You can supply a constant from
* [java.lang.Thread] or
* specify your own priority in the range 1(MIN_PRIORITY)
* to 10(MAX_PRIORITY)
* By default, the priority is set to [java.lang.Thread.MIN_PRIORITY]
*/
fun setPriority(priority: Int): Builder {
this.priority = priority
return this
}
/**
* @param handler The handler to use to handle exceptions in the service
*/
fun setExceptionHandler(handler: ExceptionHandler): Builder {
exceptionHandler = handler
return this
}
fun build(): ThreadPoolService {
return ThreadPoolService(this)
}
}
}

View file

@ -1,12 +1,9 @@
package fr.free.nrw.commons.settings
object Prefs {
const val GLOBAL_PREFS = "fr.free.nrw.commons.preferences"
const val TRACKING_ENABLED = "eventLogging"
const val DEFAULT_LICENSE = "defaultLicense"
const val UPLOADS_SHOWING = "uploadsShowing"
const val MANAGED_EXIF_TAGS = "managed_exif_tags"
const val VANISHED_ACCOUNT = "vanishAccount"
const val DESCRIPTION_LANGUAGE = "languageDescription"
const val APP_UI_LANGUAGE = "appUiLanguage"
const val KEY_THEME_VALUE = "appThemePref"

View file

@ -12,7 +12,6 @@ import fr.free.nrw.commons.theme.BaseActivity
class SettingsActivity : BaseActivity() {
private lateinit var binding: ActivitySettingsBinding
// private var settingsDelegate: AppCompatDelegate? = null
/**
* to be called when the activity starts

View file

@ -1,7 +1,6 @@
package fr.free.nrw.commons.settings
import android.Manifest.permission
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Context.MODE_PRIVATE
@ -11,7 +10,6 @@ import android.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.KeyEvent
import android.view.View
import android.widget.AdapterView
import android.widget.Button
@ -21,6 +19,7 @@ import android.widget.TextView
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts.RequestMultiplePermissions
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.appcompat.app.AlertDialog
import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference
@ -36,6 +35,7 @@ import com.karumi.dexter.listener.PermissionRequest
import com.karumi.dexter.listener.multi.MultiplePermissionsListener
import fr.free.nrw.commons.R
import fr.free.nrw.commons.Utils
import fr.free.nrw.commons.activity.SingleWebViewActivity
import fr.free.nrw.commons.campaigns.CampaignView
import fr.free.nrw.commons.contributions.ContributionController
import fr.free.nrw.commons.contributions.MainActivity
@ -50,6 +50,7 @@ import fr.free.nrw.commons.recentlanguages.RecentLanguagesDao
import fr.free.nrw.commons.upload.LanguagesAdapter
import fr.free.nrw.commons.utils.DialogUtil
import fr.free.nrw.commons.utils.PermissionUtils
import fr.free.nrw.commons.utils.StringUtil
import fr.free.nrw.commons.utils.ViewUtil
import java.util.Locale
import javax.inject.Inject
@ -73,6 +74,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
@Inject
lateinit var locationManager: LocationServiceManager
private var vanishAccountPreference: Preference? = null
private var themeListPreference: ListPreference? = null
private var descriptionLanguageListPreference: Preference? = null
private var appUiLanguageListPreference: Preference? = null
@ -81,6 +83,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
private var recentLanguagesTextView: TextView? = null
private var separator: View? = null
private var languageHistoryListView: ListView? = null
private lateinit var inAppCameraLocationPermissionLauncher: ActivityResultLauncher<Array<String>>
private val GET_CONTENT_PICKER_HELP_URL = "https://commons-app.github.io/docs.html#get-content"
@ -116,6 +119,26 @@ class SettingsFragment : PreferenceFragmentCompat() {
themeListPreference = findPreference(Prefs.KEY_THEME_VALUE)
prepareTheme()
vanishAccountPreference = findPreference(Prefs.VANISHED_ACCOUNT)
vanishAccountPreference?.setOnPreferenceClickListener {
AlertDialog.Builder(requireContext())
.setTitle(R.string.account_vanish_request_confirm_title)
.setMessage(StringUtil.fromHtml(getString(R.string.account_vanish_request_confirm)))
.setNegativeButton(R.string.cancel){ dialog,_ ->
dialog.dismiss()
}
.setPositiveButton(R.string.vanish_account) { dialog, _ ->
SingleWebViewActivity.showWebView(
context = requireActivity(),
url = VANISH_ACCOUNT_URL,
successUrl = VANISH_ACCOUNT_SUCCESS_URL
)
dialog.dismiss()
}
.show()
true
}
val multiSelectListPref: MultiSelectListPreference? = findPreference(
Prefs.MANAGED_EXIF_TAGS
)
@ -131,7 +154,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
inAppCameraLocationPref?.setOnPreferenceChangeListener { _, newValue ->
val isInAppCameraLocationTurnedOn = newValue as Boolean
if (isInAppCameraLocationTurnedOn) {
createDialogsAndHandleLocationPermissions(requireActivity())
createDialogsAndHandleLocationPermissions()
}
true
}
@ -256,7 +279,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
*
* @param activity
*/
private fun createDialogsAndHandleLocationPermissions(activity: Activity) {
private fun createDialogsAndHandleLocationPermissions() {
inAppCameraLocationPermissionLauncher.launch(arrayOf(permission.ACCESS_FINE_LOCATION))
}
@ -284,7 +307,6 @@ class SettingsFragment : PreferenceFragmentCompat() {
return object : PreferenceGroupAdapter(preferenceScreen) {
override fun onBindViewHolder(holder: PreferenceViewHolder, position: Int) {
super.onBindViewHolder(holder, position)
val preference = getItem(position)
val iconFrame: View? = holder.itemView.findViewById(R.id.icon_frame)
iconFrame?.visibility = View.GONE
}
@ -487,7 +509,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
editor.apply()
}
@Suppress("LongLine")
companion object {
private const val VANISH_ACCOUNT_URL = "https://meta.m.wikimedia.org/wiki/Special:Contact/accountvanishapps"
private const val VANISH_ACCOUNT_SUCCESS_URL = "https://meta.m.wikimedia.org/wiki/Special:GlobalVanishRequest/vanished"
/**
* Create Locale based on different types of language codes
* @param languageCode

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.upload
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
@ -13,6 +16,7 @@ import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.facebook.imagepipeline.request.ImageRequest
import com.google.android.material.snackbar.Snackbar
import fr.free.nrw.commons.R
import fr.free.nrw.commons.contributions.Contribution
import java.io.File
@ -51,6 +55,24 @@ class FailedUploadsAdapter(
position: Int,
) {
val item: Contribution? = getItem(position)
val itemView = holder.itemView
val clipboardManager =
itemView.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
itemView.setOnLongClickListener {
val clip = ClipData.newPlainText(
itemView.context.getString(R.string.caption),
item?.media?.displayTitle
)
clipboardManager.setPrimaryClip(clip)
Snackbar.make(
itemView,
itemView.context.getString(R.string.caption_copied_to_clipboard),
Snackbar.LENGTH_SHORT
).show()
true
}
if (item != null) {
holder.titleTextView.setText(item.media.displayTitle)
}

View file

@ -49,9 +49,10 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
while ((bis.read(buffer).also { size = it }) > 0) {
buffers.add(
writeToFile(
buffer.copyOf(size),
buffer,
file.name ?: "",
getFileExt(file.name)
getFileExt(file.name),
size
)
)
}
@ -67,7 +68,7 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
* Create a temp file containing the passed byte data.
*/
@Throws(IOException::class)
private fun writeToFile(data: ByteArray, fileName: String, fileExtension: String): File {
private fun writeToFile(data: ByteArray, fileName: String, fileExtension: String, size: Int): File {
val file = File.createTempFile(fileName, fileExtension, context.cacheDir)
try {
if (!file.exists()) {
@ -75,7 +76,7 @@ class FileUtilsWrapper @Inject constructor(private val context: Context) {
}
FileOutputStream(file).use { fos ->
fos.write(data)
fos.write(data, 0, size)
}
} catch (throwable: Exception) {
Timber.e(throwable, "Failed to create file")

View file

@ -1,5 +1,8 @@
package fr.free.nrw.commons.upload
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context.CLIPBOARD_SERVICE
import android.net.Uri
import android.text.TextUtils
import android.view.LayoutInflater
@ -13,6 +16,7 @@ import androidx.paging.PagedListAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import com.facebook.imagepipeline.request.ImageRequest
import com.google.android.material.snackbar.Snackbar
import fr.free.nrw.commons.R
import fr.free.nrw.commons.contributions.Contribution
import java.io.File
@ -99,6 +103,22 @@ class PendingUploadsAdapter(
fun bind(contribution: Contribution) {
titleTextView.text = contribution.media.displayTitle
val clipboardManager =
itemView.context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
itemView.setOnLongClickListener {
val clip = ClipData.newPlainText(
itemView.context.getString(R.string.caption),
titleTextView.text
)
clipboardManager.setPrimaryClip(clip)
Snackbar.make(
itemView,
itemView.context.getString(R.string.caption_copied_to_clipboard),
Snackbar.LENGTH_SHORT
).show()
true
}
val imageSource: String = contribution.localUri.toString()
var imageRequest: ImageRequest? = null