diff --git a/app/build.gradle b/app/build.gradle index 14bf5f3b7..f50a4e6dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ab2edf719..e7c64f929 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -55,6 +55,10 @@ android:theme="@style/LightAppTheme" tools:ignore="GoogleAppIndexingWarning" tools:replace="android:appComponentFactory"> + diff --git a/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt new file mode 100644 index 000000000..284c84caf --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/activity/SingleWebViewActivity.kt @@ -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(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) + } + } +} + diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java b/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java deleted file mode 100644 index c1c8fac18..000000000 --- a/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.java +++ /dev/null @@ -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(); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.kt b/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.kt new file mode 100644 index 000000000..378a98893 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/BackgroundPoolExceptionHandler.kt @@ -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() + } + } +} diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.java deleted file mode 100644 index 80931b1c1..000000000 --- a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.kt b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.kt new file mode 100644 index 000000000..0efe057f2 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionAwareThreadPoolExecutor.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.java deleted file mode 100644 index 38690305a..000000000 --- a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.java +++ /dev/null @@ -1,7 +0,0 @@ -package fr.free.nrw.commons.concurrency; - -import androidx.annotation.NonNull; - -public interface ExceptionHandler { - void onException(@NonNull Throwable t); -} diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.kt b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.kt new file mode 100644 index 000000000..6b3d2a0f7 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ExceptionHandler.kt @@ -0,0 +1,7 @@ +package fr.free.nrw.commons.concurrency + +interface ExceptionHandler { + + fun onException(t: Throwable) + +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.java b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.java deleted file mode 100644 index f057f61b2..000000000 --- a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.java +++ /dev/null @@ -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 ScheduledFuture schedule(Callable 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); - } - } -} diff --git a/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.kt b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.kt new file mode 100644 index 000000000..46138d676 --- /dev/null +++ b/app/src/main/java/fr/free/nrw/commons/concurrency/ThreadPoolService.kt @@ -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 schedule(callable: Callable, time: Long, timeUnit: TimeUnit): ScheduledFuture { + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt index 13e8efb57..1bd60efd7 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/Prefs.kt @@ -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" diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt index da79244bc..91c88d7b0 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsActivity.kt @@ -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 diff --git a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt index 36528a919..2f293937c 100644 --- a/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt +++ b/app/src/main/java/fr/free/nrw/commons/settings/SettingsFragment.kt @@ -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> 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 diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt index fc57ffe41..5b3eb5140 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FailedUploadsAdapter.kt @@ -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) } diff --git a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt index aa1f8aed6..68a3cb362 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/FileUtilsWrapper.kt @@ -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") diff --git a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt index 8edfcb472..7f0b8aba1 100644 --- a/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt +++ b/app/src/main/java/fr/free/nrw/commons/upload/PendingUploadsAdapter.kt @@ -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 diff --git a/app/src/main/res/layout-land/activity_login.xml b/app/src/main/res/layout-land/activity_login.xml index de281c1f5..b9adfd033 100644 --- a/app/src/main/res/layout-land/activity_login.xml +++ b/app/src/main/res/layout-land/activity_login.xml @@ -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"> + android:layout_height="match_parent" + android:layout_gravity="center_vertical"> + android:orientation="vertical" + android:layout_gravity="center_vertical"> + android:layout_height="wrap_content" + android:layout_gravity="center_vertical"> + android:orientation="vertical" + android:layout_gravity="center_vertical"> كومنز مواقع ويكي أخرى حالات استخدام الملف + الشرح + تم نسخ التسمية التوضيحية إلى الحافظة diff --git a/app/src/main/res/values-az/strings.xml b/app/src/main/res/values-az/strings.xml index 6f55437dc..cbe23481c 100644 --- a/app/src/main/res/values-az/strings.xml +++ b/app/src/main/res/values-az/strings.xml @@ -56,8 +56,8 @@ Uğurlu giriş! Giriş baş tutmadı! Fayl tapılmadı. Xahiş edirik başqa bir fayl üzərində cəhd edin. - Maksimum təkrar cəhd limitinə çatdınız! Yükləməni ləğv edin və yenidən cəhd edin - Batareya optimallaşdırılmasını söndürmək? + 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 + Batareya optimallaşdırılması söndürülsün? Doğrulama alınmadı, xahiş edirəm yenidən daxil olun Yükləmə başladı! %1$s yükləndi! diff --git a/app/src/main/res/values-bn/strings.xml b/app/src/main/res/values-bn/strings.xml index d9ee82fcf..a7a3da9a7 100644 --- a/app/src/main/res/values-bn/strings.xml +++ b/app/src/main/res/values-bn/strings.xml @@ -529,4 +529,6 @@ আপলোড অমীমাংসিত ব্যর্থ হয়েছে + ক্যাপশন + ক্যাপশন ক্লিপবোর্ডে অনুলিপি করা হয়েছে diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 1f24266f0..20c9b662c 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -814,4 +814,6 @@ Andre wikier Filanvendelser + Billedtekst + Billedtekst kopieret til udklipsholder diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 71f39baf4..07021189f 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -32,6 +32,7 @@ * Orikrin1998 * Paraboule * Patrick Star +* Pols12 * Robins7 * Sherbrooke * SleaY @@ -852,4 +853,6 @@ Autres wikis Utilisations du fichier + Légende + Légende copiée dans le presse-papier diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 69bea9079..152edac1c 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -358,4 +358,5 @@ फ़ोल्डर हटाएँ हटाएँ रद्द करें + कैप्शन diff --git a/app/src/main/res/values-ia/strings.xml b/app/src/main/res/values-ia/strings.xml index 7ec1b9834..f12022169 100644 --- a/app/src/main/res/values-ia/strings.xml +++ b/app/src/main/res/values-ia/strings.xml @@ -100,6 +100,8 @@ Prender photo A proximitate Mi incargamentos + Copiar ligamine + Le ligamine ha essite copiate al area de transferentia Condivider Visitar le pagina del file Legenda (obligatori) @@ -118,6 +120,7 @@ Cercar categorias Cerca elementos que tu file representa (montania, Taj Mahal, etc.) Salveguardar + Menu de disbordamento Refrescar Listar (Nihil incargate ancora) @@ -269,6 +272,7 @@ Copiar le wikitexto al area de transferentia Le wikitexto ha essite copiate al area de transferentia “A proximitate” poterea non functionar perque le localisation non es disponibile. + Internet indisponibile. Appare solmente le locos in cache. Le accesso al localisation ha essite refusate. Per favor indica tu localisation manualmente pro usar iste function. Permission necessari pro monstrar un lista de locos a proximitate Permission necessari pro monstrar un lista de imagines a proximitate @@ -352,11 +356,13 @@ Deler Realisationes Profilo + Insignias Statisticas Regratiamentos recipite Imagines eminente Imagines via “Locos a proximitate” - Nivello + Nivello %d + %s (Nivello %s) Imagines incargate Imagines non revertite Imagines usate @@ -388,6 +394,7 @@ Necun application cartographic compatibile pote esser trovate sur tu apparato. Per favor installa un application cartographic pro usar iste function. Imagines Locos + Categorias Adder al/Remover del marcapaginas Marcapaginas Tu non ha addite alcun marcapagina @@ -469,6 +476,7 @@ Tu non ha notificationes non legite Tu non ha notificationes legite Condivider registros usante + Consulta tu cassa de entrata Vider legites Vider non legites Un error ha occurrite durante le selection de imagines @@ -776,4 +784,21 @@ Pendente Fallite Non poteva cargar le datos del loco + Deler dossier + Confirmar deletion + Es tu secur de voler deler le dossier %1$s que contine %2$d objectos? + Deler + Cancellar + Le dossier %1$s ha essite delite + Le dossier %1$s non ha potite esser delite + Error durante le elimination del contento del dossier: %1$s + Non poteva recuperar le percurso al dossier pro le “bucket ID” %1$d + Iste loco non ha ancora un photo. Proque non prender un? + Iste loco ja ha un photo. + Ora se verifica si iste loco ha un photo. + Error durante le cargamento + Necun uso trovate + Commons + Altere wikis + Usos del file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 675082826..fc601ec2f 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -773,4 +773,6 @@ Commons Altri wiki Utilizzi del file + Didascalia + Didascalia copiata negli appunti diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml index 02ecff5eb..bc39d6c65 100644 --- a/app/src/main/res/values-lb/strings.xml +++ b/app/src/main/res/values-lb/strings.xml @@ -1,5 +1,6 @@