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

@ -50,6 +50,8 @@ dependencies {
implementation "com.google.android.material:material:1.12.0"
implementation 'com.karumi:dexter:5.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.compose.ui:ui-tooling-preview'
androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
// Jetpack Compose
def composeBom = platform('androidx.compose:compose-bom:2024.11.00')
@ -87,6 +89,8 @@ dependencies {
// Dependency injector
implementation "com.google.dagger:dagger-android:$DAGGER_VERSION"
implementation "com.google.dagger:dagger-android-support:$DAGGER_VERSION"
debugImplementation 'androidx.compose.ui:ui-tooling'
debugImplementation 'androidx.compose.ui:ui-test-manifest'
kapt "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
annotationProcessor "com.google.dagger:dagger-android-processor:$DAGGER_VERSION"

View file

@ -55,6 +55,10 @@
android:theme="@style/LightAppTheme"
tools:ignore="GoogleAppIndexingWarning"
tools:replace="android:appComponentFactory">
<activity
android:name=".activity.SingleWebViewActivity"
android:exported="false"
android:label="@string/title_activity_single_web_view" />
<activity
android:name=".nearby.WikidataFeedback"
android:exported="false" />

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

View file

@ -12,7 +12,7 @@
android:layout_width="@dimen/landscape_width"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="@dimen/small_gap">
android:layout_marginTop="@dimen/login_gap">
<androidx.cardview.widget.CardView
android:layout_width="match_parent"

View file

@ -2,12 +2,14 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:layout_gravity="center_vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
android:orientation="vertical"
android:layout_gravity="center_vertical">
<FrameLayout
android:layout_width="@dimen/landscape_width"

View file

@ -1,13 +1,15 @@
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:layout_gravity="center_vertical">
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:fillViewport="true"
android:orientation="vertical">
android:orientation="vertical"
android:layout_gravity="center_vertical">
<FrameLayout
android:layout_width="wrap_content"

View file

@ -875,4 +875,6 @@
<string name="usages_on_commons_heading">كومنز</string>
<string name="usages_on_other_wikis_heading">مواقع ويكي أخرى</string>
<string name="file_usages_container_heading">حالات استخدام الملف</string>
<string name="caption">الشرح</string>
<string name="caption_copied_to_clipboard">تم نسخ التسمية التوضيحية إلى الحافظة</string>
</resources>

View file

@ -56,8 +56,8 @@
<string name="login_success">Uğurlu giriş!</string>
<string name="login_failed">Giriş baş tutmadı!</string>
<string name="upload_failed">Fayl tapılmadı. Xahiş edirik başqa bir fayl üzərində cəhd edin.</string>
<string name="retry_limit_reached">Maksimum təkrar cəhd limitinə çatdınız! Yükləməni ləğv edin və yenidən cəhd edin</string>
<string name="unrestricted_battery_mode">Batareya optimallaşdırılmasını söndürmək?</string>
<string name="retry_limit_reached">Maksimum təkrar cəhd limitinə çatdınız! Zəhmət olmasa, yükləməni ləğv edin və yenidən cəhd edin</string>
<string name="unrestricted_battery_mode">Batareya optimallaşdırılması söndürülsün?</string>
<string name="authentication_failed" fuzzy="true">Doğrulama alınmadı, xahiş edirəm yenidən daxil olun</string>
<string name="uploading_started">Yükləmə başladı!</string>
<string name="upload_completed_notification_title">%1$s yükləndi!</string>

View file

@ -529,4 +529,6 @@
<string name="uploads">আপলোড</string>
<string name="pending">অমীমাংসিত</string>
<string name="failed">ব্যর্থ হয়েছে</string>
<string name="caption">ক্যাপশন</string>
<string name="caption_copied_to_clipboard">ক্যাপশন ক্লিপবোর্ডে অনুলিপি করা হয়েছে</string>
</resources>

View file

@ -814,4 +814,6 @@
<string name="usages_on_other_wikis_heading">Andre wikier</string>
<string name="bullet_point"></string>
<string name="file_usages_container_heading">Filanvendelser</string>
<string name="caption">Billedtekst</string>
<string name="caption_copied_to_clipboard">Billedtekst kopieret til udklipsholder</string>
</resources>

View file

@ -32,6 +32,7 @@
* Orikrin1998
* Paraboule
* Patrick Star
* Pols12
* Robins7
* Sherbrooke
* SleaY
@ -852,4 +853,6 @@
<string name="usages_on_other_wikis_heading">Autres wikis</string>
<string name="bullet_point"></string>
<string name="file_usages_container_heading">Utilisations du fichier</string>
<string name="caption">Légende</string>
<string name="caption_copied_to_clipboard">Légende copiée dans le presse-papier</string>
</resources>

View file

@ -358,4 +358,5 @@
<string name="custom_selector_delete_folder">फ़ोल्डर हटाएँ</string>
<string name="custom_selector_delete">हटाएँ</string>
<string name="custom_selector_cancel">रद्द करें</string>
<string name="caption">कैप्शन</string>
</resources>

View file

@ -100,6 +100,8 @@
<string name="menu_from_camera">Prender photo</string>
<string name="menu_nearby">A proximitate</string>
<string name="provider_contributions">Mi incargamentos</string>
<string name="menu_copy_link">Copiar ligamine</string>
<string name="menu_link_copied">Le ligamine ha essite copiate al area de transferentia</string>
<string name="menu_share">Condivider</string>
<string name="menu_view_file_page">Visitar le pagina del file</string>
<string name="share_title_hint">Legenda (obligatori)</string>
@ -118,6 +120,7 @@
<string name="categories_search_text_hint">Cercar categorias</string>
<string name="depicts_search_text_hint">Cerca elementos que tu file representa (montania, Taj Mahal, etc.)</string>
<string name="menu_save_categories">Salveguardar</string>
<string name="menu_overflow_desc">Menu de disbordamento</string>
<string name="refresh_button">Refrescar</string>
<string name="display_list_button">Listar</string>
<string name="contributions_subtitle_zero">(Nihil incargate ancora)</string>
@ -269,6 +272,7 @@
<string name="copy_wikicode">Copiar le wikitexto al area de transferentia</string>
<string name="wikicode_copied">Le wikitexto ha essite copiate al area de transferentia</string>
<string name="nearby_location_not_available">“A proximitate” poterea non functionar perque le localisation non es disponibile.</string>
<string name="nearby_showing_pins_offline">Internet indisponibile. Appare solmente le locos in cache.</string>
<string name="upload_location_access_denied">Le accesso al localisation ha essite refusate. Per favor indica tu localisation manualmente pro usar iste function.</string>
<string name="location_permission_rationale_nearby">Permission necessari pro monstrar un lista de locos a proximitate</string>
<string name="location_permission_rationale_explore">Permission necessari pro monstrar un lista de imagines a proximitate</string>
@ -352,11 +356,13 @@
<string name="delete">Deler</string>
<string name="Achievements">Realisationes</string>
<string name="Profile">Profilo</string>
<string name="badges">Insignias</string>
<string name="statistics">Statisticas</string>
<string name="statistics_thanks">Regratiamentos recipite</string>
<string name="statistics_featured">Imagines eminente</string>
<string name="statistics_wikidata_edits">Imagines via “Locos a proximitate”</string>
<string name="level" fuzzy="true">Nivello</string>
<string name="level">Nivello %d</string>
<string name="profileLevel">%s (Nivello %s)</string>
<string name="images_uploaded">Imagines incargate</string>
<string name="image_reverts">Imagines non revertite</string>
<string name="images_used_by_wiki">Imagines usate</string>
@ -388,6 +394,7 @@
<string name="map_application_missing">Necun application cartographic compatibile pote esser trovate sur tu apparato. Per favor installa un application cartographic pro usar iste function.</string>
<string name="title_page_bookmarks_pictures">Imagines</string>
<string name="title_page_bookmarks_locations">Locos</string>
<string name="title_page_bookmarks_categories">Categorias</string>
<string name="menu_bookmark">Adder al/Remover del marcapaginas</string>
<string name="provider_bookmarks">Marcapaginas</string>
<string name="bookmark_empty">Tu non ha addite alcun marcapagina</string>
@ -469,6 +476,7 @@
<string name="no_notification">Tu non ha notificationes non legite</string>
<string name="no_read_notification">Tu non ha notificationes legite</string>
<string name="share_logs_using">Condivider registros usante</string>
<string name="check_your_email_inbox">Consulta tu cassa de entrata</string>
<string name="menu_option_read">Vider legites</string>
<string name="menu_option_unread">Vider non legites</string>
<string name="error_occurred_in_picking_images">Un error ha occurrite durante le selection de imagines</string>
@ -776,4 +784,21 @@
<string name="pending">Pendente</string>
<string name="failed">Fallite</string>
<string name="could_not_load_place_data">Non poteva cargar le datos del loco</string>
<string name="custom_selector_delete_folder">Deler dossier</string>
<string name="custom_selector_confirm_deletion_title">Confirmar deletion</string>
<string name="custom_selector_confirm_deletion_message">Es tu secur de voler deler le dossier %1$s que contine %2$d objectos?</string>
<string name="custom_selector_delete">Deler</string>
<string name="custom_selector_cancel">Cancellar</string>
<string name="custom_selector_folder_deleted_success">Le dossier %1$s ha essite delite</string>
<string name="custom_selector_folder_deleted_failure">Le dossier %1$s non ha potite esser delite</string>
<string name="custom_selector_error_trashing_folder_contents">Error durante le elimination del contento del dossier: %1$s</string>
<string name="custom_selector_folder_not_found_error">Non poteva recuperar le percurso al dossier pro le “bucket ID” %1$d</string>
<string name="red_pin">Iste loco non ha ancora un photo. Proque non prender un?</string>
<string name="green_pin">Iste loco ja ha un photo.</string>
<string name="grey_pin">Ora se verifica si iste loco ha un photo.</string>
<string name="error_while_loading">Error durante le cargamento</string>
<string name="no_usages_found">Necun uso trovate</string>
<string name="usages_on_commons_heading">Commons</string>
<string name="usages_on_other_wikis_heading">Altere wikis</string>
<string name="file_usages_container_heading">Usos del file</string>
</resources>

View file

@ -773,4 +773,6 @@
<string name="usages_on_commons_heading">Commons</string>
<string name="usages_on_other_wikis_heading">Altri wiki</string>
<string name="file_usages_container_heading">Utilizzi del file</string>
<string name="caption">Didascalia</string>
<string name="caption_copied_to_clipboard">Didascalia copiata negli appunti</string>
</resources>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* GilPe
* Les Meloures
* Robby
* Soued031
@ -532,4 +533,6 @@
<string name="error_while_loading">Feeler beim Lueden</string>
<string name="usages_on_commons_heading">Commons</string>
<string name="usages_on_other_wikis_heading">Aner Wikien</string>
<string name="caption">Beschrëftung</string>
<string name="caption_copied_to_clipboard">Text an den Tëschespäicher kopéiert</string>
</resources>

View file

@ -810,4 +810,6 @@
<string name="usages_on_other_wikis_heading">Други викија</string>
<string name="bullet_point"></string>
<string name="file_usages_container_heading">Употреби на податотеката</string>
<string name="caption">Толкување</string>
<string name="caption_copied_to_clipboard">Толкувањето е ставено во меѓускладот</string>
</resources>

View file

@ -1,6 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Authors:
* A67-A67
* ABPMAB
* Bouman4
* Ciell
* Dutchy45
@ -299,6 +300,7 @@
<string name="copy_wikicode">Kopieer de wikitekst naar het klembord</string>
<string name="wikicode_copied">De wikitekst is gekopieerd naar het klembord</string>
<string name="nearby_location_not_available">In de buurt werkt mogelijk niet naar behoren. Locatie niet beschikbaar.</string>
<string name="nearby_showing_pins_offline">Internet niet beschikbaar. Alleen gecachte plaatsen worden getoond.</string>
<string name="upload_location_access_denied">Locatietoegang geweigerd. Stel uw locatie handmatig in om deze functie te gebruiken.</string>
<string name="location_permission_rationale_nearby">Toestemming vereist om een lijst van nabije plaatsen weer te geven</string>
<string name="location_permission_rationale_explore">Toestemming vereist om een lijst van nabije afbeeldingen weer te geven</string>

View file

@ -119,7 +119,7 @@
<string name="license_name_cc_by_4_0">CC BY 4.0</string>
<string name="tutorial_1_text">ਵਿਕੀਮੀਡੀਆ ਕਾਮਨਜ਼ ਜ਼ਿਆਦਾਤਰ ਉਹ ਤਸਵੀਰਾਂ ਦਾ ਭੰਡਾਰ ਹੈ ਜੋ ਵਿਕੀਪੀਡੀਆ \'ਤੇ ਵਰਤੀਆਂ ਜਾਂਦੀਆਂ ਹਨ।</string>
<string name="tutorial_1_subtext">ਤੁਹਾਡੀਆਂ ਤਸਵੀਰਾਂ ਵਿਸ਼ਵ ਦੇ ਬਾਕੀ ਲੋਕਾਂ ਨੂੰ ਸਿੱਖਿਅਤ ਕਰਨ ਲਈ ਸਹਾਈ ਹਨ!</string>
<string name="tutorial_2_text">ਕਿਰਪਾ ਕਰਕੇ ਉਹ ਤਸਵੀਰਾਂ ਅਪਲੋਡ ਕਰੋ ਜੋ ਤੁਹਾਡੇ ਦੁਆਰਾ ਲਈਆਂ ਗਈਆਂ ਹਨ ਜਾਂ ਬਣਾਈਆਂ ਗਈਆਂ ਹਨ:</string>
<string name="tutorial_2_text">ਕਿਰਪਾ ਕਰਕੇ ਉਹ ਤਸਵੀਰਾਂ ਨੂੰ ਚੜ੍ਹਾਉ ਜੋ ਤੁਹਾਡੇ ਵੱਲੋਂ ਲਈਆਂ ਗਈਆਂ ਹਨ ਜਾਂ ਬਣਾਈਆਂ ਗਈਆਂ ਹਨ:</string>
<string name="tutorial_3_text">ਕਿਰਪਾ ਕਰਕੇ ਅਪਲੋਡ ਨਾ ਕਰੋ:</string>
<string name="tutorial_4_text">ਉਦਾਹਰਣ ਵਜੋਂ ਇਹ ਅਪਲੋਡ:</string>
<string name="welcome_wikipedia_text">ਆਪਣੀਆਂ ਤਸਵੀਰਾਂ ਦਾ ਯੋਗਦਾਨ ਪਾਓ। ਵਿਕੀਪੀਡੀਆ ਲੇਖਾਂ ਨੂੰ ਸੁਰਜੀਤ ਕਰ ਦਿਓ!</string>
@ -160,7 +160,7 @@
<string name="navigation_item_upload">ਚੜ੍ਹਾਉ</string>
<string name="navigation_item_nearby">ਨੇੜੇ-ਤੇੜੇ</string>
<string name="navigation_item_about">ਬਾਰੇ</string>
<string name="navigation_item_settings">ਸੈਟਿੰਗਾਂ</string>
<string name="navigation_item_settings">ਤਰਜੀਹਾਂ</string>
<string name="navigation_item_feedback">ਸੁਝਾਅ</string>
<string name="navigation_item_logout">ਬਾਹਰ ਆਉ</string>
<string name="navigation_item_info">ਸਿਖਲਾਈ</string>

View file

@ -806,4 +806,6 @@
<string name="usages_on_other_wikis_heading">Àutre wiki</string>
<string name="bullet_point"></string>
<string name="file_usages_container_heading">Usagi dl\'archivi</string>
<string name="caption">Legenda</string>
<string name="caption_copied_to_clipboard">Legenda copià an sla taulëtta</string>
</resources>

View file

@ -144,7 +144,7 @@
<string name="welcome_help_button_text">Lassedieđut</string>
<string name="detail_panel_cats_label">Kategoriijat</string>
<string name="detail_panel_cats_loading">Luđeme...</string>
<string name="detail_panel_cats_none">Ii guhtege válljejuvvon</string>
<string name="detail_panel_cats_none">Ii oktage válljejuvvon</string>
<string name="detail_caption_empty">Ii leat govvateaksta</string>
<string name="detail_description_empty">Ii gávdno govvádus</string>
<string name="detail_discussion_empty">Ii digaštallojuvvo</string>
@ -217,6 +217,9 @@
<string name="next">Čuovvovaš</string>
<string name="title_page_bookmarks_pictures">Govat</string>
<string name="title_page_bookmarks_locations">Sajit</string>
<string name="title_page_bookmarks_categories">Kategoriijat</string>
<string name="provider_bookmarks">Girjemearkkat</string>
<string name="provider_bookmarks_location">Girjemearkkat</string>
<string name="no_categories_selected">Ii oktage kategoriija leat válljejuvvon</string>
<string name="no_depictions_selected">Ii oktage govvádus leat válljejuvvon</string>
<string name="back_button_warning">Gaskkalduhte fiilla vurkema</string>
@ -232,9 +235,11 @@
<string name="caption_edit_helper_edit_message_else">Govvateavstta lasiheapmi ii lihkostuvvan.</string>
<string name="description_activity_title">Rievdat govvádusaid ja govvateavsttaid</string>
<string name="nearby_search_hint">Šaldi, musea, hotealla jna.</string>
<string name="title_app_shortcut_bookmark">Girjemearkkat</string>
<string name="load_more">Luđe lasi</string>
<string name="add_picture_to_wikipedia_article_title">Lasit gova Wikipediai</string>
<string name="resume">joatkke</string>
<string name="bookmarks">Girjemearkkat</string>
<string name="leaderboard_column_user">Geavaheaddji</string>
<string name="cancelling_upload">Fiilla vurkema gaskkalduhttomin…</string>
<string name="license_step_title">Medialiseansa</string>

View file

@ -277,4 +277,5 @@
<string name="custom_selector_delete">مٹاؤ</string>
<string name="custom_selector_cancel">منسوخ</string>
<string name="usages_on_commons_heading">کامنز</string>
<string name="caption">عنوان</string>
</resources>

View file

@ -743,4 +743,6 @@
<string name="failed">Није успело</string>
<string name="green_pin">Ово место већ има слику</string>
<string name="grey_pin">Проверавање да ли ово место има слику.</string>
<string name="caption">Поднапис</string>
<string name="caption_copied_to_clipboard">Поднапис копиран</string>
</resources>

View file

@ -154,6 +154,8 @@
<string name="menu_from_camera">拍照</string>
<string name="menu_nearby">附近</string>
<string name="provider_contributions">我的上传</string>
<string name="menu_copy_link">复制链接</string>
<string name="menu_link_copied">链接已复制到剪贴板。</string>
<string name="menu_share">分享</string>
<string name="menu_view_file_page">查看文件页面</string>
<string name="share_title_hint">说明(必需)</string>
@ -329,6 +331,7 @@
<string name="copy_wikicode">复制wikitext到剪贴板</string>
<string name="wikicode_copied">wikitext已经复制到剪贴板</string>
<string name="nearby_location_not_available">附近可能无法正常工作,位置不可用。</string>
<string name="nearby_showing_pins_offline">互联网不可用。仅显示缓存的位置。</string>
<string name="upload_location_access_denied">位置权限被拒绝。请手动设置您的位置以使用此功能。</string>
<string name="location_permission_rationale_nearby">需要权限以显示附近地点列表</string>
<string name="location_permission_rationale_explore">需要权限以显示附近图片列表</string>
@ -412,11 +415,13 @@
<string name="delete">删除</string>
<string name="Achievements">成就</string>
<string name="Profile">个人资料</string>
<string name="badges">徽章</string>
<string name="statistics">统计</string>
<string name="statistics_thanks">已收到感谢</string>
<string name="statistics_featured">特色图片</string>
<string name="statistics_wikidata_edits">来自“附近地点”的图片</string>
<string name="level" fuzzy="true">等级</string>
<string name="level">等级%d</string>
<string name="profileLevel">%s%s級</string>
<string name="images_uploaded">已上传图片</string>
<string name="image_reverts">未还原图片</string>
<string name="images_used_by_wiki">使用过的图片</string>
@ -448,6 +453,7 @@
<string name="map_application_missing">您的设备上无法找到兼容的地图应用程序。请安装地图应用程序来使用此功能。</string>
<string name="title_page_bookmarks_pictures">图片</string>
<string name="title_page_bookmarks_locations">方位</string>
<string name="title_page_bookmarks_categories">分类</string>
<string name="menu_bookmark">添加/删除书签</string>
<string name="provider_bookmarks">书签</string>
<string name="bookmark_empty">您还未加入任何书签</string>
@ -850,4 +856,8 @@
<string name="red_pin">这个地点还没有照片,快去拍一张吧!</string>
<string name="green_pin">这个地点已经有照片了。</string>
<string name="grey_pin">现在检查这个地点是否有照片。</string>
<string name="error_while_loading">加载时出错</string>
<string name="usages_on_commons_heading">维基共享资源</string>
<string name="usages_on_other_wikis_heading">其它wiki</string>
<string name="file_usages_container_heading">文件用途</string>
</resources>

View file

@ -20,6 +20,7 @@
<dimen name="gigantic_gap">64dp</dimen>
<dimen name="standard_gap">16dp</dimen>
<dimen name="small_gap">8dp</dimen>
<dimen name="login_gap">20dp</dimen>
<dimen name="small_height">7dp</dimen>
<dimen name="tiny_gap">4dp</dimen>
<dimen name="very_tiny_gap">2dp</dimen>

View file

@ -860,5 +860,12 @@ Upload your first media by tapping on the add button.</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>
<string name="title_activity_single_web_view">SingleWebViewActivity</string>
<string name="account">Account</string>
<string name="vanish_account">Vanish Account</string>
<string name="account_vanish_request_confirm_title">Vanish account warning</string>
<string name="account_vanish_request_confirm"><![CDATA[Vanishing is a <b>last resort</b> and should <b>only be used when you wish to stop editing forever</b> and also to hide as many of your past associations as possible.<br/><br/>Account deletion on Wikimedia Commons is done by changing your account name to make it so others cannot recognize your contributions in a process called account vanishing. <b>Vanishing does not guarantee complete anonymity or remove contributions to the projects</b>.]]></string>
<string name="caption">Caption</string>
<string name="caption_copied_to_clipboard">Caption copied to clipboard</string>
</resources>

View file

@ -129,4 +129,14 @@
android:title="@string/send_log_file" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/account">
<Preference
android:key="vanishAccount"
app:singleLineTitle="false"
android:title="@string/vanish_account"/>
</PreferenceCategory>
</PreferenceScreen>

View file

@ -21,7 +21,7 @@ KOTLIN_VERSION=1.9.22
LEAK_CANARY_VERSION=2.10
DAGGER_VERSION=2.23
ROOM_VERSION=2.6.1
PREFERENCE_VERSION=1.1.0
PREFERENCE_VERSION=1.2.1
CORE_KTX_VERSION=1.9.0
ADAPTER_DELEGATES_VERSION=4.3.0
PAGING_VERSION=2.1.2