From 38b4fb78f9937830ffd180e32180b5770328f2e6 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 10 Apr 2026 14:54:35 -0300 Subject: [PATCH 01/38] Change how app details updates the PackageInfo Previously we were re-loading the PackageInfo ourselves when we knew there should be an update. However, the problem was that we hadn't always known. So now we simply observe the cached PackageInfo from the AppInstalledAppsCache which so far as been a reliable source of truth. --- .../fdroid/ui/details/AppDetailsViewModel.kt | 45 +++++++++---------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index 01ae0b543..f1cde9358 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -3,11 +3,10 @@ package org.fdroid.ui.details import android.app.Application import android.content.Intent import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import android.content.pm.PackageManager.GET_SIGNATURES import androidx.activity.result.ActivityResult import androidx.annotation.UiThread import androidx.annotation.WorkerThread +import androidx.core.content.pm.PackageInfoCompat.getLongVersionCode import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.application import androidx.lifecycle.viewModelScope @@ -25,9 +24,10 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import mu.KotlinLogging import org.fdroid.UpdateChecker import org.fdroid.database.AppMetadata @@ -40,6 +40,7 @@ import org.fdroid.index.RepoManager import org.fdroid.index.v2.PackageVersion import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallState +import org.fdroid.install.InstalledAppsCache import org.fdroid.repo.RepoPreLoader import org.fdroid.settings.SettingsManager import org.fdroid.updates.UpdatesManager @@ -61,6 +62,7 @@ constructor( private val networkMonitor: NetworkMonitor, private val settingsManager: SettingsManager, private val appInstallManager: AppInstallManager, + private val installedAppsCache: InstalledAppsCache, ) : AndroidViewModel(app) { private val log = KotlinLogging.logger {} private val packageInfoFlow = MutableStateFlow(null) @@ -89,18 +91,20 @@ constructor( } init { - viewModelScope.launch(dispatcher) { loadPackageInfoFlow() } + viewModelScope.launch(dispatcher) { + // Collect the cached packageInfo for this app. This way we have a central place + // to react to state changes for installs, updates and uninstalls + installedAppsCache.installedApps + .map { cacheMap -> cacheMap[packageName]?.let { getLongVersionCode(it) } } + .distinctUntilChanged() + .collect { loadPackageInfoFlow() } + } } @WorkerThread private fun loadPackageInfoFlow() { val packageManager = app.packageManager - val packageInfo = - try { - @Suppress("DEPRECATION") packageManager.getPackageInfo(packageName, GET_SIGNATURES) - } catch (_: PackageManager.NameNotFoundException) { - null - } + val packageInfo = installedAppsCache.installedApps.value[packageName] packageInfoFlow.value = if (packageInfo == null) { AppInfo(packageName) @@ -123,15 +127,15 @@ constructor( packageName = packageName, appMetadata = appMetadata, version = version, - currentVersionName = packageInfoFlow.value?.packageInfo?.versionName, // TODO + currentVersionName = packageInfoFlow.value?.packageInfo?.versionName, repo = repoManager.getRepository(appMetadata.repoId) ?: return@launch, iconModel = iconModel, canAskPreApprovalNow = true, ) - if (result is InstallState.Installed) { - // to reload packageInfoFlow with fresh packageInfo - loadPackageInfoFlow() - } + log.info { "Install result was: $result" } + // We are not doing anything with that result, + // because an installation could have been triggered via 'Update all' in My Apps as well. + // So updating states need to happen elsewhere. } } @@ -139,11 +143,7 @@ constructor( fun requestUserConfirmation(installState: InstallState.UserConfirmationNeeded) { scope.launch(Dispatchers.Main) { val result = appInstallManager.requestUserConfirmation(packageName, installState) - if (result is InstallState.Installed) - withContext(Dispatchers.Main) { - // to reload packageInfoFlow with fresh packageInfo - loadPackageInfoFlow() - } + log.info { "requestUserConfirmation result was: $result" } } } @@ -164,10 +164,7 @@ constructor( fun onUninstallResult(activityResult: ActivityResult) { val name = appDetails.value?.name val result = appInstallManager.onUninstallResult(packageName, name, activityResult) - if (result is InstallState.Uninstalled) { - // to reload packageInfoFlow with fresh packageInfo - viewModelScope.launch(dispatcher) { loadPackageInfoFlow() } - } + log.info { "Uninstall result was: $result" } } @UiThread From d8b41da9bdcb0c0eaa1cdcfdef155a5710fdcf34 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 10 Apr 2026 15:28:48 -0300 Subject: [PATCH 02/38] disable proguard optimization again to see if this is really worth having and in any case save the mapping.txt file --- .gitlab-ci.yml | 1 + app/proguard-rules.pro | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 82bdcb1d6..ce2dbcb54 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -103,6 +103,7 @@ app assembleRelease test: paths: - app/build/reports - app/build/outputs/apk + - app/build/outputs/mapping - libs/*/build/reports - legacy/build/reports - legacy/build/outputs/apk diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 2d269191a..0d81ad1a6 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -1,4 +1,5 @@ -dontobfuscate +-dontoptimize -keepattributes SourceFile,LineNumberTable,Exceptions # Anything less causes issues like not finding primary constructor in ReflectionDiffer From ebddb760cfa3acb436262627ad869bf6d955698f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 13 Apr 2026 12:04:56 -0300 Subject: [PATCH 03/38] Don't crash when system sends us duplicate installation broadcasts --- .../fdroid/install/SessionInstallManager.kt | 72 +++++++++++++++---- .../install/SessionInstallManagerTest.kt | 56 ++++++++++++--- 2 files changed, 104 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt index ea9b9edca..1495e8a9e 100644 --- a/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt +++ b/app/src/main/kotlin/org/fdroid/install/SessionInstallManager.kt @@ -25,6 +25,7 @@ import androidx.core.content.ContextCompat.registerReceiver import androidx.core.os.LocaleListCompat import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import javax.inject.Singleton import kotlin.coroutines.resume @@ -122,7 +123,7 @@ constructor( app: AppMetadata, icon: Bitmap?, canRequestUserConfirmationNow: Boolean, - ): PreApprovalResult = suspendCancellableCoroutine { cont -> + ): PreApprovalResult = safeSuspendCoroutine { cont -> val params = getSessionParams(app.packageName) val sessionId = installer.createSession(params) log.info { "Opened session $sessionId for ${app.packageName}" } @@ -133,7 +134,7 @@ constructor( when (status) { PackageInstaller.STATUS_SUCCESS -> { cont.resume(PreApprovalResult.Success(sessionId)) - context.unregisterReceiver(this) + unregisterReceiver(this) } PackageInstaller.STATUS_PENDING_USER_ACTION -> { val flags = FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE @@ -149,13 +150,13 @@ constructor( log.error(e) { "Error sending pre-approval intent: " } val s = PreApprovalResult.UserConfirmationRequired(sessionId, pendingIntent) cont.resume(s) - context.unregisterReceiver(this) + unregisterReceiver(this) } } else { log.info { "Can not ask pre-approval for ${app.packageName}: $intent" } val s = PreApprovalResult.UserConfirmationRequired(sessionId, pendingIntent) cont.resume(s) - context.unregisterReceiver(this) + unregisterReceiver(this) } } else -> { @@ -166,14 +167,14 @@ constructor( else -> PreApprovalResult.Error(msg) } cont.resume(result) - context.unregisterReceiver(this) + unregisterReceiver(this) } } } registerReceiver(context, receiver, IntentFilter(ACTION_INSTALL), RECEIVER_NOT_EXPORTED) cont.invokeOnCancellation { log.info { "Pre-approval for ${app.packageName} cancelled." } - context.unregisterReceiver(receiver) + unregisterReceiver(receiver) } installer.openSession(sessionId).use { session -> @@ -197,7 +198,7 @@ constructor( packageName: String, state: InstallStateWithInfo, apkFile: File, - ): InstallState = suspendCancellableCoroutine { cont -> + ): InstallState = safeSuspendCoroutine { cont -> val size = apkFile.length() log.info { "Installing ${apkFile.name} with size $size bytes" } @@ -213,12 +214,12 @@ constructor( log.error(e) { "Error when creating session: " } val s = InstallState.Error("${e::class.java.simpleName} ${e.message}", state) cont.resume(s) - return@suspendCancellableCoroutine + return@safeSuspendCoroutine } // set-up receiver for install result val receiver = receiverFactory.create(sessionId) { status, intent, msg -> - context.unregisterReceiver(this) + unregisterReceiver(this) when (status) { PackageInstaller.STATUS_SUCCESS -> { val newState = @@ -278,7 +279,7 @@ constructor( registerReceiver(context, receiver, IntentFilter(ACTION_INSTALL), RECEIVER_NOT_EXPORTED) cont.invokeOnCancellation { log.info { "App installation was cancelled, unregistering broadcast receiver..." } - context.unregisterReceiver(receiver) + unregisterReceiver(receiver) try { installer.abandonSession(sessionId) } catch (e: SecurityException) { @@ -306,11 +307,11 @@ constructor( } suspend fun requestUserConfirmation(state: InstallConfirmationState): InstallState = - suspendCancellableCoroutine { cont -> + safeSuspendCoroutine { cont -> val isPreApproval = state is InstallState.PreApprovalConfirmationNeeded val receiver = receiverFactory.create(state.sessionId) { status, _, msg -> - context.unregisterReceiver(this) + unregisterReceiver(this) when (status) { PackageInstaller.STATUS_SUCCESS -> { val newState = @@ -346,12 +347,12 @@ constructor( } } registerReceiver(context, receiver, IntentFilter(ACTION_INSTALL), RECEIVER_NOT_EXPORTED) - cont.invokeOnCancellation { context.unregisterReceiver(receiver) } + cont.invokeOnCancellation { unregisterReceiver(receiver) } try { state.intent.send() } catch (e: Exception) { log.error(e) { "Error sending user confirmation intent: " } - context.unregisterReceiver(receiver) + unregisterReceiver(receiver) cont.resume(InstallState.Error("${e::class.java.simpleName} ${e.message}", state)) } } @@ -423,4 +424,47 @@ constructor( val pendingIntent = PendingIntent.getBroadcast(context, sessionId, broadcastIntent, flags) return pendingIntent.intentSender } + + private fun unregisterReceiver(receiver: InstallBroadcastReceiver) { + try { + context.unregisterReceiver(receiver) + } catch (e: IllegalArgumentException) { + // Duplicate callbacks can race with unregistration + log.debug(e) { "Receiver already unregistered" } + } + } + + /** + * A helper function to create a suspend function that can be resumed only once. This shouldn't be + * needed, but we have seen crash reports where we received [PackageInstaller.STATUS_SUCCESS] and + * then [PackageInstaller.STATUS_FAILURE_ABORTED] for the same sessionId. + */ + private suspend fun safeSuspendCoroutine(block: (SafeContinuation) -> Unit): T = + suspendCancellableCoroutine { continuation -> + val safeCont = + object : SafeContinuation { + private val isCompleted = AtomicBoolean(false) + private var resumedValue: T? = null + + override fun resume(value: T) { + if (isCompleted.compareAndSet(false, true)) { + resumedValue = value + continuation.resume(value) + } else { + log.warn { "Already resumed with $resumedValue, not trying to resume with $value" } + } + } + + override fun invokeOnCancellation(handler: () -> Unit) { + continuation.invokeOnCancellation { handler() } + } + } + block(safeCont) + } + + private interface SafeContinuation { + fun resume(value: T) + + fun invokeOnCancellation(handler: () -> Unit) + } } diff --git a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt index ccc838d93..19355e207 100644 --- a/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt +++ b/app/src/test/java/org/fdroid/install/SessionInstallManagerTest.kt @@ -259,7 +259,8 @@ internal class SessionInstallManagerTest { every { packageInstaller.openSession(999) } returns mockk(relaxed = true) val preapprovalReceiver: InstallBroadcastReceiver = mockk(relaxed = true) every { receiverFactory.create(999, any()) } returns preapprovalReceiver - every { registerReceiver(context, preapprovalReceiver, any(), any()) } returns mockk() + every { registerReceiver(context, preapprovalReceiver, any(), any()) } returns + mockk() val iconError = sessionInstallManager.requestPreapproval( @@ -352,6 +353,42 @@ internal class SessionInstallManagerTest { verify(atLeast = 1) { context.unregisterReceiver(any()) } } + @Test + fun `install ignores duplicate callbacks after completion`() = runBlocking { + val apkFile: File = + tmpFolder.newFile("app-duplicate-callback.apk").apply { writeBytes(byteArrayOf(7)) } + + val listenerSlot = slot() + every { receiverFactory.create(sessionId, capture(listenerSlot)) } returns receiver + + val sender: IntentSender = mockk(relaxed = true) + every { PendingIntent.getBroadcast(any(), any(), any(), any()) } returns pendingIntent + every { pendingIntent.intentSender } returns sender + + every { packageInstaller.openSession(sessionId) } returns session + every { session.openWrite(packageName, 0, apkFile.length()) } returns ByteArrayOutputStream() + every { session.fsync(any()) } just runs + every { session.commit(any()) } answers + { + listenerSlot.captured.invoke(receiver, PackageInstaller.STATUS_SUCCESS, null, null) + listenerSlot.captured.invoke(receiver, PackageInstaller.STATUS_FAILURE_ABORTED, null, null) + } + every { session.close() } just Runs + every { registerReceiver(context, receiver, any(), any()) } returns mockk() + + val result = + sessionInstallManager.install( + sessionId = sessionId, + packageName = packageName, + state = installingState, + apkFile = apkFile, + ) + + // first state wins, even though we got STATUS_FAILURE_ABORTED afterward + assertIs(result) + verify(atLeast = 1) { context.unregisterReceiver(any()) } + } + @Test fun `install returns UserConfirmationNeeded when STATUS_PENDING_USER_ACTION`() { runBlocking { @@ -419,15 +456,14 @@ internal class SessionInstallManagerTest { // start the installation in the background so we can cancel() it val installScope = CoroutineScope(Dispatchers.Unconfined) - val installJob = - installScope.async { - sessionInstallManager.install( - sessionId = sessionId, - packageName = packageName, - state = installingState, - apkFile = apkFile, - ) - } + val installJob = installScope.async { + sessionInstallManager.install( + sessionId = sessionId, + packageName = packageName, + state = installingState, + apkFile = apkFile, + ) + } installJob.cancelAndJoin() verify(atLeast = 1) { context.unregisterReceiver(receiver) } From ff6c02afb5e97206b724c30f618e23c70777e27d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 13 Apr 2026 13:23:35 -0300 Subject: [PATCH 04/38] Remove nightly deploy workaround which isn't needed anymore --- .gitlab-ci.yml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index ce2dbcb54..d0f5163a7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -435,16 +435,4 @@ deploy_nightly: - rm -rf app/build/outputs/apk legacy/build/outputs/apk - ./gradlew :app:assembleBasicNightly :legacy:assembleFullDebug - mv app/build/outputs/apk/basicNightly/release/app-basic-nightly-release-unsigned.apk app/build/outputs/apk/basicNightly/release/app-debug.apk - - # taken from fdroiddata/.gitlab-ci.yml as a tmp workaround until this is released: - # https://gitlab.com/fdroid/fdroidserver/-/merge_requests/1666 - - rm -rf $fdroidserver - - mkdir $fdroidserver - - git ls-remote https://gitlab.com/fdroid/fdroidserver.git master - - curl --silent https://gitlab.com/fdroid/fdroidserver/-/archive/master/fdroidserver-master.tar.gz - | tar -xz --directory=$fdroidserver --strip-components=1 - - export PATH="$fdroidserver:$PATH" - - export PYTHONPATH="$fdroidserver:$fdroidserver/examples" - - export PYTHONUNBUFFERED=true - - fdroid nightly -v From 9741fe98a8d8e60c213927b835dbb22f50325592 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Apr 2026 08:37:07 -0300 Subject: [PATCH 05/38] Add extra logging of incoming intents to better debug what is happening when we sometimes don't react properly to intents --- app/src/main/kotlin/org/fdroid/MainActivity.kt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/main/kotlin/org/fdroid/MainActivity.kt b/app/src/main/kotlin/org/fdroid/MainActivity.kt index 19e8a00f6..24e700902 100644 --- a/app/src/main/kotlin/org/fdroid/MainActivity.kt +++ b/app/src/main/kotlin/org/fdroid/MainActivity.kt @@ -1,9 +1,12 @@ package org.fdroid import android.Manifest.permission.POST_NOTIFICATIONS +import android.app.ComponentCaller +import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build.VERSION.SDK_INT import android.os.Bundle +import android.util.Log import android.view.WindowManager.LayoutParams.FLAG_SECURE import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -58,4 +61,9 @@ class MainActivity : AppCompatActivity() { requestPermissionLauncher.launchSafe(POST_NOTIFICATIONS) } } + + override fun onNewIntent(intent: Intent, caller: ComponentCaller) { + Log.i("MainActivity", "onNewIntent: $intent, caller: $caller") + super.onNewIntent(intent, caller) + } } From 28668ea80c3e67ff3a19d94247f04a672f0ef980 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Apr 2026 14:24:36 -0300 Subject: [PATCH 06/38] Add an onboarding card for updates --- .../ui/screenshots/MyAppsScreenshotTest.kt | 1 + .../org/fdroid/settings/OnboardingManager.kt | 8 ++ .../main/kotlin/org/fdroid/ui/apps/MyApps.kt | 2 + .../kotlin/org/fdroid/ui/apps/MyAppsInfo.kt | 3 + .../kotlin/org/fdroid/ui/apps/MyAppsList.kt | 46 +++++++++--- .../org/fdroid/ui/apps/MyAppsPresenter.kt | 74 +++++++++---------- .../org/fdroid/ui/apps/MyAppsViewModel.kt | 3 + .../kotlin/org/fdroid/ui/lists/AppList.kt | 4 +- .../fdroid/ui/repositories/Repositories.kt | 4 +- .../ui/repositories/details/RepoDetails.kt | 4 +- .../org/fdroid/ui/utils/OnboardingCard.kt | 43 ++++++++--- .../org/fdroid/ui/utils/PreviewUtils.kt | 3 + app/src/main/res/values/strings.xml | 3 + .../kotlin/org/fdroid/ui/apps/MyAppsTest.kt | 4 + ...yInstalledAppsAndNoInternet_2ed8e27d_0.png | 4 +- 15 files changed, 142 insertions(+), 64 deletions(-) diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt index 26b2c952a..07ceaefda 100644 --- a/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt +++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/MyAppsScreenshotTest.kt @@ -30,6 +30,7 @@ class MyAppsScreenshotTest(localeName: String) : LocalizedScreenshotTest(localeN appUpdates = getUpdates(localeList), installingApps = emptyList(), installedApps = getInstalledApps(localeList), + showUpdatesHint = false, showAppIssueHint = false, networkState = NetworkState(isOnline = true, isMetered = false), ) diff --git a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt index 26d9fa574..1dd31d95e 100644 --- a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt @@ -18,6 +18,7 @@ class OnboardingManager constructor(@param:ApplicationContext private val context: Context) { private companion object { + const val KEY_MY_APPS_UPDATES = "myAppsUpdates" const val KEY_FILTER = "appFilter" const val KEY_REPO_LIST = "repoList" const val KEY_REPO_DETAILS = "repoDetails" @@ -26,6 +27,9 @@ constructor(@param:ApplicationContext private val context: Context) { private val prefs = context.getSharedPreferences("onboarding", MODE_PRIVATE) + private val _showMyAppsUpdatesHint = Onboarding(KEY_MY_APPS_UPDATES, prefs) + val showMyAppsUpdatesHint = _showMyAppsUpdatesHint.flow + private val _showFilterOnboarding = Onboarding(KEY_FILTER, prefs) val showFilterOnboarding = _showFilterOnboarding.flow @@ -38,6 +42,10 @@ constructor(@param:ApplicationContext private val context: Context) { private val _showAppIssueHint = Onboarding(KEY_APP_ISSUE_HINT, prefs) val showAppIssueHint = _showAppIssueHint.flow + fun onMyAppsUpdateHintSeen() { + _showMyAppsUpdatesHint.onSeen(prefs) + } + fun onFilterOnboardingSeen() { _showFilterOnboarding.onSeen(prefs) } diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt index e4d6fdfcd..177c32577 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt @@ -188,6 +188,7 @@ fun MyAppsLoadingPreview() { installingApps = emptyList(), appUpdates = null, installedApps = null, + showUpdatesHint = false, showAppIssueHint = false, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), @@ -226,6 +227,7 @@ fun MyAppsEmptyPreview() { installingApps = emptyList(), appUpdates = emptyList(), installedApps = emptyList(), + showUpdatesHint = false, showAppIssueHint = false, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt index 78c00c2f8..f6aed4cca 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt @@ -15,6 +15,7 @@ data class MyAppsModel( val installingApps: List, val appsWithIssue: List? = null, val installedApps: List? = null, + val showUpdatesHint: Boolean, val showAppIssueHint: Boolean, val sortOrder: AppListSortOrder = AppListSortOrder.NAME, val networkState: NetworkState, @@ -32,6 +33,8 @@ interface MyAppsActions { fun ignoreAppIssue(item: AppWithIssueItem) + fun onUpdatesHintSeen() + fun onAppIssueHintSeen() fun exportInstalledApps() diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt index 949645d99..4bd51b2b7 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt @@ -37,6 +37,7 @@ import org.fdroid.database.NotAvailable import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.MeteredConnectionDialog import org.fdroid.ui.utils.OfflineBar +import org.fdroid.ui.utils.OnboardingCard import org.fdroid.ui.utils.getMyAppsInfo import org.fdroid.ui.utils.myAppsModel @@ -74,7 +75,17 @@ fun MyAppsList( modifier.then(if (currentPackageName == null) Modifier else Modifier.selectableGroup()), ) { // Updates header with Update all button - if (!updatableApps.isNullOrEmpty()) { + if (updatableApps != null && updatableApps.isEmpty() && myAppsInfo.model.showUpdatesHint) { + item(key = "A-hint", contentType = "hint") { + OnboardingCard( + title = stringResource(R.string.apps_updates_hint_title), + message = stringResource(R.string.apps_updates_hint_text), + buttonText = stringResource(R.string.hide), + onGotIt = { myAppsInfo.actions.onUpdatesHintSeen() }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + } else if (!updatableApps.isNullOrEmpty()) { if (!myAppsInfo.model.networkState.isOnline) { item(key = "OfflineBar", contentType = "offlineBar") { OfflineBar() } } @@ -211,12 +222,8 @@ fun MyAppsList( NotAvailableDialog(app.packageName) { showNotAvailableDialog = false } } } - // Installed apps header (only show when we have non-empty lists above) - val aboveNonEmpty = - installingApps.isNotEmpty() || - !updatableApps.isNullOrEmpty() || - !appsWithIssue.isNullOrEmpty() - if (aboveNonEmpty && !installedApps.isNullOrEmpty()) { + // List of installed apps + if (!installedApps.isNullOrEmpty()) { item(key = "D", contentType = "header") { Text( text = stringResource(R.string.installed_apps__activity_title), @@ -224,9 +231,6 @@ fun MyAppsList( modifier = Modifier.padding(16.dp), ) } - } - // List of installed apps - if (installedApps != null) items(items = installedApps, key = { it.packageName }, contentType = { "D" }) { app -> val isSelected = app.packageName == currentPackageName val interactionModifier = @@ -241,6 +245,7 @@ fun MyAppsList( val modifier = Modifier.animateItem().then(interactionModifier) InstalledAppRow(app, isSelected, modifier) } + } } val meteredLambda = showMeteredDialog if (meteredLambda != null) @@ -264,3 +269,24 @@ private fun MyAppsListPreview() { ) } } + +@Preview +@Composable +@RestrictTo(RestrictTo.Scope.TESTS) +private fun Preview() { + FDroidContent { + MyApps( + myAppsInfo = + getMyAppsInfo( + myAppsModel.copy( + appUpdates = emptyList(), + installingApps = emptyList(), + appsWithIssue = emptyList(), + ) + ), + currentPackageName = null, + onAppItemClick = {}, + onNav = {}, + ) + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt index 0ccc3cc73..013c76eb6 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt @@ -21,6 +21,7 @@ fun MyAppsPresenter( appInstallStatesFlow: StateFlow>, appsWithIssuesFlow: StateFlow?>, installedAppsFlow: Flow>, + showUpdatesHintFlow: StateFlow, showAppIssueHintFlow: StateFlow, searchQueryFlow: StateFlow, sortOrderFlow: StateFlow, @@ -36,52 +37,48 @@ fun MyAppsPresenter( // we want to show apps currently installing/updating even if they have updates available, // so we need to handle those first - val installingApps = - appInstallStates.mapNotNull { (packageName, state) -> - if (state is InstallStateWithInfo) { - val keep = - searchQuery.isBlank() || state.name.normalize().contains(searchQuery, ignoreCase = true) - if (keep) { - processedPackageNames.add(packageName) - InstallingAppItem(packageName, state) - } else null - } else { - null - } - } - val updates = - appUpdates?.filter { + val installingApps = appInstallStates.mapNotNull { (packageName, state) -> + if (state is InstallStateWithInfo) { val keep = - if (searchQuery.isBlank()) { - it.packageName !in processedPackageNames - } else { - it.packageName !in processedPackageNames && - it.name.normalize().contains(searchQuery, ignoreCase = true) - } - if (keep) processedPackageNames.add(it.packageName) - keep + searchQuery.isBlank() || state.name.normalize().contains(searchQuery, ignoreCase = true) + if (keep) { + processedPackageNames.add(packageName) + InstallingAppItem(packageName, state) + } else null + } else { + null } - val withIssues = - appsWithIssues?.filter { - val keep = - if (searchQuery.isBlank()) { - it.packageName !in processedPackageNames - } else { - it.packageName !in processedPackageNames && - it.name.normalize().contains(searchQuery, ignoreCase = true) - } - if (keep) processedPackageNames.add(it.packageName) - keep - } - val installed = - installedApps?.filter { + } + val updates = appUpdates?.filter { + val keep = if (searchQuery.isBlank()) { it.packageName !in processedPackageNames } else { it.packageName !in processedPackageNames && it.name.normalize().contains(searchQuery, ignoreCase = true) } + if (keep) processedPackageNames.add(it.packageName) + keep + } + val withIssues = appsWithIssues?.filter { + val keep = + if (searchQuery.isBlank()) { + it.packageName !in processedPackageNames + } else { + it.packageName !in processedPackageNames && + it.name.normalize().contains(searchQuery, ignoreCase = true) + } + if (keep) processedPackageNames.add(it.packageName) + keep + } + val installed = installedApps?.filter { + if (searchQuery.isBlank()) { + it.packageName !in processedPackageNames + } else { + it.packageName !in processedPackageNames && + it.name.normalize().contains(searchQuery, ignoreCase = true) } + } var updateBytes: Long? = 0L updates?.forEach { val size = it.update.size @@ -102,6 +99,9 @@ fun MyAppsPresenter( appUpdates = updates?.sort(sortOrder), appsWithIssue = withIssues?.sort(sortOrder), installedApps = installed?.sort(sortOrder), + showUpdatesHint = + showUpdatesHintFlow.collectAsState().value && + installingApps.none { it.installState.showProgress }, showAppIssueHint = showAppIssueHintFlow.collectAsState().value, sortOrder = sortOrder, networkState = networkStateFlow.collectAsState().value, diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index 6db150a63..49f9cf700 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -99,6 +99,7 @@ constructor( appInstallStatesFlow = appInstallManager.appInstallStates, appsWithIssuesFlow = updatesManager.appsWithIssues, installedAppsFlow = installedAppItems, + showUpdatesHintFlow = onboardingManager.showMyAppsUpdatesHint, showAppIssueHintFlow = onboardingManager.showAppIssueHint, searchQueryFlow = searchQuery, sortOrderFlow = sortOrder, @@ -139,6 +140,8 @@ constructor( updatesManager.loadUpdates() } + override fun onUpdatesHintSeen() = onboardingManager.onMyAppsUpdateHintSeen() + override fun onAppIssueHintSeen() = onboardingManager.onAppIssueHintSeen() override fun exportInstalledApps() { diff --git a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt index 860ecae64..7de8fcae4 100644 --- a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt @@ -61,7 +61,7 @@ import org.fdroid.ui.FDroidContent import org.fdroid.ui.search.TopSearchBar import org.fdroid.ui.utils.BackButton import org.fdroid.ui.utils.BigLoadingIndicator -import org.fdroid.ui.utils.OnboardingCard +import org.fdroid.ui.utils.OnboardingPopupCard import org.fdroid.ui.utils.TopAppBarButton import org.fdroid.ui.utils.appListItems import org.fdroid.ui.utils.getAppListInfo @@ -81,7 +81,7 @@ fun AppList( val hintController = rememberHintController(overlay = getHintOverlayColor()) val hint = rememberHint { - OnboardingCard( + OnboardingPopupCard( title = stringResource(R.string.onboarding_app_list_filter_title), message = stringResource(R.string.onboarding_app_list_filter_message), modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt index e82f4de58..d5f7cad13 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt @@ -43,7 +43,7 @@ import org.fdroid.R import org.fdroid.download.NetworkState import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.BigLoadingIndicator -import org.fdroid.ui.utils.OnboardingCard +import org.fdroid.ui.utils.OnboardingPopupCard import org.fdroid.ui.utils.getHintOverlayColor import org.fdroid.ui.utils.getRepositoriesInfo import org.fdroid.ui.utils.repoItems @@ -54,7 +54,7 @@ fun Repositories(info: RepositoryInfo, isBigScreen: Boolean, onBackClicked: () - val hintController = rememberHintController(overlay = getHintOverlayColor()) val hint = rememberHint { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - OnboardingCard( + OnboardingPopupCard( title = stringResource(R.string.repo_list_info_title), message = stringResource(R.string.repo_list_info_text), modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt index a262b3423..75347781c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt @@ -39,7 +39,7 @@ import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.BackButton import org.fdroid.ui.utils.BigLoadingIndicator import org.fdroid.ui.utils.MeteredConnectionDialog -import org.fdroid.ui.utils.OnboardingCard +import org.fdroid.ui.utils.OnboardingPopupCard import org.fdroid.ui.utils.TopAppBarButton import org.fdroid.ui.utils.TopAppBarOverflowButton import org.fdroid.ui.utils.getHintOverlayColor @@ -58,7 +58,7 @@ fun RepoDetails( val hintController = rememberHintController(overlay = getHintOverlayColor()) val hint = rememberHint { Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - OnboardingCard( + OnboardingPopupCard( title = stringResource(R.string.repo_details), message = stringResource(R.string.repo_details_info_text), modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt b/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt index df24a65fb..9e6644747 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt @@ -1,5 +1,8 @@ package org.fdroid.ui.utils +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn import androidx.compose.material3.ElevatedCard @@ -20,16 +23,38 @@ import androidx.compose.ui.unit.dp import org.fdroid.R import org.fdroid.ui.FDroidContent +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun OnboardingPopupCard( + title: String, + message: String, + modifier: Modifier = Modifier, + buttonText: String = stringResource(R.string.got_it), + onGotIt: () -> Unit = {}, +) { + val focusRequester = remember { FocusRequester() } + OnboardingCard( + title = title, + message = message, + modifier = modifier.widthIn(max = TooltipDefaults.richTooltipMaxWidth), + buttonText = buttonText, + buttonModifier = Modifier.focusRequester(focusRequester), + onGotIt = onGotIt, + ) + LaunchedEffect(Unit) { focusRequester.requestFocus() } +} + @Composable @OptIn(ExperimentalMaterial3Api::class) fun OnboardingCard( title: String, message: String, modifier: Modifier = Modifier, + buttonText: String = stringResource(R.string.got_it), + buttonModifier: Modifier = Modifier, onGotIt: () -> Unit = {}, ) { - val focusRequester = remember { FocusRequester() } - ElevatedCard(modifier = modifier.widthIn(max = TooltipDefaults.richTooltipMaxWidth)) { + ElevatedCard(modifier = modifier) { Text( text = title, style = MaterialTheme.typography.titleSmall, @@ -40,15 +65,15 @@ fun OnboardingCard( style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(horizontal = 16.dp), ) - TextButton( - onClick = onGotIt, - modifier = - Modifier.focusRequester(focusRequester).padding(vertical = 8.dp, horizontal = 16.dp), - ) { - Text(text = stringResource(R.string.got_it)) + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { + TextButton( + onClick = onGotIt, + modifier = buttonModifier.padding(vertical = 8.dp, horizontal = 16.dp), + ) { + Text(text = buttonText) + } } } - LaunchedEffect(Unit) { focusRequester.requestFocus() } } @Preview diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 58f1c9461..8f40097e1 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -407,6 +407,8 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = override fun ignoreAppIssue(item: AppWithIssueItem) {} + override fun onUpdatesHintSeen() {} + override fun onAppIssueHintSeen() {} override fun exportInstalledApps() {} @@ -495,6 +497,7 @@ internal val myAppsModel = lastUpdated = System.currentTimeMillis() - DAYS.toMillis(3), ), ), + showUpdatesHint = true, showAppIssueHint = true, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c93feae15..8609bf3c6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,6 +56,7 @@ Use filters to only show apps from specific categories or repositories. You can also change the sort order. Got it Clear + Hide Productivity Tools @@ -295,6 +296,8 @@ This often occurs with apps installed via Google Play or other sources, if they Download canceled Installed apps + Congratulations 🥳 + All your apps are up to date Share installed apps Apps installed by F-Droid as CSV file diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt index 8b2795a5f..cbcdfeeef 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt @@ -22,6 +22,7 @@ fun MyAppsOnlyInstalledAppsAndNoInternet() { installingApps = emptyList(), appsWithIssue = null, installedApps = getInstalledApps(), + showUpdatesHint = true, showAppIssueHint = false, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = true, isMetered = false), @@ -46,6 +47,7 @@ fun MyAppsInstalledAndIssues() { installingApps = emptyList(), appsWithIssue = getAppIssues(), installedApps = getInstalledApps(), + showUpdatesHint = false, showAppIssueHint = true, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), @@ -70,6 +72,7 @@ fun MyAppsUpdatesOnlyAndNoInternet() { installingApps = emptyList(), appsWithIssue = null, installedApps = emptyList(), + showUpdatesHint = false, showAppIssueHint = false, sortOrder = AppListSortOrder.LAST_UPDATED, networkState = NetworkState(isOnline = false, isMetered = false), @@ -119,6 +122,7 @@ fun MyAppsFullList() { installingApps = installingApps, appsWithIssue = getAppIssues(), installedApps = getInstalledApps(), + showUpdatesHint = false, showAppIssueHint = false, sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = true, isMetered = false), diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png index 2505a0bb8..dac56789a 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:037f0d24d270c8fb52ac88bff5f40c999ea11035d03e270b6fd7968ad24506a5 -size 67807 +oid sha256:d915f7d6abf21e98310c1806371c071eb04e62d9104a5f6c06d933f1fffbb237 +size 88468 From 73137f49cb2de93fba681281591c780ebeb6f410 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Apr 2026 16:01:33 -0300 Subject: [PATCH 07/38] Show onboarding for anti-features in app details --- .../org/fdroid/settings/OnboardingManager.kt | 8 ++++ .../org/fdroid/ui/details/AntiFeatures.kt | 4 +- .../org/fdroid/ui/details/AppDetails.kt | 40 ++++++++++++++++++- .../org/fdroid/ui/details/AppDetailsItem.kt | 6 ++- .../fdroid/ui/details/AppDetailsViewModel.kt | 8 ++++ .../org/fdroid/ui/details/DetailsPresenter.kt | 3 ++ .../org/fdroid/ui/utils/PreviewUtils.kt | 1 + .../kotlin/org/fdroid/ui/utils/UiUtils.kt | 2 +- app/src/main/res/values/strings.xml | 4 ++ 9 files changed, 71 insertions(+), 5 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt index 1dd31d95e..2f7b946d1 100644 --- a/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/OnboardingManager.kt @@ -23,6 +23,7 @@ constructor(@param:ApplicationContext private val context: Context) { const val KEY_REPO_LIST = "repoList" const val KEY_REPO_DETAILS = "repoDetails" const val KEY_APP_ISSUE_HINT = "appIssueHint" + const val KEY_ANTI_FEATURES = "antiFeatures" } private val prefs = context.getSharedPreferences("onboarding", MODE_PRIVATE) @@ -42,6 +43,9 @@ constructor(@param:ApplicationContext private val context: Context) { private val _showAppIssueHint = Onboarding(KEY_APP_ISSUE_HINT, prefs) val showAppIssueHint = _showAppIssueHint.flow + private val _showAntiFeaturesOnboarding = Onboarding(KEY_ANTI_FEATURES, prefs) + val showAntiFeaturesOnboarding = _showAntiFeaturesOnboarding.flow + fun onMyAppsUpdateHintSeen() { _showMyAppsUpdatesHint.onSeen(prefs) } @@ -61,6 +65,10 @@ constructor(@param:ApplicationContext private val context: Context) { fun onAppIssueHintSeen() { _showAppIssueHint.onSeen(prefs) } + + fun onAntiFeaturesOnboardingSeen() { + _showAntiFeaturesOnboarding.onSeen(prefs) + } } private data class Onboarding(val key: String, private val _flow: MutableStateFlow) { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt index d58fe2f46..270d69347 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt @@ -30,11 +30,11 @@ import org.fdroid.ui.utils.testApp @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun AntiFeatures(antiFeatures: List) { +fun AntiFeatures(antiFeatures: List, modifier: Modifier = Modifier) { ElevatedCard( colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.inverseSurface), - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), ) { ExpandableSection( icon = rememberVectorPainter(Icons.Default.WarningAmber), diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index d6bc28fea..135266d8f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -41,12 +42,14 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment.Companion.CenterHorizontally import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.LiveRegionMode @@ -66,6 +69,12 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat +import com.viktormykhailiv.compose.hints.hintAnchor +import com.viktormykhailiv.compose.hints.rememberHint +import com.viktormykhailiv.compose.hints.rememberHintAnchorState +import com.viktormykhailiv.compose.hints.rememberHintController +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.R import org.fdroid.install.InstallState @@ -78,6 +87,8 @@ import org.fdroid.ui.lists.AppListType import org.fdroid.ui.navigation.NavigationKey import org.fdroid.ui.utils.BigLoadingIndicator import org.fdroid.ui.utils.ExpandableSection +import org.fdroid.ui.utils.OnboardingPopupCard +import org.fdroid.ui.utils.getHintOverlayColor import org.fdroid.ui.utils.testApp @Composable @@ -107,6 +118,21 @@ fun AppDetails( showInstallError = true } } + // onboarding hint plumbing + val hintController = rememberHintController(overlay = getHintOverlayColor()) + val hint = rememberHint { + OnboardingPopupCard( + title = stringResource(R.string.app_details_anti_features_title), + message = stringResource(R.string.app_details_anti_features_text), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + item.actions.onAntiFeaturesOnboardingSeen() + hintController.dismiss() + }, + ) + } + val hintAnchor = rememberHintAnchorState(hint) + val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() var size by remember { mutableStateOf(IntSize.Zero) } Column( @@ -217,7 +243,19 @@ fun AppDetails( } // Anti-features if (!item.antiFeatures.isNullOrEmpty()) { - AntiFeatures(item.antiFeatures) + AntiFeatures( + antiFeatures = item.antiFeatures, + modifier = + Modifier.hintAnchor(state = hintAnchor, shape = RoundedCornerShape(16.dp)).onPlaced { + if (item.showAntiFeaturesOnboarding) { + coroutineScope.launch { + delay(500) // we still need a delay to not highlight the wrong place + hintController.show(hintAnchor) + item.actions.onAntiFeaturesOnboardingSeen() + } + } + }, + ) } // Screenshots if (item.phoneScreenshots.isNotEmpty()) { diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index 53bcfb4a0..b3956eb27 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -21,8 +21,8 @@ import org.fdroid.index.RELEASE_CHANNEL_BETA import org.fdroid.index.v2.PackageVersion import org.fdroid.install.InstallState import org.fdroid.install.SessionInstallManager -import org.fdroid.ui.categories.CategoryItem import org.fdroid.search.SearchHelper.removeZeroWhiteSpace +import org.fdroid.ui.categories.CategoryItem data class AppDetailsItem( val app: AppMetadata, @@ -62,6 +62,7 @@ data class AppDetailsItem( val appPrefs: AppPrefs? = null, val whatsNew: String? = null, val antiFeatures: List? = null, + val showAntiFeaturesOnboarding: Boolean = false, val issue: AppIssue? = null, val authorHasMoreThanOneApp: Boolean = false, val proxy: ProxyConfig? = null, @@ -82,6 +83,7 @@ data class AppDetailsItem( suggestedVersion: AppVersion?, possibleUpdate: AppVersion?, appPrefs: AppPrefs?, + showAntiFeaturesOnboarding: Boolean, issue: AppIssue?, authorHasMoreThanOneApp: Boolean, localeList: LocaleListCompat, @@ -131,6 +133,7 @@ data class AppDetailsItem( localeList = localeList, proxy = proxy, ), + showAntiFeaturesOnboarding = showAntiFeaturesOnboarding, issue = issue, authorHasMoreThanOneApp = authorHasMoreThanOneApp, proxy = proxy, @@ -240,6 +243,7 @@ data class AppDetailsActions( val onRepoChanged: (Long) -> Unit, val onPreferredRepoChanged: (Long) -> Unit, val allowBetaVersions: () -> Unit, + val onAntiFeaturesOnboardingSeen: () -> Unit, val ignoreAllUpdates: (() -> Unit)? = null, val ignoreThisUpdate: (() -> Unit)? = null, val shareApk: Intent? = null, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index f1cde9358..bee95654d 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -42,6 +42,7 @@ import org.fdroid.install.AppInstallManager import org.fdroid.install.InstallState import org.fdroid.install.InstalledAppsCache import org.fdroid.repo.RepoPreLoader +import org.fdroid.settings.OnboardingManager import org.fdroid.settings.SettingsManager import org.fdroid.updates.UpdatesManager import org.fdroid.utils.IoDispatcher @@ -61,6 +62,7 @@ constructor( private val updatesManager: UpdatesManager, private val networkMonitor: NetworkMonitor, private val settingsManager: SettingsManager, + private val onboardingManager: OnboardingManager, private val appInstallManager: AppInstallManager, private val installedAppsCache: InstalledAppsCache, ) : AndroidViewModel(app) { @@ -84,6 +86,7 @@ constructor( viewModel = this, packageInfoFlow = packageInfoFlow, currentRepoIdFlow = currentRepoIdFlow, + showAntiFeaturesOnboardingFlow = onboardingManager.showAntiFeaturesOnboarding, appsWithIssuesFlow = updatesManager.appsWithIssues, networkStateFlow = networkMonitor.networkState, ) @@ -227,6 +230,11 @@ constructor( } } + @UiThread + fun onAntiFeaturesOnboardingSeen() { + onboardingManager.onAntiFeaturesOnboardingSeen() + } + @AssistedFactory interface Factory { fun create(packageName: String): AppDetailsViewModel diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index d32ced754..a07c4734a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -47,6 +47,7 @@ fun DetailsPresenter( viewModel: AppDetailsViewModel, packageInfoFlow: StateFlow, currentRepoIdFlow: StateFlow, + showAntiFeaturesOnboardingFlow: StateFlow, appsWithIssuesFlow: StateFlow?>, networkStateFlow: StateFlow, ): AppDetailsItem? { @@ -206,6 +207,7 @@ fun DetailsPresenter( onRepoChanged = viewModel::onRepoChanged, onPreferredRepoChanged = viewModel::onPreferredRepoChanged, allowBetaVersions = viewModel::allowBetaUpdates, + onAntiFeaturesOnboardingSeen = viewModel::onAntiFeaturesOnboardingSeen, ignoreAllUpdates = if (installedVersionCode == null) { null @@ -265,6 +267,7 @@ fun DetailsPresenter( suggestedVersion = suggestedVersion, possibleUpdate = possibleUpdate, appPrefs = appPrefs, + showAntiFeaturesOnboarding = showAntiFeaturesOnboardingFlow.collectAsState().value, issue = issue, authorHasMoreThanOneApp = authorHasMoreThanOneApp, localeList = locales, diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 8f40097e1..f324bd2b0 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -257,6 +257,7 @@ fun getAppDetailsActions() = onRepoChanged = {}, onPreferredRepoChanged = {}, allowBetaVersions = {}, + onAntiFeaturesOnboardingSeen = {}, ignoreAllUpdates = {}, ignoreThisUpdate = {}, shareApk = Intent(), diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt index caea0d7d7..cdad68279 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext import org.fdroid.database.Repository @Composable -fun getHintOverlayColor() = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f) +fun getHintOverlayColor() = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f) fun Context.startActivitySafe(i: Intent?) { if (i == null) return diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8609bf3c6..9a34ca5bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -226,6 +226,10 @@ This often occurs with apps installed via Google Play or other sources, if they This might also be interesting to you: More apps by %1$s + What are Anti-Features? + Some apps come with behaviors you didn’t ask for — and might not want. Things like ads, tracking, or dependence on proprietary services. We call these Anti-Features. +\n\nApps in F-Droid are clearly labeled when they contain Anti-Features, so you can make an informed choice. + About F-Droid About F-Droid Basic Apps on F-Droid are free and open source software (FOSS), they respect your privacy, and they\'re available for anyone to see the source code, build on and improve.\n\nApp distribution is transparent, privacy-respecting, and accountable, setting a standard that challenges the mobile ecosystem to do better.\n\nF-Droid\'s impact is measured not just in apps published, but in the freedom, trust, and digital rights it helps protect.\n\nHelp F-Droid to protect community-powered innovation and continue our mission of openness, privacy, and user empowerment. From fb078b0a5fda80075b6badb810167e4c43ea601d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Apr 2026 16:36:32 -0300 Subject: [PATCH 08/38] Improve version display in app details --- .../kotlin/org/fdroid/ui/details/Versions.kt | 142 +++++++++--------- .../DetailsInstallTest_9f4e7551_0.png | 4 +- .../DetailsNightTest_150baa59_0.png | 4 +- 3 files changed, 74 insertions(+), 76 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt index 91b6c7665..f6d3da2bb 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons @@ -83,7 +84,7 @@ fun Version( modifier = Modifier.padding(bottom = 8.dp).clickable { expanded = !expanded }, ) { ExpandIconChevron(expanded) - Row { + Row(verticalAlignment = Alignment.CenterVertically) { Column(modifier = Modifier.weight(1f)) { Text( text = item.version.versionName, @@ -103,14 +104,14 @@ fun Version( containerColor = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 8.dp), ) { - Text(text = stringResource(R.string.app_installed), modifier = Modifier.padding(2.dp)) + Text(text = stringResource(R.string.app_installed), modifier = Modifier.padding(4.dp)) } if (item.isSuggested) Badge( containerColor = MaterialTheme.colorScheme.secondary, modifier = Modifier.padding(start = 8.dp), ) { - Text(text = stringResource(R.string.app_suggested), modifier = Modifier.padding(2.dp)) + Text(text = stringResource(R.string.app_suggested), modifier = Modifier.padding(4.dp)) } } } @@ -120,84 +121,80 @@ fun Version( ) { val coroutineScope = rememberCoroutineScope() var showMeteredDialog by remember { mutableStateOf(false) } - Row(horizontalArrangement = spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically) { - Column(modifier = Modifier.weight(1f)) { - if (!item.isCompatible || !item.isSignerCompatible) + Column(modifier = Modifier.fillMaxWidth().padding(start = 40.dp)) { + if (!item.isCompatible || !item.isSignerCompatible) + Text( + text = + if (!item.isCompatible) { + stringResource(R.string.app_details_incompatible_version) + } else { + stringResource(R.string.app_details_incompatible_signer) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) + item.version.size?.let { size -> + Text( + text = + stringResource( + R.string.size_colon, + Formatter.formatFileSize(LocalContext.current, size), + ), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + SelectionContainer { + Text( + text = stringResource(R.string.version_code_colon, item.version.versionCode.toString()), + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + } + val sdkString = buildString { + item.version.packageManifest.minSdkVersion?.let { sdk -> + append(stringResource(R.string.sdk_min_version, sdk)) + } + item.version.packageManifest.targetSdkVersion?.let { sdk -> + if (isNotEmpty()) append(" ") + append(stringResource(R.string.sdk_target_version, sdk)) + } + item.version.packageManifest.maxSdkVersion?.let { sdk -> + if (isNotEmpty()) append(" ") + append(stringResource(R.string.sdk_max_version, sdk)) + } + } + if (sdkString.isNotEmpty()) + SelectionContainer { Text( - text = - if (!item.isCompatible) { - stringResource(R.string.app_details_incompatible_version) - } else { - stringResource(R.string.app_details_incompatible_signer) - }, + text = stringResource(R.string.sdk_versions_colon, sdkString), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.error, + maxLines = 1, + overflow = TextOverflow.StartEllipsis, ) - item.version.size?.let { size -> + } + item.version.packageManifest.nativecode?.let { nativeCode -> + if (nativeCode.isNotEmpty()) + SelectionContainer { + Text( + text = stringResource(R.string.architectures_colon, nativeCode.joinToString(", ")), + style = MaterialTheme.typography.bodySmall, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } + item.version.signer?.let { signer -> + SelectionContainer { Text( - text = - stringResource( - R.string.size_colon, - Formatter.formatFileSize(LocalContext.current, size), - ), + text = stringResource(R.string.signer_colon, signer.sha256[0].substring(0..15)), style = MaterialTheme.typography.bodySmall, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } - SelectionContainer { - Text( - text = - stringResource(R.string.version_code_colon, item.version.versionCode.toString()), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - ) - } - val sdkString = buildString { - item.version.packageManifest.minSdkVersion?.let { sdk -> - append(stringResource(R.string.sdk_min_version, sdk)) - } - item.version.packageManifest.targetSdkVersion?.let { sdk -> - if (isNotEmpty()) append(" ") - append(stringResource(R.string.sdk_target_version, sdk)) - } - item.version.packageManifest.maxSdkVersion?.let { sdk -> - if (isNotEmpty()) append(" ") - append(stringResource(R.string.sdk_max_version, sdk)) - } - } - if (sdkString.isNotEmpty()) - SelectionContainer { - Text( - text = stringResource(R.string.sdk_versions_colon, sdkString), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.StartEllipsis, - ) - } - item.version.packageManifest.nativecode?.let { nativeCode -> - if (nativeCode.isNotEmpty()) - SelectionContainer { - Text( - text = - stringResource(R.string.architectures_colon, nativeCode.joinToString(", ")), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } - item.version.signer?.let { signer -> - SelectionContainer { - Text( - text = stringResource(R.string.signer_colon, signer.sha256[0].substring(0..15)), - style = MaterialTheme.typography.bodySmall, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } } if (item.showInstallButton) { val coroutineScope = rememberCoroutineScope() @@ -211,6 +208,7 @@ fun Version( coroutineScope.launch { scrollUp() } } }, + modifier = Modifier.padding(top = 8.dp, end = 16.dp).fillMaxWidth(), ) } } diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png index c22a8e40a..900497091 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsInstallTest_9f4e7551_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d43c4f0e7edc8dc6df20e67e6833b9d6351b01d8e0d28aa95965fd8474874298 -size 306138 +oid sha256:716e489ba77720da9e1fe56a7d34a3aea8e9a8907def61eb4479447cf1f7c78d +size 306602 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png index 33c3b667d..d3e9f4634 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsTestKt/DetailsNightTest_150baa59_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:13101051c48afbeba93de01bec1927e26e7644935e944f235305a2630e4f316d -size 362639 +oid sha256:1dba67602ed49df8ee22fe0d53193e17812dcfc8663a8c3da2cf937522287b3e +size 346513 From 99bd047022f38efd59b0eeed985d8ebdcdb84dd5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Apr 2026 17:20:34 -0300 Subject: [PATCH 09/38] Improve repo disable warning dialog --- .../ui/repositories/RepositoriesList.kt | 37 ++++++++++++++++++- app/src/main/res/values-ar/strings.xml | 1 - app/src/main/res/values-b+hi+Latn/strings.xml | 1 - app/src/main/res/values-be/strings.xml | 1 - app/src/main/res/values-bg/strings.xml | 1 - app/src/main/res/values-ca/strings.xml | 1 - app/src/main/res/values-cs/strings.xml | 1 - app/src/main/res/values-da/strings.xml | 1 - app/src/main/res/values-de/strings.xml | 1 - app/src/main/res/values-el/strings.xml | 1 - app/src/main/res/values-en-rGB/strings.xml | 1 - app/src/main/res/values-eo/strings.xml | 1 - app/src/main/res/values-es/strings.xml | 1 - app/src/main/res/values-et/strings.xml | 1 - app/src/main/res/values-eu/strings.xml | 1 - app/src/main/res/values-fa/strings.xml | 1 - app/src/main/res/values-fi/strings.xml | 1 - app/src/main/res/values-fr/strings.xml | 1 - app/src/main/res/values-ga/strings.xml | 1 - app/src/main/res/values-gl/strings.xml | 1 - app/src/main/res/values-he/strings.xml | 1 - app/src/main/res/values-hr/strings.xml | 1 - app/src/main/res/values-hu/strings.xml | 1 - app/src/main/res/values-id/strings.xml | 1 - app/src/main/res/values-is/strings.xml | 1 - app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-kn/strings.xml | 1 - app/src/main/res/values-ko/strings.xml | 1 - app/src/main/res/values-lv/strings.xml | 1 - app/src/main/res/values-nb/strings.xml | 1 - app/src/main/res/values-nl/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 1 - app/src/main/res/values-pt-rPT/strings.xml | 1 - app/src/main/res/values-pt/strings.xml | 1 - app/src/main/res/values-ro/strings.xml | 1 - app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-sc/strings.xml | 1 - app/src/main/res/values-sk/strings.xml | 1 - app/src/main/res/values-sl/strings.xml | 1 - app/src/main/res/values-sq/strings.xml | 1 - app/src/main/res/values-sr/strings.xml | 1 - app/src/main/res/values-sv/strings.xml | 1 - app/src/main/res/values-sw/strings.xml | 1 - app/src/main/res/values-ta/strings.xml | 1 - app/src/main/res/values-th/strings.xml | 1 - app/src/main/res/values-tr/strings.xml | 1 - app/src/main/res/values-uk/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 1 - app/src/main/res/values-yue/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rHK/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 4 +- 55 files changed, 39 insertions(+), 55 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt index 253b8b0cb..12e6fda04 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt @@ -3,8 +3,11 @@ package org.fdroid.ui.repositories import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.plus import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState @@ -12,7 +15,12 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.AlertDialog +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -21,6 +29,7 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.graphics.shadow.Shadow @@ -108,7 +117,33 @@ fun RepositoriesList( val repoId = showDisableRepoDialog if (repoId != null) { AlertDialog( - text = { Text(text = stringResource(R.string.repo_disable_warning)) }, + title = { Text(text = stringResource(R.string.repo_disable_dialog_title)) }, + text = { + Column(verticalArrangement = spacedBy(8.dp)) { + ElevatedCard( + colors = + CardDefaults.elevatedCardColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer + ) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + ) { + Icon( + Icons.Default.Warning, + contentDescription = null, + modifier = Modifier.padding(start = 8.dp), + ) + Text( + text = stringResource(R.string.repo_disable_dialog_warning), + modifier = Modifier.padding(8.dp), + ) + } + } + Text(stringResource(R.string.repo_disable_dialog_text)) + } + }, onDismissRequest = { showDisableRepoDialog = null }, confirmButton = { TextButton( diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 0ee39d949..e3b6c2e80 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -609,7 +609,6 @@ أظهِر التطبيقات المؤرشفة والإصدارات القديمة من التطبيقات عطّل (مفضّل) - سيؤدي تعطيل هذا المستودع إلى إزالة حالته \"المفضلة\" من جميع التطبيقات. عنوان URL الذي تحاول إضافته (%s) هو مرآة لمستودع موجود. أضِف المستودع والمرآة اعرض المستودع diff --git a/app/src/main/res/values-b+hi+Latn/strings.xml b/app/src/main/res/values-b+hi+Latn/strings.xml index 503e9958d..c49d62825 100644 --- a/app/src/main/res/values-b+hi+Latn/strings.xml +++ b/app/src/main/res/values-b+hi+Latn/strings.xml @@ -529,7 +529,6 @@ %3$s se app details save kiye ja rahe haì (%1$d/%2$d) Update karte waqt error: %s Anján - Is bhanđár ko disable kar dene par iski \"cuni hui\" sthiti sabhi apps se haţ jáyegi. Sabhi bhanđárè up-to-date haì Báki kisi bhi bhanđár ne error nahì diya. Theme diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml index 231009f24..6f8a3e630 100644 --- a/app/src/main/res/values-be/strings.xml +++ b/app/src/main/res/values-be/strings.xml @@ -571,7 +571,6 @@ Архіўныя рэпазітары не могуць быць дададзены напрамую. Націсніце на рэпазітар са спісу і ўключыце ў ім архіў. Адлюстроўваць архіўныя і састарэлыя версіі праграм Архіў рэпазіторыя - Адключэнне гэтага рэпазітара выдаліць яго статус «пераважны» з усіх праграм. IPFS-шлюзы Дадаць шлюз IPFS Дадаць diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index ab467893d..8fb0852fc 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -557,7 +557,6 @@ Предпочитане на хранилището Хранилището е източник на приложения. Този списък показва всички добавени за момента хранилища. Изключените хранилища не се използват.\n\nАко едно приложение е налично в повече от едно хранилище, се използва хранилището, което е по-горе в списъка. Можете да подреждате хранилищата, с дълго задържане и влачене. Архив на хранилище не може да бъде добавен директно. Докоснете хранилище в списъка и го направете архивно. - Изключване на това хранилище ще премахне състоянието „предпочитано“ от всички приложения. Добавяне на хранилище или огледало Преглед Сканиране на QR diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 591d4ac68..2b51c25ff 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -485,7 +485,6 @@ Llista de repositoris Arxiu del repositori L\'arxiu del dipòsit no està disponible actualment - Desactivant aquest dipòsit traureu el seu estat de \"preferit\" a totes les aplicacions. Actualitzada fa %1$d any Actualitzada fa %1$d anys diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 7861278c1..9195620ec 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -565,7 +565,6 @@ Zakázat Zobrazit archivované a zastaralé verze aplikací Archiv repozitáře momentálně není dostupný - Zakázáním tohoto repozitáře jej odeberete z „vybraných“ u všech aplikací. Repozitář je zdroj aplikací. Tento seznam zobrazuje všechny aktuálně přidané repozitáře. Zakázané repozitáře nejsou používány.\n\nPokud se aplikace nachází více než v jednom repozitáři, bude automaticky vybrán repozitář výše v seznamu. Repozitáře můžete přesunout podržením a přesunutím. nikdy Poslední aktualizace: %s diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index b3d860454..48025cf24 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -545,7 +545,6 @@ Ugyldigt repository.\n\nKontakt vedligeholderen og informer dem om problemet. Start dit Wi-Fi og forbind til det samme netværk som din ven for at udveksle apps. Et repository er en kilde til apps. Denne liste viser alle aktuelt tilføjede repositories. Deaktiverede repositories bruges ikke.\n\nHvis en app findes i mere end ét repository, foretrækkes det øverste på listen automatisk. Du kan ændre rækkefølgen af repositories ved at holde og trække dem. - Deaktiverer du dette repository, fjernes dets \"foretrækkes\" status fra alle apps. Kopieret til udklipsholder Indsæt Tilladelsesnavn kopieret til udklipsholder diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 8ad984f07..38427ffee 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -562,7 +562,6 @@ Zeig archivierte Apps und veraltete Versionen von Apps Paketquellen-Archiv Archiv-Paketquelle derzeit nicht verfügbar - Das Deaktivieren dieser Paketquelle wird ihren »bevorzugt«-Status von allen Apps entfernen. Paketquellen-Liste Eine Paketquelle ist eine Quelle für Apps. Diese Liste zeigt alle derzeit hinzugefügten Paketquellen an. Deaktivierte Paketquellen werden nicht verwendet. \n \nWenn eine App in mehr als einer Paketquelle enthalten ist, wird die in der Liste weiter oben stehende Paketquelle automatisch bevorzugt. Du kannst Paketquellen durch langes Drücken und Ziehen neu anordnen. Archiv-Paketquellen können nicht direkt hinzugefügt werden. Tippe auf die Paketquelle in der Liste und aktiviere das Archiv dort. diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 23de548ea..612673fc7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -536,7 +536,6 @@ \nΕάν μια εφαρμογή βρίσκεται σε περισσότερα από ένα αποθετήρια, προτιμάται αυτόματα το αποθετήριο που βρίσκεται ψηλότερα στη λίστα. Μπορείτε να αναδιατάξετε τα αποθετήρια πατώντας παρατεταμένα και σύροντάς τα. Αρχείο αποθετηρίου Εμφάνιση αρχειοθετημένων εφαρμογών και ξεπερασμένων εκδόσεων εφαρμογών - Η απενεργοποίηση αυτού του αποθετηρίου θα αφαιρέσει την κατάσταση \"προτιμώμενη\" από όλες τις εφαρμογές. Απενεργοποίηση Αντιγραφή URL στο πρόχειρο Για να ανταλλάξετε εφαρμογές, ξεκινήστε το Wi-Fi και συνδεθείτε στο ίδιο δίκτυο με τον φίλο σας. diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index d1dfcbd59..d0c622c40 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -561,7 +561,6 @@ never Repository list Archive repo currently not available - Disabling this repository will remove its \"preferred\" status from all apps. Disable Repositories Use cached results to minimise DNS queries. diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 831d1ca4b..a633fe869 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -551,7 +551,6 @@ Ne eblas senpere aldoni arĥivan deponejon. Frapetu deponejon sur la listo kaj tie aktivigi arĥivon. Arĥiva deponejo ne estas nune disponebla Malaktivigi - Per malaktivigi tiun ĉi deponejon, ĝi estos forigita kiel preferata deponejo por ĉiuj ĝiaj aplikaĵoj. Lasta ĝisdatigo: %s neniam Arĥiva deponejo diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 96f33140b..04144e5ed 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -588,7 +588,6 @@ Lista de repositorios Versiones históricas Muestra aplicaciones antiguas y versiones obsoletas de aplicaciones existentes - Deshabilitar este repositorio eliminará su estado \"preferido\" de todas las aplicaciones. Desactivar Un repositorio es una fuente de aplicaciones. Esta lista muestra todos los repositorios que has añadido. Los repositorios desactivados no se utilizan.\n\nSi una aplicación está en más de un repositorio se utilizará la versión del repositorio que esté más alto en la lista. Puedes reordenar el listado manteniendo pulsado y arrastrando cada entrada. La URL que está intentando añadir (%s) es una réplica de un repositorio existente. diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 0dc616b5a..6ddce00b9 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -567,7 +567,6 @@ Viimane uuendus: %s Arhiiv repo praegu mitte saadaval Näita arhiivitud äppe või äppide vanasid versioone - Selle tarkvarahoidla väljalülitamine eemaldab tema \"eelistatud\" oleku kõikide rakenduste jaoks. Lülita välja mitte kunagi Eelista tarkvarahoidlat diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index c0dd403dc..630addd63 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -576,7 +576,6 @@ Gordailuko artxiboa Erakutsi gordetako aplikazioak eta aplikazioen bertsio zaharkituak Artxiboen gordailua une honetan ez dago erabilgarri - Gordailu hau desgaituz gero, aplikazio guztietatik \"hobetsitakoa\" egoera kenduko da. Desgailu Erabili DNS cachea Erabili cachean gordetako emaitzak DNS kontsultak minimizatzeko. diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 5ae2fef61..f4aa4cbd5 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -551,7 +551,6 @@ سیاههٔ مخزن‌ها بایگانی مخزن نمایش کاره‌های بایگانی شده و نگارش‌های از رده‌خارجشان - از کار انداختن این مخزن وضعیت «ترجیحی»اش را از همهٔ کاره‌ها برمی‌دارد. مخزن بایگانی اکنون در دسترس نیست مخزن مخزن‌ها diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 0bc7a765e..3234f25bb 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -558,7 +558,6 @@ Lataa sovelluksia IPFS-verkkopäätepisteistä. (Toimii vain F-Droid-ohjelmavarastoissa, joissa on IPFS-tuki.) Ohjelmavarastoluettelo Ohjelmavarastoarkisto - Tämän ohjelmavaraston poistaminen käytöstä poistaa sen \"ensisijaisen\" tilan kaikista sovelluksista. Lataa sovelluksia IPFS-verkkopäätepisteiden avulla: pois käytöstä Lataa sovelluksia IPFS-verkkopäätepisteen avulla: %1$d käytössä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 2cd1bb9b4..c935b9966 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -576,7 +576,6 @@ Archive des dépôts Afficher les apps archivées et les anciennes versions des apps Dernière mise à jour : %s - Désactiver ce dépôt enlèvera son statut « préféré » de toutes les applications. jamais Un dépôt est un pourvoyeur d\'applis. Cette liste affiche tous les dépôts actuellement ajoutés. Les dépôts désactivés ne sont pas utilisés. \n \nSi une appli est disponible dans plus d\'un dépôt, le dépôt le plus haut dans la liste est préféré. Vous pouvez changer l\'ordre des dépôts avec un appui long puis en les faisant glisser. Les dépôts d\'archive ne peuvent pas être directement ajoutés. Appuyez sur le dépôt dans la liste puis activez l\'archive à cet emplacement. diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index a39a9b45a..2922ad326 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -437,7 +437,6 @@ Ciallaíonn scriosadh stór nach mbeidh feidhmchláir uaidh ar fáil a thuilleadh. \n \nNóta: Fanfaidh gach feidhmchlár a suiteáladh roimhe seo ar do ghléas. - Má dhíchumasaítear an stór seo, bainfear a stádas \"is fearr\" as gach feidhmchlár. Díchumasaigh Stór pacáiste sábháilte %1$s. Ag lorg stór pacáiste ag diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 4ccfd7a70..07f7f59ff 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -537,7 +537,6 @@ Este espello xa foi engadido (%s). A URL que está tentando engadir (%s) é un espello dun repositorio existente. Arquivo de repositorios - Desactivar este repositorio eliminará o seu estado de \"preferente\" de todas as aplicacións. Desactivar nocturno Para compartir aplicacións, conéctate á mesma rede Wi-Fi que a túa amizade. diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index f5dd483c3..35816a64c 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -585,7 +585,6 @@ מאגר הוא מקור של יישומונים. הרשימה הזאת מציגה את כל המאגרים שנוספו. המאגרים המושבתים אינם בשימוש. \n \nאם יישומון הוא יותר במאגר אחד, המאגר שנמצא יותר גבוה ברשימה מקבל עדיפות אוטומטית. אפשר לסדר את המאגרים מחדש בלחיצה ארוכה עליהם וגרירתם. - השבתת המאגר תסיר את מצב ה„העדפה” שלו מכל היישומונים. מאגר ארכיון לא זמין כרגע הורדת יישומונים באמצעות נקודות גישת IPFS: מושבתת הוספת שער גישת IPFS diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index e5d970030..907c8d2d5 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -605,7 +605,6 @@ Arhivski repozitorij trenutačno nije dostupan Izbrisati zrcalni server? Možeš nastaviti instalirati aplikacije iz ovog repozitorija pomoću drugih zrcalnih servera. - Deaktiviranje ovog repozitorija će ukloniti njegov „preferirani” status iz svih aplikacija. Deaktiviraj Dijeli zrcalni server Prikaži QR kod repozitorija diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 345964194..7ab8d8498 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -570,7 +570,6 @@ soha Az archív tároló jelenleg nem érhető el Archivált alkalmazások és elavult verziók megjelenítése - A tároló letiltásával eltávolítja az „előnyben részesített” állapotát az összes alkalmazástól. Adja meg a Bluetooth vagy a Közeli eszköz engedélyt! Következő frissítés a feltételek teljesülése esetén APK-fájl megosztása diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 63707363a..45b35d7ee 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -538,7 +538,6 @@ Tidak dapat mencari alamat repositori dalam teks terbagi. Arsip repositori Repositori arsip saat ini tidak tersedia - Menonaktifkan repositori ini akan menghilangkan status \"lebih disukai\" dari semua aplikasi. sasaran %s Nonaktifkan Unduh aplikasi dari titik akhir web IPFS. (Hanya bekerja pada repositori F-Droid dengan dukungan IPFS.) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index e71855a3b..37eaab009 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -557,7 +557,6 @@ Bæta við hugbúnaðarsafni og spegli aldrei Slóðin sem þú ert að reyna að bæta við (%s) er spegill fyrir nýtt hugbúnaðarsafn. Bætt verður við bæði upprunalegu hugbúnaðarsafni og spegli þess. - Sé þetta hugbúnaðarsafn gert óvirkt verður staða þess sem \"ákjósanlegt\" fjarlægð af öllum forritum. Hugbúnaðarsafn (ákjósanlegt) Útvíkka lista yfir hugbúnaðarsöfn diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 859b55bd8..97c1f51ec 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -591,7 +591,6 @@ mai Elenco repository Repo di archivio attualmente non disponibile - Disattivando questo repository verrà rimosso il suo stato \"preferito\" da tutte le app. Aggiungi repository e mirror Vedi il repository Scansiona codice QR diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 56b13a351..3966fd893 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -559,7 +559,6 @@ IPFSウェブ終点を使用してアプリをダウンロード: 無効 IPFSウェブ終点からアプリをダウンロードします。 (IPFSに対応するF-Droidレポジトリだけで機能します。) - このリポジトリを無効にすると、すべてのアプリからそれの「優先」状態が削除されます。 IPFSゲートウェイ IPFSゲートウェイを追加 F-DroidはSDカード上からレポジトリとミラーを検索できますが、権限が必要です。 diff --git a/app/src/main/res/values-kn/strings.xml b/app/src/main/res/values-kn/strings.xml index 0ca4448e6..eee9b2cba 100644 --- a/app/src/main/res/values-kn/strings.xml +++ b/app/src/main/res/values-kn/strings.xml @@ -501,7 +501,6 @@ ಸಕ್ರಿಯ ವಿ.ಪಿ.ಎನ್ ಜೊತೆಗೆ ಸಂಘರ್ಷಿಸುತ್ತದೆ! ಎಫ್-ಡ್ರಾಯ್ಡ್ ಬೇಸಿಕ್ ಕಸ್ಟಮೈಜ್ ಐ ಪಿ ಎಫ್ ಎಸ್ ಗೇಟ್ವೇಸ್ - ಡಿಸೇಬ್ಲಿಂಗ್ ದಿಸ್ ರೆಪೊಸಿಟರಿ ವಿಲ್ ರಿಮೋವ್ ಇಟ್ಸ್ \"ಪ್ರಿಫರ್ಡ್\" ಸ್ಥಿತಿ ಫ್ರಾಮ್ ಆಲ್ ಅಲ್ ಅಪ್ಪ್ಸ್. ಅಪ್ಲಿಕೇಶನ್ ಫೈಲ್ ಹಂಚಿಸಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ ಈ ಅಪ್ಲಿಕೇಶನ್ ಹಳಈ ಅಪ್ಲಿಕೇಶನ್‌ನು ಹಳೆಯ ಆಂಡ್ರಾಯ್ಡ್ ಆವೃತ್ತಿಗಾಗಿ ನಿರ್ಮಿಸಲಾಗಿದೆ ಮತ್ತು ಸ್ವಯಂಮತ್ತಾಯವಾಗಿ ನವೀಕರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ ನೀವು ಹಳೆಯ ಆವೃತ್ತಿಯ F-Droid ಅನ್ನು ಹೊಂದಿರುವ ಯಾರಾದರು ಸಂಪರ್ಕ ಸ್ಥಾಪಿಸಲು ಪ್ರಯತ್ನಿಸುತ್ತಿದ್ದೀರಿ. ಅವರು ತಮ್ಮ F-Droid ಅಪ್ಲಿಕೇಶನ್‌ನನ್ನು ನವೀಕರಿಸಬೇಕಾಗಿರುತ್ತದೆ. ಅವರು ನಿಮ್ಮಿಂದ F-Droid ಅಪ್ಲಿಕೇಶನ್‌ನನ್ನು ನವೀಕರಿಸಲು ಸ್ವಾಪ್ ಬಳಸಬಹುದು. diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index dee78388e..ad887e082 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -548,7 +548,6 @@ 설치에 권장되는 버전이 없습니다. 이 저장소는 이미 추가되었습니다. Archive 저장소는 직접 추가할 수 없습니다. 목록에서 저장소를 탭하고 거기 아카이브를 활성화. - 이 저장소를 비활성화하면 모든 앱의 \"preferred\"상태를 제거합니다. 대상 %s 마지막 업데이트: %s 한 적 없음 diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index a744c697f..48f96dbf6 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -575,7 +575,6 @@ URL, ko tiek mēģināts pievienot (%s), ir jaunas glabātavas spoguļserveris. Tiks pievienota gan sākotnējā glabātava, gan spoguļserveris. Rādīt arhivētās lietotnes un novecojušas lietotņu versijas - Šīs glabātavas atspējošana noņems tās stāvokli \"vēlama\" no visām lietotnēm. Tuvākais nākamais atjauninājums: %s Nākamais atjauninājums, kad ir izpildījušies nosacījumi Piesaistīti tīkla pakalpojumi diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index ffb9d3e0e..5b46c02da 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -581,7 +581,6 @@ Pakkebrønnarkivet er ikke tilgjengelig nå Slett speilet? Du kan fortsette å installere apper fra denne pakkebrønnen ved å bruke andre speil. - Å skru av denne pakkebrønnen vil fjerne den som «foretrukket» fra alle apper. Skru av Del speil Vis QR-kode til pakkebrønn diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index e332b96f4..c5d22d18d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -570,7 +570,6 @@ \n \nAls een app in meerdere gegevensbanken voorkomt dan wordt de eerste gegevensbank in de lijst gebruikt. U kunt de volgorde veranderen door een gegevensbank ingedrukt te houden en te verplaatsen. Toon gearchiveerde apps en verouderde versies van apps - Uitschakelen van deze gegevensbank zal de \"geprefereerde\" status verwijderen van alle apps. Download apps van IPFS web eindpunten: %1$d ingeschakeld Download apps van IPFS web eindpunten: %1$d ingeschakeld diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index e0bc1605f..22d5134d1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -596,7 +596,6 @@ Preferuj repozytorium Archiwalne repozytoria nie mogą być dodawane bezpośrednio. Naciśnij repozytorium na liście i włącz tam archiwum. Ostatnia aktualizacja: %s - Wyłączenie tego repozytorium spowoduje usunięcie jego statusu „preferowane” ze wszystkich aplikacji. Dodaj repozytorium i serwer lustrzany Wyświetl repozytorium Zeskanuj kod QR diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6eb9ffff5..e5211b485 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -568,7 +568,6 @@ Arquivo do repositório Mostrar apps arquivados e versões desatualizadas de apps O arquivo do repositório não está disponível no momento - Desativar este repositório removerá o estado de \"preferido\" dele de todos os apps. Desativar (preferido) Adicionar repositório e espelho diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 81c9fc8e8..ccf14c408 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -543,7 +543,6 @@ Um repositório é uma fonte de aplicações. Esta lista mostra todos os repositórios atualmente adicionados. Os repositórios desativados não são utilizados.\n\nSe uma aplicação estiver em mais do que um repositório, o repositório no topo da lista é automaticamente preferido. Pode reordenar os repositórios premindo longamente e arrastando-os. Arquivo do repositório O repositório de arquivos não está atualmente disponível - A desativação deste repositório removerá o seu estatdo de \"preferido\" de todas as apps. Desativar F-Droid pode procurar por repositórios e espelhos no seu Cartão de Memória, mas precisa da sua permissão para fazer isso. Para trocar aplicações, inicie o seu Wi-Fi e ligue-se à mesma rede que o seu amigo. diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 90459002c..32ba51497 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -571,7 +571,6 @@ Mostrar aplicações arquivadas e versões desatualizadas de aplicações O repositório de arquivos não está atualmente disponível Desativar - A desativação deste repositório removerá o seu estatuto de \"preferido\" de todas as aplicações. Gateways IPFS Repositório Repositórios diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 7727e6103..91a759cfc 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -570,7 +570,6 @@ Lista de depozite Arhivă depozit Arhivă depozit momentan indisponibilă - Dezactivarea acestui depozit îl va elimina ca fiind \"preferat\" pentru toate aplicațiile. Ultima actualizare: %s niciodată Un depozit este o sursă de aplicații. Această listă afișează toate depozitele adăugate în prezent. Depozitele dezactivate nu sunt utilizate. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index cfc69dbf0..42a2b193d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -576,7 +576,6 @@ (выбран) Архивные репозитории не могут быть добавлены напрямую. Нажмите на репозиторий из списка и включите в нем архив. Репозиторий — это источник приложений. В этом списке отображаются все добавленные репозитории. Отключенные репозитории не используются.\n\nЕсли приложение находится в нескольких репозиториях, предпочтение автоматически отдается репозиторию, расположенному выше в списке. Изменить порядок репозиториев можно долгим нажатием и перетаскиванием. - Отключение этого репозитория удалит его статус \"предпочтительный\" из всех приложений. Отключить Не удалось найти адрес репозитория в общем тексте. Добавить репозиторий и зеркало diff --git a/app/src/main/res/values-sc/strings.xml b/app/src/main/res/values-sc/strings.xml index 30fc35738..cb5ec2b5e 100644 --- a/app/src/main/res/values-sc/strings.xml +++ b/app/src/main/res/values-sc/strings.xml @@ -538,7 +538,6 @@ Iscàrriga aplicatziones dae sos puntos finales IPFS: %1$d abilitadu Iscàrriga aplicatziones dae sos puntos finales IPFS: %1$d abilitados - Disabilitende custu depòsitu nd\'as a bogare s\'istadu de \"preferidu\" suo dae totu sas aplicatziones. Ùrtima atualizatzione: %s mai Lista de depòsitos diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 74449f224..f63ae2b2d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -638,7 +638,6 @@ Archív repozitára nie je momentálne k dispozícii Odstrániť zrkadlo? Aplikácie z tohto repozitára môžete naďalej inštalovať prostredníctvom iných zrkadiel. - Deaktiváciou tohto repozitára sa zruší jeho status „predvoleného“ vo všetkých aplikáciách. Vypnúť Zrkadlo na zdieľanie Zobraziť QR kód repozitára diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 040588b96..160e6c1eb 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -634,7 +634,6 @@ Arhivski repozitorij trenutno ni na voljo Izbriši zrcalo? Aplikacije iz tega repozitorija lahko še naprej nameščate z uporabo drugih zrcalnih strežnikov. - Če onemogočite to skladišče, boste odstranili njegov status \"prednostno\" iz vseh aplikacij. Onemogoči Deli zrcalo Prikaži QR kodo repozitorija diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 028f46d39..e5f40f5e3 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -520,7 +520,6 @@ Listë deposh Arkiv deposh Shfaq aplikacione të arkivuar dhe versione të vjetruar të aplikacioneves - Çaktivizimi i kësaj depoje do t’i heqë asaj gjendjen “e parapëlqyer” nga krejt aplikacionet. Kanale IPFS Përditësimi i fundit më: %s kurrë diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index bedf1eafc..1efb8dc81 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -562,7 +562,6 @@ Листа репозиторијума Прошири листу репозиторијума Приказује архивиране апликације и застареле верзије апликација - Онемогућавањем овог репозиторијума уклањате његов статус „префериран” из свих апликација. (префериран) Преферирај репозиторијум Последње ажурирање: %s diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4127b5c88..f6aa08b7a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -573,7 +573,6 @@ Förrådets arkiv är inte tillgängligt just nu Tidigast nästkommande uppdatering: %s Inaktivera - Att inaktivera förrådet kommer ta bort dess status som \"föredragen\" från alla appar. IPFS-gateways Det valda förvaret har inga kompatibla appversioner. För att ta emot uppdateringar, föredra ett förvar med kompatibla versioner. Hämta appar med IPFS-webbändpunkter: inaktiverad diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index ec706d4c2..4429586d2 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -559,7 +559,6 @@ Kumbukumbu za ghala haiwezi kuongezwa moja kwa moja. Gusa ghala kwenye orodha na uwezeshe kumbukumbu hio hapo. Ghala la kumbukumbu halipatikani kwa sasa. Milango za IPFS - Kulemaza ghala hili litaondoa hali yake ya \"Iliyopendekezwa\" kutoka kwa apu zote. Apu zilizojumuishwa: URL hio unayojaribu kuongeza (%s) ni kioo cha ghala iliyopo. URL unayojaribu kuongeza (%s) ni kioo cha ghala mpya. Zote mbili ghala asili na kioo zitaongezwa. diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index cdef44a45..0732965a3 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -529,7 +529,6 @@ களஞ்சிய பட்டியலை விரிவாக்கு டிஎன்எச் வினவல்களைக் குறைக்க தற்காலிக சேமிப்பு முடிவுகளைப் பயன்படுத்தவும். களஞ்சிய காப்பகம் - இந்த களஞ்சியத்தை முடக்குவது அனைத்து பயன்பாடுகளிலிருந்தும் அதன் \"விருப்பமான\" நிலையை அகற்றும். முயற்சி செய்யுங்கள் ஒருபோதும் களஞ்சியத்தையும் கண்ணாடியையும் சேர்க்கவும் diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index 93f4405c2..6225f30aa 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -534,7 +534,6 @@ รายการพื้นที่เก็บข้อมูล ที่เก็บถาวร ที่เก็บถาวรไม่พร้อมใช้งานในขณะนี้ - การปิดใช้งานที่เก็บนี้จะลบสถานะ \"ที่ต้องการ\" ออกจากแอปทั้งหมด ปิดการใช้งาน แปะ เกตเวย์ IPFS แบบกำหนดเอง diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index ed48c559f..159aca14e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -572,7 +572,6 @@ Son Güncelleme: %s yok Arşiv deposu şu anda kullanılabilir değil - Bu deponun devre dışı bırakılması, tüm uygulamalardan \"tercih edilen\" durumunu kaldıracaktır. Devre dışı bırak (tercih edilen) Depo ve yansı ekle diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a6bdd2fc7..0365920b9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -568,7 +568,6 @@ Розгорнути список репозиторіїв Показувати заархівовані застосунки та застарілі версії застосунків Вимкнути - Вимкнення цього репозиторію призведе до вилучення його стану \"бажаного\" з усіх застосунків. Востаннє оновлено: %s ніколи Список репозиторіїв diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 088ee4a70..bda4dcd6c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -526,7 +526,6 @@ Kho này đã được thêm từ trước. Cập nhật lần cuối: %s chưa từng cập nhật - Vô hiệu hóa kho này sẽ gỡ bỏ trạng thái \"ưu tiên\" của nó khỏi tất cả ứng dụng. mục tiêu %s Kho ứng dụng (repository) lưu giữ thông tin cài đặt và cập nhật cho các ứng dụng, Danh sách này hiển thị tất cả các kho đã thêm. Các kho bị vô hiệu hóa sẽ không cung cấp được thông tin cài đặt và cập nhật cho các ứng dụng trong kho ấy.\n \nNếu một ứng dụng tồn tại trong nhiều kho, kho có vị trí cao hơn trong danh sách sẽ được ưu tiên. Bạn có thể sắp xếp vị trí kho bằng cách nhấn giữ và kéo thả chúng. Dùng trình cài đặt cũ diff --git a/app/src/main/res/values-yue/strings.xml b/app/src/main/res/values-yue/strings.xml index b0fd2dc7a..f6afe9886 100644 --- a/app/src/main/res/values-yue/strings.xml +++ b/app/src/main/res/values-yue/strings.xml @@ -557,7 +557,6 @@ 從來沒有 存庫清單 封印存庫目前欠奉 - 若然停用呢個存庫,所有 app 都唔會再「揀選」佢個囉噃。 停用 存庫 揀選存庫 diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 8dfdfe130..45cbf0726 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -555,7 +555,6 @@ 上次更新:%s 从未 存储库列表 - 停用此存储库会从所有应用去除其“首选”状态。 (首选) 包含的应用: 你正尝试添加的 URL (%s) 是一个新存储库的镜像。原始存储库和此镜像都将被添加。 diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml index bc6dedc36..7f1882d74 100644 --- a/app/src/main/res/values-zh-rHK/strings.xml +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -561,7 +561,6 @@ 軟件庫封存 顯示已封存的應用程式與各應用程式的過時版本 封存軟件庫目前不可用 - 若停用此軟件庫,所有應用程式均將不再「喜好」此軟件庫。 停用 軟件庫 (喜好) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 8b7ca20e8..26b33b752 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -554,7 +554,6 @@ 從不 軟體庫是應用程式的來源。此清單顯示所有目前新增的軟體庫。不會使用已停用的軟體庫。 \n \n若一個應用程式位於多個軟體庫中,則自動偏好清單中較高的軟體庫。您可以透過長按並拖曳軟體庫來重新排序。 封存軟體庫目前無法使用 - 停用此軟體庫將從所有應用程式中刪除其「偏好」狀態。 新增軟體庫與鏡像 掃描 QR Code 包含的應用程式: diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9a34ca5bf..a893fd847 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -646,7 +646,9 @@ This often occurs with apps installed via Google Play or other sources, if they Disabled "%1$s".\n\nYou will need to re-enable this repository to install apps from it. - Disabling this repository will remove its \"preferred\" status from all apps. + Disable repository + If you\'ve set this repository as preferred for any apps, that preference will be lost + All apps from this repository will disappear. Already installed apps won\'t be affected. Disable Saved package repository %1$s. Looking for package repository at\n%1$s From ace079d1bef5a7aea80f4275918fc1db97366d73 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 15 Apr 2026 10:16:59 -0300 Subject: [PATCH 10/38] Fixing reacting to intents after app got killed --- app/src/main/kotlin/org/fdroid/MainActivity.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/MainActivity.kt b/app/src/main/kotlin/org/fdroid/MainActivity.kt index 24e700902..54f1550c4 100644 --- a/app/src/main/kotlin/org/fdroid/MainActivity.kt +++ b/app/src/main/kotlin/org/fdroid/MainActivity.kt @@ -6,7 +6,6 @@ import android.content.Intent import android.content.pm.PackageManager.PERMISSION_GRANTED import android.os.Build.VERSION.SDK_INT import android.os.Bundle -import android.util.Log import android.view.WindowManager.LayoutParams.FLAG_SECURE import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -17,6 +16,7 @@ import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import javax.inject.Inject import kotlinx.coroutines.launch +import mu.KotlinLogging import org.fdroid.settings.SettingsManager import org.fdroid.ui.Main import org.fdroid.ui.utils.launchSafe @@ -26,12 +26,15 @@ import org.fdroid.ui.utils.launchSafe @AndroidEntryPoint class MainActivity : AppCompatActivity() { + private val log = KotlinLogging.logger {} + val requestPermissionLauncher = registerForActivityResult(RequestPermission()) {} @Inject lateinit var settingsManager: SettingsManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + log.info { "onCreate($savedInstanceState)" } // observe preventScreenshots setting and react to changes lifecycleScope.launch { // this flow doesn't change when we are paused, so we keep collecting it @@ -47,6 +50,7 @@ class MainActivity : AppCompatActivity() { setContent { Main { // inform OnNewIntentListeners about the initial intent (otherwise would be missed) + log.info { "Passing initial intent: $intent" } if (intent != null) { onNewIntent(intent) // set intent to null to avoid re-processing on configuration changes @@ -63,7 +67,15 @@ class MainActivity : AppCompatActivity() { } override fun onNewIntent(intent: Intent, caller: ComponentCaller) { - Log.i("MainActivity", "onNewIntent: $intent, caller: $caller") + log.info { + val callerInfo = if (SDK_INT >= 35) caller.`package` else caller.toString() + "onNewIntent: $intent, caller: $callerInfo" + } + // if the app got killed and restarted via an intent, + // this may run before the IntentListener is attached, so it would miss this intent + if (SDK_INT >= 35) setIntent(intent, caller) else setIntent(intent) + + // calling super seems to be needed, so the IntentListener gets informed super.onNewIntent(intent, caller) } } From c58b595b5c272dc2430e8968916bda029e2998a9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 14 Apr 2026 17:43:51 -0300 Subject: [PATCH 11/38] Allow user to never show metered network warning dialog --- .../org/fdroid/download/NetworkMonitor.kt | 43 ++++++++++++++----- .../org/fdroid/settings/SettingsConstants.kt | 3 ++ .../org/fdroid/settings/SettingsManager.kt | 11 +++++ .../kotlin/org/fdroid/ui/apps/MyAppsInfo.kt | 2 + .../kotlin/org/fdroid/ui/apps/MyAppsList.kt | 7 ++- .../org/fdroid/ui/apps/MyAppsViewModel.kt | 4 ++ .../org/fdroid/ui/details/AppDetails.kt | 2 +- .../org/fdroid/ui/details/AppDetailsHeader.kt | 5 ++- .../org/fdroid/ui/details/AppDetailsItem.kt | 1 + .../fdroid/ui/details/AppDetailsViewModel.kt | 5 +++ .../org/fdroid/ui/details/DetailsPresenter.kt | 1 + .../kotlin/org/fdroid/ui/details/Versions.kt | 7 ++- .../org/fdroid/ui/discover/FirstStart.kt | 4 +- .../org/fdroid/ui/repositories/RepoEntry.kt | 3 ++ .../ui/repositories/RepositoriesList.kt | 7 ++- .../ui/repositories/RepositoriesViewModel.kt | 4 ++ .../fdroid/ui/repositories/RepositoryInfo.kt | 2 + .../org/fdroid/ui/repositories/add/AddRepo.kt | 3 +- .../ui/repositories/add/AddRepoIntro.kt | 14 +++--- .../ui/repositories/add/AddRepoViewModel.kt | 6 ++- .../ui/repositories/details/RepoDetails.kt | 7 ++- .../repositories/details/RepoDetailsInfo.kt | 2 + .../details/RepoDetailsViewModel.kt | 4 ++ .../ui/utils/MeteredConnectionDialog.kt | 34 +++++++++++++-- .../org/fdroid/ui/utils/PreviewUtils.kt | 7 +++ app/src/main/res/values/strings.xml | 1 + .../org/fdroid/ui/repositories/AddRepoTest.kt | 6 +++ .../views/repos/ManageReposActivity.java | 2 +- 28 files changed, 162 insertions(+), 35 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt b/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt index 3f4f648c1..af8b28058 100644 --- a/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt +++ b/app/src/main/kotlin/org/fdroid/download/NetworkMonitor.kt @@ -9,11 +9,14 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_NOT_METERED import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.update -import org.fdroid.settings.SettingsConstants.AutoUpdateValues +import kotlinx.coroutines.launch import org.fdroid.settings.SettingsManager +import org.fdroid.utils.IoDispatcher @Singleton class NetworkMonitor @@ -21,18 +24,19 @@ class NetworkMonitor constructor( @param:ApplicationContext private val context: Context, private val settingsManager: SettingsManager, + @param:IoDispatcher private val coroutineScope: CoroutineScope, ) : ConnectivityManager.NetworkCallback() { private val connectivityManager = context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager - private val neverMetered - get() = settingsManager.autoUpdateApps == AutoUpdateValues.Always + private val warnWhenMetered + get() = settingsManager.warnWhenMeteredFlow.value private val _networkState = MutableStateFlow( connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let { - NetworkState(it, neverMetered) - } ?: NetworkState(isOnline = false, isMetered = false) + NetworkState(it, warnWhenMetered) + } ?: NetworkState(isOnline = false, isMetered = false, warnWhenMetered = warnWhenMetered) ) val networkState = _networkState.asStateFlow() @@ -43,24 +47,41 @@ constructor( * we are doing instead is simpler and seems to work better. */ connectivityManager.registerDefaultNetworkCallback(this) + // observe the setting for whether to warn when on a metered network + coroutineScope.launch { + // drop first emission as that is the initial value, and we only want to react to changes + settingsManager.warnWhenMeteredFlow.drop(1).collect { warnWhenMetered -> + if (!warnWhenMetered) { + _networkState.update { it.copy(warnWhenMetered = false) } + } + } + } } override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) { - _networkState.update { NetworkState(networkCapabilities, neverMetered) } + _networkState.update { NetworkState(networkCapabilities, warnWhenMetered) } } override fun onLost(network: Network) { - _networkState.update { NetworkState(isOnline = false, isMetered = false) } + _networkState.update { + NetworkState(isOnline = false, isMetered = false, warnWhenMetered = warnWhenMetered) + } } } -data class NetworkState(val isOnline: Boolean, val isMetered: Boolean) { +data class NetworkState( + val isOnline: Boolean, + private val isMetered: Boolean, + val warnWhenMetered: Boolean = true, +) { + val showWarningDialog: Boolean = isMetered && warnWhenMetered + constructor( networkCapabilities: NetworkCapabilities, - neverMetered: Boolean, + warnWhenMetered: Boolean, ) : this( isOnline = networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET), - isMetered = - if (neverMetered) false else !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED), + isMetered = !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED), + warnWhenMetered = warnWhenMetered, ) } diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt index 531b90735..f6f1423e6 100644 --- a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt +++ b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt @@ -55,6 +55,9 @@ object SettingsConstants { const val PREF_KEY_PREVENT_SCREENSHOTS = "preventScreenshots" const val PREF_DEFAULT_PREVENT_SCREENSHOTS = false + const val PREF_KEY_WARN_WHEN_METERED = "warnWhenMetered" + const val PREF_DEFAULT_WARN_WHEN_METERED = true + const val PREF_KEY_SHOW_INCOMPATIBLE = "incompatibleVersions" const val PREF_DEFAULT_SHOW_INCOMPATIBLE = true diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt index 8aaa35e71..2c3ac978a 100644 --- a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt @@ -34,6 +34,7 @@ import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_REPO_UPDATES import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_SHOW_INCOMPATIBLE import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_THEME +import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_WARN_WHEN_METERED import org.fdroid.settings.SettingsConstants.PREF_DNS_CACHE import org.fdroid.settings.SettingsConstants.PREF_DNS_CACHE_DEFAULT import org.fdroid.settings.SettingsConstants.PREF_KEY_APP_LIST_SORT_ORDER @@ -50,6 +51,7 @@ import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY import org.fdroid.settings.SettingsConstants.PREF_KEY_REPO_UPDATES import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_INCOMPATIBLE import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME +import org.fdroid.settings.SettingsConstants.PREF_KEY_WARN_WHEN_METERED import org.fdroid.settings.SettingsConstants.PREF_USE_DNS_CACHE import org.fdroid.settings.SettingsConstants.PREF_USE_DNS_CACHE_DEFAULT import org.fdroid.settings.SettingsConstants.getAppListSortOrder @@ -188,6 +190,10 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val return prefs.edit { putString(PREF_DNS_CACHE, value) } } + private val _warnWhenMeteredFlow = + MutableStateFlow(prefs.getBoolean(PREF_KEY_WARN_WHEN_METERED, PREF_DEFAULT_WARN_WHEN_METERED)) + val warnWhenMeteredFlow = _warnWhenMeteredFlow.asStateFlow() + val filterIncompatible: Boolean get() = !prefs.getBoolean(PREF_KEY_SHOW_INCOMPATIBLE, PREF_DEFAULT_SHOW_INCOMPATIBLE) @@ -206,6 +212,11 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val prefs.edit { putString(PREF_KEY_MY_APPS_SORT_ORDER, value.toSettings()) } } + fun onDontWarnOnMeteredNetwork() { + prefs.edit { putBoolean(PREF_KEY_WARN_WHEN_METERED, false) } + _warnWhenMeteredFlow.update { false } + } + fun saveAppListFilter(sortOrder: AppListSortOrder, filterIncompatible: Boolean) { prefs.edit { putBoolean(PREF_KEY_SHOW_INCOMPATIBLE, !filterIncompatible) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt index f6aed4cca..d804dd4e7 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt @@ -37,5 +37,7 @@ interface MyAppsActions { fun onAppIssueHintSeen() + fun onNotWarnWhenMetered() + fun exportInstalledApps() } diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt index 4bd51b2b7..2aa082bce 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt @@ -103,7 +103,7 @@ fun MyAppsList( myAppsInfo.actions.updateAll() showUpdateAllButton = false } - if (myAppsInfo.model.networkState.isMetered) { + if (myAppsInfo.model.networkState.showWarningDialog) { showMeteredDialog = installLambda } else { installLambda() @@ -251,7 +251,10 @@ fun MyAppsList( if (meteredLambda != null) MeteredConnectionDialog( numBytes = myAppsInfo.model.appUpdatesBytes, - onConfirm = { meteredLambda() }, + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) myAppsInfo.actions.onNotWarnWhenMetered() + meteredLambda() + }, onDismiss = { showMeteredDialog = null }, ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt index 49f9cf700..53509b0df 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsViewModel.kt @@ -144,6 +144,10 @@ constructor( override fun onAppIssueHintSeen() = onboardingManager.onAppIssueHintSeen() + override fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + override fun exportInstalledApps() { scope.launch { val stringBuilder = diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index 135266d8f..6f030ef01 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -259,7 +259,7 @@ fun AppDetails( } // Screenshots if (item.phoneScreenshots.isNotEmpty()) { - Screenshots(item.networkState.isMetered, item.phoneScreenshots) + Screenshots(item.networkState.showWarningDialog, item.phoneScreenshots) } // Donate card if (item.showDonate) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt index 86276dc3d..6538d0860 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt @@ -304,7 +304,7 @@ fun AppDetailsHeader( // button is for either installing or updating Button( onClick = { - if (item.networkState.isMetered) { + if (item.networkState.showWarningDialog) { showMeteredDialog = true } else { require(item.suggestedVersion != null) { "suggestedVersion was null" } @@ -325,7 +325,8 @@ fun AppDetailsHeader( if (showMeteredDialog) MeteredConnectionDialog( numBytes = version?.size, - onConfirm = { + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) item.actions.onNotWarnWhenMetered() require(item.suggestedVersion != null) { "suggestedVersion was null" } item.actions.installAction(item.app, item.suggestedVersion, item.icon) }, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt index b3956eb27..98b7e5395 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsItem.kt @@ -242,6 +242,7 @@ data class AppDetailsActions( val onUninstallResult: (ActivityResult) -> Unit, val onRepoChanged: (Long) -> Unit, val onPreferredRepoChanged: (Long) -> Unit, + val onNotWarnWhenMetered: () -> Unit, val allowBetaVersions: () -> Unit, val onAntiFeaturesOnboardingSeen: () -> Unit, val ignoreAllUpdates: (() -> Unit)? = null, diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt index bee95654d..11b310122 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsViewModel.kt @@ -183,6 +183,11 @@ constructor( } } + @UiThread + fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + override fun onCleared() { log.info { "App details screen left: $packageName" } appInstallManager.cleanUp(packageName) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt index a07c4734a..1e713da20 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/DetailsPresenter.kt @@ -206,6 +206,7 @@ fun DetailsPresenter( onUninstallResult = viewModel::onUninstallResult, onRepoChanged = viewModel::onRepoChanged, onPreferredRepoChanged = viewModel::onPreferredRepoChanged, + onNotWarnWhenMetered = viewModel::onNotWarnWhenMetered, allowBetaVersions = viewModel::allowBetaUpdates, onAntiFeaturesOnboardingSeen = viewModel::onAntiFeaturesOnboardingSeen, ignoreAllUpdates = diff --git a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt index f6d3da2bb..9b86974d8 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/Versions.kt @@ -57,10 +57,11 @@ fun Versions(item: AppDetailsItem, scrollUp: suspend () -> Unit) { item.versions?.forEach { versionItem -> Version( item = versionItem, - isMetered = item.networkState.isMetered, + isMetered = item.networkState.showWarningDialog, installAction = { version: AppVersion -> item.actions.installAction(item.app, version, item.icon) }, + onNotWarnWhenMetered = item.actions.onNotWarnWhenMetered, scrollUp = scrollUp, ) } @@ -73,6 +74,7 @@ fun Version( item: VersionItem, isMetered: Boolean, installAction: (AppVersion) -> Unit, + onNotWarnWhenMetered: () -> Unit, scrollUp: suspend () -> Unit, ) { val isPreview = LocalInspectionMode.current @@ -215,7 +217,8 @@ fun Version( if (showMeteredDialog) MeteredConnectionDialog( numBytes = item.version.size, - onConfirm = { + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) onNotWarnWhenMetered() installAction(item.version as AppVersion) coroutineScope.launch { scrollUp() } }, diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt b/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt index 3ccbfac31..3741ace95 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/FirstStart.kt @@ -52,10 +52,10 @@ fun FirstStart( // TODO it would be nice to surface normal update errors better and also let the user retry } Column(verticalArrangement = Center, modifier = modifier) { - if ((!networkState.isOnline || networkState.isMetered) && !override) { + if ((!networkState.isOnline || networkState.showWarningDialog) && !override) { // offline or metered, not overridden val res = - if (networkState.isMetered) { + if (networkState.showWarningDialog) { stringResource(R.string.first_start_metered) } else { stringResource(R.string.first_start_offline) diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt index 4767175ba..d308ab15f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepoEntry.kt @@ -46,6 +46,8 @@ fun EntryProviderScope.repoEntry(navigator: Navigator, isBigScreen: Bool override fun onRepositoryEnabled(repoId: Long, enabled: Boolean) = viewModel.onRepositoryEnabled(repoId, enabled) + override fun onNotWarnWhenMetered() = viewModel.onNotWarnWhenMetered() + override fun onAddRepo() { navigator.navigate(NavigationKey.AddRepo()) } @@ -94,6 +96,7 @@ fun EntryProviderScope.repoEntry(navigator: Navigator, isBigScreen: Bool networkStateFlow = viewModel.networkState, proxyConfig = viewModel.proxyConfig, onFetchRepo = viewModel::onFetchRepo, + onNotWarnWhenMetered = viewModel::onNotWarnWhenMetered, onAddRepo = viewModel::addFetchedRepository, onExistingRepo = { repoId -> navigator.goBack() diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt index 12e6fda04..be5398730 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt @@ -94,7 +94,7 @@ fun RepositoriesList( isSelected = isSelected, onRepoEnabled = { enabled -> if (enabled) { - if (info.model.networkState.isMetered) + if (info.model.networkState.showWarningDialog) showMeteredDialog = { info.onRepositoryEnabled(repoItem.repoId, true) } else info.onRepositoryEnabled(repoItem.repoId, true) } else { @@ -170,7 +170,10 @@ fun RepositoriesList( if (meteredLambda != null) MeteredConnectionDialog( numBytes = null, - onConfirm = { meteredLambda() }, + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) info.onNotWarnWhenMetered() + meteredLambda() + }, onDismiss = { showMeteredDialog = null }, ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt index 1bef2f631..e24dfa320 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt @@ -95,6 +95,10 @@ constructor( } } + fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + fun onRepositoriesMoved(fromRepoId: Long, toRepoId: Long) { log.info { "onRepositoriesMoved($fromRepoId, $toRepoId)" } repoSortingMap.update { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt index b07c23bb0..c30d8d700 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt @@ -12,6 +12,8 @@ interface RepositoryInfo { fun onRepositoryEnabled(repoId: Long, enabled: Boolean) + fun onNotWarnWhenMetered() + fun onAddRepo() fun onRepositoryMoved(fromRepoId: Long, toRepoId: Long) diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt index 42b09452c..3dc4b1914 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepo.kt @@ -37,6 +37,7 @@ fun AddRepo( networkStateFlow: StateFlow, proxyConfig: ProxyConfig?, onFetchRepo: (String) -> Unit, + onNotWarnWhenMetered: () -> Unit, onAddRepo: () -> Unit, onExistingRepo: (Long) -> Unit, onRepoAdded: (String, Long) -> Unit, @@ -74,7 +75,7 @@ fun AddRepo( when (state) { None -> { val networkState = networkStateFlow.collectAsStateWithLifecycle().value - AddRepoIntroContent(networkState, onFetchRepo, paddingValues) + AddRepoIntroContent(networkState, onFetchRepo, onNotWarnWhenMetered, paddingValues) } is Fetching -> { if (state.receivedRepo == null) { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt index e6199b65d..92b784a39 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoIntro.kt @@ -88,6 +88,7 @@ import org.fdroid.ui.utils.startActivitySafe fun AddRepoIntroContent( networkState: NetworkState, onFetchRepo: (String) -> Unit, + onNotWarnWhenMetered: () -> Unit, paddingValues: PaddingValues = PaddingValues(), ) { val scrollState = rememberScrollState() @@ -150,7 +151,7 @@ fun AddRepoIntroContent( Unit } } - if (networkState.isMetered) showMeteredDialog = scanLambda else scanLambda() + if (networkState.showWarningDialog) showMeteredDialog = scanLambda else scanLambda() }, ) AnimatedVisibility( @@ -229,7 +230,7 @@ fun AddRepoIntroContent( FDroidButton( text = stringResource(R.string.repo_add_add), onClick = { - if (networkState.isMetered) + if (networkState.showWarningDialog) showMeteredDialog = { onFetchRepo(textState.value.text) } else onFetchRepo(textState.value.text) }, @@ -244,7 +245,10 @@ fun AddRepoIntroContent( if (meteredLambda != null) MeteredConnectionDialog( numBytes = null, - onConfirm = { meteredLambda() }, + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) onNotWarnWhenMetered() + meteredLambda() + }, onDismiss = { showMeteredDialog = null }, ) } @@ -254,7 +258,7 @@ fun AddRepoIntroContent( private fun Preview() { FDroidContent { val networkStateFlow = MutableStateFlow(NetworkState(isOnline = true, isMetered = false)) - AddRepo(None, networkStateFlow, null, {}, {}, {}, { _, _ -> }) {} + AddRepo(None, networkStateFlow, null, {}, {}, {}, {}, { _, _ -> }, {}) } } @@ -263,6 +267,6 @@ private fun Preview() { private fun PreviewNight() { FDroidContent { val networkStateFlow = MutableStateFlow(NetworkState(isOnline = false, isMetered = false)) - AddRepo(None, networkStateFlow, null, {}, {}, {}, { _, _ -> }) {} + AddRepo(None, networkStateFlow, null, {}, {}, {}, {}, { _, _ -> }, {}) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt index b128cf5d7..f15685739 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/add/AddRepoViewModel.kt @@ -28,7 +28,7 @@ class AddRepoViewModel constructor( app: Application, networkMonitor: NetworkMonitor, - settingsManager: SettingsManager, + private val settingsManager: SettingsManager, private val repoManager: RepoManager, private val updateManager: UpdatesManager, @param:IoDispatcher private val ioScope: CoroutineScope, @@ -74,4 +74,8 @@ constructor( } } } + + fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt index 75347781c..ee96e3327 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt @@ -95,7 +95,10 @@ fun RepoDetails( if (meteredLambda != null) MeteredConnectionDialog( numBytes = null, - onConfirm = { meteredLambda() }, + onConfirm = { notWarnWhenMetered -> + if (notWarnWhenMetered) info.actions.onNotWarnWhenMetered() + meteredLambda() + }, onDismiss = { showMeteredDialog = null }, ) Scaffold( @@ -120,7 +123,7 @@ fun RepoDetails( contentDescription = stringResource(R.string.repo_force_update), enabled = info.model.isUpdateButtonEnabled, onClick = { - if (info.model.networkState.isMetered) + if (info.model.networkState.showWarningDialog) showMeteredDialog = { RepoUpdateWorker.updateNow(context, repo.repoId) } else RepoUpdateWorker.updateNow(context, repo.repoId) }, diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt index e0ac6f31b..3727505b6 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsInfo.kt @@ -32,6 +32,8 @@ interface RepoDetailsActions { fun setArchiveRepoEnabled(enabled: Boolean) + fun onNotWarnWhenMetered() + fun onOnboardingSeen() suspend fun generateQrCode(repo: Repository): Bitmap? { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt index 76acfe406..a828c36be 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetailsViewModel.kt @@ -154,6 +154,10 @@ constructor( } } + override fun onNotWarnWhenMetered() { + settingsManager.onDontWarnOnMeteredNetwork() + } + override fun onOnboardingSeen() = onboardingManager.onRepoDetailsOnboardingSeen() private fun Repository.archiveState(): ArchiveState { diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt b/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt index d32516784..8aff4347c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/MeteredConnectionDialog.kt @@ -1,18 +1,34 @@ package org.fdroid.ui.utils import android.text.format.Formatter +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp import org.fdroid.R @Composable -fun MeteredConnectionDialog(numBytes: Long?, onConfirm: () -> Unit, onDismiss: () -> Unit) { +fun MeteredConnectionDialog( + numBytes: Long?, + onConfirm: (Boolean) -> Unit, + onDismiss: () -> Unit, +) { + var dontShowAgain by remember { mutableStateOf(false) } AlertDialog( title = { Text(text = stringResource(R.string.dialog_metered_title)) }, text = { @@ -23,14 +39,26 @@ fun MeteredConnectionDialog(numBytes: Long?, onConfirm: () -> Unit, onDismiss: ( val sizeStr = Formatter.formatFileSize(LocalContext.current, numBytes) stringResource(R.string.dialog_metered_text, sizeStr) } - Text(text = s) + Column { + Text(text = s) + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp), + ) { + Checkbox(checked = dontShowAgain, onCheckedChange = { dontShowAgain = it }) + Text( + text = stringResource(R.string.dialog_metered_dont_show), + modifier = Modifier.padding(start = 8.dp), + ) + } + } }, onDismissRequest = onDismiss, confirmButton = { TextButton( onClick = { onDismiss() - onConfirm() + onConfirm(dontShowAgain) } ) { Text( diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index f324bd2b0..1d55eb87d 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -256,6 +256,7 @@ fun getAppDetailsActions() = onUninstallResult = { _ -> }, onRepoChanged = {}, onPreferredRepoChanged = {}, + onNotWarnWhenMetered = {}, allowBetaVersions = {}, onAntiFeaturesOnboardingSeen = {}, ignoreAllUpdates = {}, @@ -412,6 +413,8 @@ fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = override fun onAppIssueHintSeen() {} + override fun onNotWarnWhenMetered() {} + override fun exportInstalledApps() {} } } @@ -553,6 +556,8 @@ fun getRepositoriesInfo(model: RepositoryModel, currentRepositoryId: Long? = nul override fun onRepositoryEnabled(repoId: Long, enabled: Boolean) {} + override fun onNotWarnWhenMetered() {} + override fun onAddRepo() {} override fun onRepositoryMoved(fromRepoId: Long, toRepoId: Long) {} @@ -604,6 +609,8 @@ fun getRepoDetailsInfo( override fun setArchiveRepoEnabled(enabled: Boolean) {} + override fun onNotWarnWhenMetered() {} + override fun onOnboardingSeen() {} } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a893fd847..54c3a5624 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -244,6 +244,7 @@ This often occurs with apps installed via Google Play or other sources, if they Downloading %1$s may incur charges depending on your data plan Downloading may incur charges depending on your data plan + Don\'t show this again Download Tap to load screenshots via mobile data diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt index ec6696fc4..4e7d71027 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/repositories/AddRepoTest.kt @@ -29,6 +29,7 @@ fun AddRepoIntroTest() = networkStateFlow = networkStateFlow, proxyConfig = null, onFetchRepo = {}, + onNotWarnWhenMetered = {}, onAddRepo = {}, onExistingRepo = {}, onRepoAdded = { _, _ -> }, @@ -50,6 +51,7 @@ fun AddRepoIntroNightTest() = networkStateFlow = MutableStateFlow(NetworkState(isOnline = false, isMetered = false)), proxyConfig = null, onFetchRepo = {}, + onNotWarnWhenMetered = {}, onAddRepo = {}, onExistingRepo = {}, onRepoAdded = { _, _ -> }, @@ -73,6 +75,7 @@ fun AddRepoFetchingProgressTest() = networkStateFlow = networkStateFlow, proxyConfig = null, onFetchRepo = {}, + onNotWarnWhenMetered = {}, onAddRepo = {}, onExistingRepo = {}, onRepoAdded = { _, _ -> }, @@ -97,6 +100,7 @@ fun AddRepoFetchingPreviewTest() = networkStateFlow = networkStateFlow, proxyConfig = null, onFetchRepo = {}, + onNotWarnWhenMetered = {}, onAddRepo = {}, onExistingRepo = {}, onRepoAdded = { _, _ -> }, @@ -114,6 +118,7 @@ fun AddRepoAddingTest() = networkStateFlow = networkStateFlow, proxyConfig = null, onFetchRepo = {}, + onNotWarnWhenMetered = {}, onAddRepo = {}, onExistingRepo = {}, onRepoAdded = { _, _ -> }, @@ -131,6 +136,7 @@ fun AddRepoErrorTest() = networkStateFlow = networkStateFlow, proxyConfig = null, onFetchRepo = {}, + onNotWarnWhenMetered = {}, onAddRepo = {}, onExistingRepo = {}, onRepoAdded = { _, _ -> }, diff --git a/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java b/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java index be561936a..d28dd8f8a 100644 --- a/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java +++ b/legacy/src/main/java/org/fdroid/fdroid/views/repos/ManageReposActivity.java @@ -213,7 +213,7 @@ public class ManageReposActivity extends AppCompatActivity implements RepoAdapte public void onToggleEnabled(Repository repo) { if (repo.getEnabled()) { new MaterialAlertDialogBuilder(this) - .setMessage(R.string.repo_disable_warning) + .setMessage(R.string.repo_disable_dialog_warning) .setPositiveButton(R.string.repo_disable_warning_button, (dialog, id) -> { disableRepo(repo); dialog.dismiss(); From aa6965d79b22372bee5a1d7960ddf6b464139cb1 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Thu, 16 Apr 2026 17:54:02 -0300 Subject: [PATCH 12/38] [download] Ask for compression when making HTTP requests --- gradle/libs.versions.toml | 1 + gradle/verification-metadata.xml | 10 ++++++++++ libs/download/build.gradle.kts | 1 + .../kotlin/org/fdroid/download/HttpManager.kt | 5 +++++ .../kotlin/org/fdroid/download/HttpManagerTest.kt | 4 +++- 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a19e13c91..0588afe58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -172,6 +172,7 @@ ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-mock = { module = "io.ktor:ktor-client-mock", version.ref = "ktor" } ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } +ktor-client-encoding = { module = "io.ktor:ktor-client-encoding", version.ref = "ktor" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } goncalossilva-resources = { module = "com.goncalossilva:resources", version.ref = "goncalossilvaResources" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index efecf23a6..0be71feb9 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3298,6 +3298,16 @@ + + + + + + + + + + diff --git a/libs/download/build.gradle.kts b/libs/download/build.gradle.kts index 227e2ce27..e4e637b60 100644 --- a/libs/download/build.gradle.kts +++ b/libs/download/build.gradle.kts @@ -42,6 +42,7 @@ kotlin { dependencies { api(project(":libs:core")) api(libs.ktor.client.core) + implementation(libs.ktor.client.encoding) implementation(libs.microutils.kotlin.logging) } } diff --git a/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt b/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt index 20c8d7dd6..82896c80a 100644 --- a/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt +++ b/libs/download/src/commonMain/kotlin/org/fdroid/download/HttpManager.kt @@ -6,6 +6,7 @@ import io.ktor.client.engine.ProxyConfig import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.ResponseException import io.ktor.client.plugins.UserAgent +import io.ktor.client.plugins.compression.ContentEncoding import io.ktor.client.plugins.timeout import io.ktor.client.request.HttpRequestBuilder import io.ktor.client.request.basicAuth @@ -84,6 +85,10 @@ constructor( proxy = proxyConfig } install(UserAgent) { agent = userAgent } + install(ContentEncoding) { + deflate() + gzip() + } install(HttpTimeout) { if (highTimeouts || proxyConfig.isTor()) { connectTimeoutMillis = TIMEOUT_MILLIS_HIGH diff --git a/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt b/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt index b061973b4..5e4d1b9e6 100644 --- a/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt +++ b/libs/download/src/commonTest/kotlin/org/fdroid/download/HttpManagerTest.kt @@ -13,6 +13,7 @@ import io.ktor.client.engine.mock.respondRedirect import io.ktor.client.plugins.ClientRequestException import io.ktor.client.plugins.RedirectResponseException import io.ktor.client.plugins.ServerResponseException +import io.ktor.http.HttpHeaders.AcceptEncoding import io.ktor.http.HttpHeaders.Authorization import io.ktor.http.HttpHeaders.ETag import io.ktor.http.HttpHeaders.UserAgent @@ -50,7 +51,7 @@ internal class HttpManagerTest { private val downloadRequest = DownloadRequest("foo", mirrors) @Test - fun testUserAgent() = runSuspend { + fun testUserAgentAndCompression() = runSuspend { val mockEngine = MockEngine { respondOk() } val httpManager = HttpManager(userAgent, null, httpClientEngineFactory = get(mockEngine)) @@ -59,6 +60,7 @@ internal class HttpManagerTest { mockEngine.requestHistory.forEach { request -> assertEquals(userAgent, request.headers[UserAgent]) + assertEquals("deflate,gzip", request.headers[AcceptEncoding]) } } From c2d3bb7586906f8f94a89ccf39c8e8c2497eab05 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 17 Apr 2026 10:48:54 -0300 Subject: [PATCH 13/38] Prevent accidental dismiss of onboarding hints Now, the user needs to press the 'Got it' button to dismiss the hint. Performing back gesture also still works. --- .../org/fdroid/ui/details/AntiFeatures.kt | 45 ++++++++++++++++-- .../org/fdroid/ui/details/AppDetails.kt | 47 +++++++++---------- .../kotlin/org/fdroid/ui/lists/AppList.kt | 24 +++++----- .../fdroid/ui/repositories/Repositories.kt | 26 +++++----- .../ui/repositories/details/RepoDetails.kt | 26 +++++----- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 15 ++++++ 7 files changed, 120 insertions(+), 65 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt index 270d69347..fc18fdd08 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AntiFeatures.kt @@ -15,13 +15,23 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter.Companion.tint import androidx.compose.ui.graphics.vector.rememberVectorPainter +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.onVisibilityChanged import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.viktormykhailiv.compose.hints.HintAnchorState +import com.viktormykhailiv.compose.hints.hintAnchor +import com.viktormykhailiv.compose.hints.rememberHint +import com.viktormykhailiv.compose.hints.rememberHintAnchorState import org.fdroid.R import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.AsyncShimmerImage @@ -30,11 +40,38 @@ import org.fdroid.ui.utils.testApp @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun AntiFeatures(antiFeatures: List, modifier: Modifier = Modifier) { +fun AntiFeatures( + antiFeatures: List, + hintAnchor: HintAnchorState, + showOnboarding: Boolean, + modifier: Modifier = Modifier, + onShowOnboarding: () -> Unit, +) { + var placed by remember { mutableStateOf(false) } + var visible by remember { mutableStateOf(false) } ElevatedCard( colors = CardDefaults.elevatedCardColors(containerColor = MaterialTheme.colorScheme.inverseSurface), - modifier = modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .hintAnchor(state = hintAnchor, shape = CardDefaults.elevatedShape) + .onPlaced { + if (visible && showOnboarding) { + onShowOnboarding() + } else { + placed = true + } + } + .onVisibilityChanged(minDurationMs = 1000) { isVisible -> + if (isVisible && placed && showOnboarding) { + onShowOnboarding() + } else if (isVisible) { + visible = true + } + } + .padding(8.dp), ) { ExpandableSection( icon = rememberVectorPainter(Icons.Default.WarningAmber), @@ -81,5 +118,7 @@ fun AntiFeatures(antiFeatures: List, modifier: Modifier = Modifier) @Preview @Composable fun AntiFeaturesPreview() { - FDroidContent { AntiFeatures(testApp.antiFeatures!!) } + val hint = rememberHint {} + val hintAnchor = rememberHintAnchorState(hint) + FDroidContent { AntiFeatures(testApp.antiFeatures!!, hintAnchor, showOnboarding = false) {} } } diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index 6f030ef01..0d238eae2 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -49,7 +48,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.LiveRegionMode @@ -69,11 +67,10 @@ import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.core.os.LocaleListCompat -import com.viktormykhailiv.compose.hints.hintAnchor +import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState import com.viktormykhailiv.compose.hints.rememberHintController -import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.fdroid.LocaleChooser.getBestLocale import org.fdroid.R @@ -120,17 +117,18 @@ fun AppDetails( } // onboarding hint plumbing val hintController = rememberHintController(overlay = getHintOverlayColor()) - val hint = rememberHint { - OnboardingPopupCard( - title = stringResource(R.string.app_details_anti_features_title), - message = stringResource(R.string.app_details_anti_features_text), - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), - onGotIt = { - item.actions.onAntiFeaturesOnboardingSeen() - hintController.dismiss() - }, - ) - } + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + OnboardingPopupCard( + title = stringResource(R.string.app_details_anti_features_title), + message = stringResource(R.string.app_details_anti_features_text), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + item.actions.onAntiFeaturesOnboardingSeen() + hintController.dismiss() + }, + ) + } val hintAnchor = rememberHintAnchorState(hint) val coroutineScope = rememberCoroutineScope() val scrollState = rememberScrollState() @@ -245,17 +243,14 @@ fun AppDetails( if (!item.antiFeatures.isNullOrEmpty()) { AntiFeatures( antiFeatures = item.antiFeatures, - modifier = - Modifier.hintAnchor(state = hintAnchor, shape = RoundedCornerShape(16.dp)).onPlaced { - if (item.showAntiFeaturesOnboarding) { - coroutineScope.launch { - delay(500) // we still need a delay to not highlight the wrong place - hintController.show(hintAnchor) - item.actions.onAntiFeaturesOnboardingSeen() - } - } - }, - ) + hintAnchor = hintAnchor, + showOnboarding = item.showAntiFeaturesOnboarding, + ) { + coroutineScope.launch { + hintController.show(hintAnchor) + item.actions.onAntiFeaturesOnboardingSeen() + } + } } // Screenshots if (item.phoneScreenshots.isNotEmpty()) { diff --git a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt index 7de8fcae4..0cb682620 100644 --- a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt @@ -51,6 +51,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.viktormykhailiv.compose.hints.HintHost +import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.hintAnchor import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState @@ -80,17 +81,18 @@ fun AppList( val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) val hintController = rememberHintController(overlay = getHintOverlayColor()) - val hint = rememberHint { - OnboardingPopupCard( - title = stringResource(R.string.onboarding_app_list_filter_title), - message = stringResource(R.string.onboarding_app_list_filter_message), - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), - onGotIt = { - appListInfo.actions.onOnboardingSeen() - hintController.dismiss() - }, - ) - } + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + OnboardingPopupCard( + title = stringResource(R.string.onboarding_app_list_filter_title), + message = stringResource(R.string.onboarding_app_list_filter_message), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + appListInfo.actions.onOnboardingSeen() + hintController.dismiss() + }, + ) + } val hintAnchor = rememberHintAnchorState(hint) LaunchedEffect(appListInfo.showOnboarding) { if (appListInfo.showOnboarding) { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt index d5f7cad13..87d98889a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt @@ -36,6 +36,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.viktormykhailiv.compose.hints.HintHost +import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState import com.viktormykhailiv.compose.hints.rememberHintController @@ -52,19 +53,20 @@ import org.fdroid.ui.utils.repoItems @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) fun Repositories(info: RepositoryInfo, isBigScreen: Boolean, onBackClicked: () -> Unit) { val hintController = rememberHintController(overlay = getHintOverlayColor()) - val hint = rememberHint { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - OnboardingPopupCard( - title = stringResource(R.string.repo_list_info_title), - message = stringResource(R.string.repo_list_info_text), - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), - onGotIt = { - info.onOnboardingSeen() - hintController.dismiss() - }, - ) + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + OnboardingPopupCard( + title = stringResource(R.string.repo_list_info_title), + message = stringResource(R.string.repo_list_info_text), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + info.onOnboardingSeen() + hintController.dismiss() + }, + ) + } } - } val hintAnchor = rememberHintAnchorState(hint) LaunchedEffect(info.model.showOnboarding) { if (!isBigScreen && info.model.showOnboarding) { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt index ee96e3327..13d6279a9 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.viktormykhailiv.compose.hints.HintHost +import com.viktormykhailiv.compose.hints.HintProperties import com.viktormykhailiv.compose.hints.rememberHint import com.viktormykhailiv.compose.hints.rememberHintAnchorState import com.viktormykhailiv.compose.hints.rememberHintController @@ -56,19 +57,20 @@ fun RepoDetails( val repo = info.model.repo val hintController = rememberHintController(overlay = getHintOverlayColor()) - val hint = rememberHint { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - OnboardingPopupCard( - title = stringResource(R.string.repo_details), - message = stringResource(R.string.repo_details_info_text), - modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), - onGotIt = { - info.actions.onOnboardingSeen() - hintController.dismiss() - }, - ) + val hint = + rememberHint(HintProperties(dismissOnClickOutside = false)) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + OnboardingPopupCard( + title = stringResource(R.string.repo_details), + message = stringResource(R.string.repo_details_info_text), + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp), + onGotIt = { + info.actions.onOnboardingSeen() + hintController.dismiss() + }, + ) + } } - } val hintAnchor = rememberHintAnchorState(hint) LaunchedEffect(info.model.showOnboarding) { if (info.model.showOnboarding) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0588afe58..c869934a1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ glide = "5.0.5" glideCompose = "1.0.0-beta08" coilCompose = "3.4.0" molecule = "2.2.0" -hints = "2.0.1" +hints = "2.1.0" composePreference = "2.2.0" androidxCoreKtx = "1.18.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 0be71feb9..76dc9049e 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3033,6 +3033,11 @@ + + + + + @@ -4363,11 +4368,21 @@ + + + + + + + + + + From 87aab92675c81073f8c66350d0d1039bfcdf461f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 20 Apr 2026 15:46:03 -0300 Subject: [PATCH 14/38] Upgrade compose hints onboarding library to fix a crash --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c869934a1..f87676f83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ glide = "5.0.5" glideCompose = "1.0.0-beta08" coilCompose = "3.4.0" molecule = "2.2.0" -hints = "2.1.0" +hints = "2.2.1" composePreference = "2.2.0" androidxCoreKtx = "1.18.0" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 76dc9049e..a608546b7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -3038,6 +3038,11 @@ + + + + + From b5e21a7f323a99b1ebbe845e3452108ce780b253 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 21 Apr 2026 11:51:29 -0300 Subject: [PATCH 15/38] Add screenshot test for empty My Apps --- .../kotlin/org/fdroid/ui/apps/MyAppsTest.kt | 25 +++++++++++++++++++ .../MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png | 3 +++ 2 files changed, 28 insertions(+) create mode 100644 app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt index cbcdfeeef..42f08c6ec 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/apps/MyAppsTest.kt @@ -11,6 +11,31 @@ import org.fdroid.ui.ScreenshotTest import org.fdroid.ui.utils.getMyAppsInfo import org.fdroid.ui.utils.getPreviewVersion +@Composable +@PreviewTest +@Preview(showBackground = true, showSystemUi = true) +fun MyAppsEmpty() { + ScreenshotTest(numUpdates = 0, hasAppIssues = false) { + val model = + MyAppsModel( + appUpdates = emptyList(), + installingApps = emptyList(), + appsWithIssue = null, + installedApps = emptyList(), + showUpdatesHint = true, + showAppIssueHint = false, + sortOrder = AppListSortOrder.NAME, + networkState = NetworkState(isOnline = true, isMetered = false), + ) + MyApps( + myAppsInfo = getMyAppsInfo(model), + currentPackageName = null, + onAppItemClick = {}, + onNav = {}, + ) + } +} + @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true) diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png new file mode 100644 index 000000000..a3fe519ee --- /dev/null +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1945387654f00f1a8e91757b6d8ae700d35b1fc1b078118f7866a938cc5b5598 +size 44161 From a2f6947c7945e98a65854796cb912f746b9a948f Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 21 Apr 2026 12:11:31 -0300 Subject: [PATCH 16/38] Show onboardings again after backing out --- app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt | 5 +---- app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt | 1 - .../main/kotlin/org/fdroid/ui/repositories/Repositories.kt | 4 ++-- .../kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt | 1 - 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt index 0d238eae2..765bba6f7 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetails.kt @@ -246,10 +246,7 @@ fun AppDetails( hintAnchor = hintAnchor, showOnboarding = item.showAntiFeaturesOnboarding, ) { - coroutineScope.launch { - hintController.show(hintAnchor) - item.actions.onAntiFeaturesOnboardingSeen() - } + coroutineScope.launch { hintController.show(hintAnchor) } } } // Screenshots diff --git a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt index 0cb682620..db68890c9 100644 --- a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt @@ -97,7 +97,6 @@ fun AppList( LaunchedEffect(appListInfo.showOnboarding) { if (appListInfo.showOnboarding) { hintController.show(hintAnchor) - appListInfo.actions.onOnboardingSeen() } } diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt index 87d98889a..6d8c4dfc1 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp import com.viktormykhailiv.compose.hints.HintHost import com.viktormykhailiv.compose.hints.HintProperties @@ -71,7 +72,6 @@ fun Repositories(info: RepositoryInfo, isBigScreen: Boolean, onBackClicked: () - LaunchedEffect(info.model.showOnboarding) { if (!isBigScreen && info.model.showOnboarding) { hintController.show(hintAnchor) - info.onOnboardingSeen() } } val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) @@ -155,8 +155,8 @@ fun RepositoriesScaffoldLoadingPreview() { } } -@Preview(showBackground = true, uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL) @Composable +@PreviewLightDark private fun RepositoriesScaffoldPreview() { HintHost { FDroidContent { diff --git a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt index 13d6279a9..dba3c963b 100644 --- a/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt +++ b/app/src/main/kotlin/org/fdroid/ui/repositories/details/RepoDetails.kt @@ -75,7 +75,6 @@ fun RepoDetails( LaunchedEffect(info.model.showOnboarding) { if (info.model.showOnboarding) { hintController.show(hintAnchor) - info.actions.onOnboardingSeen() } } From f1efe874d9c5934e00b25cd1ea5c469c2ead8520 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 21 Apr 2026 12:34:12 -0300 Subject: [PATCH 17/38] Update dependencies In reaction to https://deptools.io/demo/f-droid/fdroidclient --- gradle/libs.versions.toml | 19 +- gradle/verification-metadata.xml | 370 +++++++++++++++++++++++++++++++ legacy/build.gradle | 2 + 3 files changed, 381 insertions(+), 10 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f87676f83..2d11b55cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] compileSdk = "36" kotlin = "2.3.20" -androidGradlePlugin = "9.1.0" +androidGradlePlugin = "9.1.1" androidKspPlugin = "2.3.6" hilt = "2.59.2" hiltWork = "1.3.0" @@ -11,8 +11,8 @@ mavenPublish = "0.35.0" screenshot = "0.0.1-alpha13" ktfmt = "0.26.0" -kotlinxSerializationCore = "1.10.0" -kotlinxSerializationJson = "1.10.0" +kotlinxSerializationCore = "1.11.0" +kotlinxSerializationJson = "1.11.0" kotlinxCoroutinesTest = "1.10.2" ktor = "3.4.2" @@ -43,7 +43,7 @@ androidxActivityCompose = "1.13.0" accompanistDrawablepainter = "0.37.3" # navigation3 -nav3Core = "1.1.0-rc01" +nav3Core = "1.1.0" lifecycleViewmodelNav3 = "2.10.0" material3AdaptiveNav3 = "1.0.0-alpha03" @@ -54,14 +54,13 @@ guardianprojectNetcipher = "2.2.0-alpha" guardianprojectPanic = "1.0" acra = "5.13.1" adapterdelegates4 = "4.3.2" -#noinspection GradleDependency Commons IO > 2.5 uses java.nio.file, which requires desugaring -commonsIo = "2.6" -commonsNet = "3.6" -bouncycastle = "1.83" +commonsIo = "2.21.0" +commonsNet = "3.13.0" +bouncycastle = "1.84" #noinspection NewerVersionAvailable upgrade to 3.6.1 failed BonjourManagerTest jmdns = "3.5.5" nanohttpd = "2.3.1" -guava = "33.5.0-android" +guava = "33.6.0-android" rxjava = "3.1.12" rxandroid = "3.0.2" @@ -87,7 +86,7 @@ mockitoCore = "5.1.1" hamcrest = "2.2" goncalossilvaResources = "0.15.0" turbine = "1.2.1" -json = "20220320" +json = "20251224" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a608546b7..1a14e07d8 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -933,6 +933,11 @@ + + + + + @@ -943,6 +948,11 @@ + + + + + @@ -1203,6 +1213,11 @@ + + + + + @@ -1213,6 +1228,11 @@ + + + + + @@ -1623,6 +1643,11 @@ + + + + + @@ -1633,6 +1658,11 @@ + + + + + @@ -1648,6 +1678,11 @@ + + + + + @@ -1658,6 +1693,11 @@ + + + + + @@ -1668,6 +1708,11 @@ + + + + + @@ -1678,6 +1723,11 @@ + + + + + @@ -1688,6 +1738,11 @@ + + + + + @@ -1698,6 +1753,11 @@ + + + + + @@ -1708,6 +1768,11 @@ + + + + + @@ -1718,6 +1783,11 @@ + + + + + @@ -1728,6 +1798,11 @@ + + + + + @@ -1738,6 +1813,11 @@ + + + + + @@ -1748,6 +1828,11 @@ + + + + + @@ -1758,6 +1843,11 @@ + + + + + @@ -1768,6 +1858,11 @@ + + + + + @@ -1778,6 +1873,11 @@ + + + + + @@ -1788,6 +1888,11 @@ + + + + + @@ -1798,6 +1903,11 @@ + + + + + @@ -1808,6 +1918,11 @@ + + + + + @@ -1818,6 +1933,11 @@ + + + + + @@ -1828,6 +1948,11 @@ + + + + + @@ -1838,6 +1963,11 @@ + + + + + @@ -1853,6 +1983,11 @@ + + + + + @@ -1863,6 +1998,11 @@ + + + + + @@ -1873,6 +2013,11 @@ + + + + + @@ -1883,6 +2028,11 @@ + + + + + @@ -1893,6 +2043,11 @@ + + + + + @@ -1923,6 +2078,11 @@ + + + + + @@ -1933,6 +2093,11 @@ + + + + + @@ -1943,6 +2108,11 @@ + + + + + @@ -1953,6 +2123,11 @@ + + + + + @@ -1963,6 +2138,11 @@ + + + + + @@ -1978,6 +2158,11 @@ + + + + + @@ -1998,6 +2183,11 @@ + + + + + @@ -2008,6 +2198,11 @@ + + + + + @@ -2018,6 +2213,11 @@ + + + + + @@ -2028,6 +2228,11 @@ + + + + + @@ -2038,6 +2243,11 @@ + + + + + @@ -2048,6 +2258,11 @@ + + + + + @@ -2068,6 +2283,11 @@ + + + + + @@ -2078,6 +2298,11 @@ + + + + + @@ -2088,6 +2313,11 @@ + + + + + @@ -2098,6 +2328,11 @@ + + + + + @@ -2108,6 +2343,11 @@ + + + + + @@ -2118,6 +2358,11 @@ + + + + + @@ -2128,6 +2373,11 @@ + + + + + @@ -2138,6 +2388,11 @@ + + + + + @@ -2148,6 +2403,11 @@ + + + + + @@ -2158,6 +2418,11 @@ + + + + + @@ -2168,6 +2433,11 @@ + + + + + @@ -2178,6 +2448,11 @@ + + + + + @@ -2188,6 +2463,11 @@ + + + + + @@ -2198,6 +2478,11 @@ + + + + + @@ -2208,6 +2493,11 @@ + + + + + @@ -2218,6 +2508,11 @@ + + + + + @@ -2228,6 +2523,11 @@ + + + + + @@ -2238,6 +2538,11 @@ + + + + + @@ -2248,6 +2553,11 @@ + + + + + @@ -2628,6 +2938,11 @@ + + + + + @@ -2708,6 +3023,11 @@ + + + + + @@ -3068,6 +3388,11 @@ + + + + + @@ -3078,6 +3403,11 @@ + + + + + @@ -4158,6 +4488,11 @@ + + + + + @@ -4173,6 +4508,11 @@ + + + + + @@ -4193,6 +4533,11 @@ + + + + + @@ -5168,6 +5513,11 @@ + + + + + @@ -5178,6 +5528,11 @@ + + + + + @@ -5198,11 +5553,21 @@ + + + + + + + + + + @@ -5223,6 +5588,11 @@ + + + + + diff --git a/legacy/build.gradle b/legacy/build.gradle index 7d6d87c97..8dfb58968 100644 --- a/legacy/build.gradle +++ b/legacy/build.gradle @@ -62,6 +62,8 @@ android { } debug { getIsDefault().set(true) + minifyEnabled false + shrinkResources false manifestPlaceholders = [applicationLabel: APP_NAME_DEBUG] applicationIdSuffix ".debug" versionNameSuffix "-debug" From ce6f2380ccea701a5384bc7e45bd4dcdba1a63d3 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 21 Apr 2026 15:15:41 -0300 Subject: [PATCH 18/38] Redesign discover screen removes category groups and uses featured categories instead --- .../ui/screenshots/DiscoverScreenshotTest.kt | 7 +- .../org/fdroid/ui/categories/CategoryGroup.kt | 19 ---- .../org/fdroid/ui/categories/CategoryItem.kt | 98 +++++-------------- .../org/fdroid/ui/categories/CategoryList.kt | 87 +++++++--------- .../org/fdroid/ui/categories/CategoryRow.kt | 40 ++++++++ .../org/fdroid/ui/discover/AppCarousel.kt | 45 ++++++--- .../org/fdroid/ui/discover/DiscoverContent.kt | 8 +- .../org/fdroid/ui/discover/DiscoverModel.kt | 3 +- .../fdroid/ui/discover/DiscoverPresenter.kt | 2 +- .../fdroid/ui/discover/DiscoverViewModel.kt | 9 +- app/src/main/res/values-ar/strings.xml | 10 -- app/src/main/res/values-bg/strings.xml | 10 -- app/src/main/res/values-bn-rBD/strings.xml | 2 - app/src/main/res/values-ca/strings.xml | 10 -- app/src/main/res/values-cs/strings.xml | 10 -- app/src/main/res/values-da/strings.xml | 10 -- app/src/main/res/values-de/strings.xml | 10 -- app/src/main/res/values-el/strings.xml | 10 -- app/src/main/res/values-en-rGB/strings.xml | 10 -- app/src/main/res/values-eo/strings.xml | 10 -- app/src/main/res/values-es/strings.xml | 10 -- app/src/main/res/values-et/strings.xml | 10 -- app/src/main/res/values-eu/strings.xml | 10 -- app/src/main/res/values-fa/strings.xml | 10 -- app/src/main/res/values-fi/strings.xml | 9 -- app/src/main/res/values-fr/strings.xml | 10 -- app/src/main/res/values-ga/strings.xml | 10 -- app/src/main/res/values-gl/strings.xml | 3 - app/src/main/res/values-he/strings.xml | 10 -- app/src/main/res/values-hr/strings.xml | 10 -- app/src/main/res/values-hu/strings.xml | 10 -- app/src/main/res/values-id/strings.xml | 10 -- app/src/main/res/values-is/strings.xml | 10 -- app/src/main/res/values-it/strings.xml | 10 -- app/src/main/res/values-ja/strings.xml | 10 -- app/src/main/res/values-kab/strings.xml | 3 - app/src/main/res/values-ko/strings.xml | 10 -- app/src/main/res/values-lt/strings.xml | 10 -- app/src/main/res/values-lv/strings.xml | 10 -- app/src/main/res/values-mk/strings.xml | 10 -- app/src/main/res/values-nb/strings.xml | 10 -- app/src/main/res/values-nl/strings.xml | 10 -- app/src/main/res/values-pl/strings.xml | 10 -- app/src/main/res/values-pt-rBR/strings.xml | 10 -- app/src/main/res/values-pt-rPT/strings.xml | 10 -- app/src/main/res/values-pt/strings.xml | 10 -- app/src/main/res/values-ro/strings.xml | 10 -- app/src/main/res/values-ru/strings.xml | 10 -- app/src/main/res/values-sk/strings.xml | 10 -- app/src/main/res/values-sl/strings.xml | 10 -- app/src/main/res/values-sq/strings.xml | 10 -- app/src/main/res/values-sr/strings.xml | 10 -- app/src/main/res/values-sv/strings.xml | 10 -- app/src/main/res/values-sw/strings.xml | 10 -- app/src/main/res/values-tr/strings.xml | 10 -- app/src/main/res/values-uk/strings.xml | 10 -- app/src/main/res/values-uz/strings.xml | 1 - app/src/main/res/values-vi/strings.xml | 10 -- app/src/main/res/values-zh-rCN/strings.xml | 10 -- app/src/main/res/values-zh-rTW/strings.xml | 10 -- app/src/main/res/values/strings.xml | 13 +-- .../org/fdroid/ui/discover/DiscoverTest.kt | 16 ++- .../DiscoverTest_2ed8e27d_0.png | 4 +- 63 files changed, 162 insertions(+), 657 deletions(-) delete mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt create mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt index fd7e553ff..ee3670d6c 100644 --- a/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt +++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt @@ -1,5 +1,6 @@ package org.fdroid.ui.screenshots +import java.text.Collator import org.fdroid.ui.discover.Discover import org.fdroid.ui.discover.LoadedDiscoverModel import org.junit.Test @@ -16,12 +17,16 @@ class DiscoverScreenshotTest(localeName: String) : LocalizedScreenshotTest(local @Test fun appDetails() = screenshotTest("1_Discover") { localeList -> + val collator = Collator.getInstance(localeList.get(0)) val model = LoadedDiscoverModel( newApps = getNewApps(localeList), recentlyUpdatedApps = getRecentlyUpdatedApps(localeList), mostDownloadedApps = getMostDownloadedApps(localeList), - categories = getCategoryItems(localeList).groupBy { it.group }, + categories = + getCategoryItems(localeList).filter { it.featured }.sortedWith { c1, c2 -> + collator.compare(c1.name, c2.name) + }, hasRepoIssues = false, ) Discover(discoverModel = model, onListTap = {}, onAppTap = {}, onNav = {}) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt deleted file mode 100644 index fd22601d2..000000000 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.fdroid.ui.categories - -import androidx.annotation.StringRes -import org.fdroid.R - -data class CategoryGroup(val id: String, @get:StringRes val name: Int) - -object CategoryGroups { - val productivity = CategoryGroup("productivity", R.string.category_group_productivity) - val tools = CategoryGroup("tools", R.string.category_group_tools) - val wallets = CategoryGroup("wallets", R.string.category_group_wallets) - val media = CategoryGroup("media", R.string.category_group_media) - val communication = CategoryGroup("communication", R.string.category_group_communication) - val device = CategoryGroup("device", R.string.category_group_device) - val network = CategoryGroup("network", R.string.category_group_network) - val storage = CategoryGroup("storage", R.string.category_group_storage) - val interests = CategoryGroup("interests", R.string.category_group_interests) - val misc = CategoryGroup("misc", R.string.category_group_misc) -} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 34d9bae11..2d593ff45 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -153,81 +153,31 @@ data class CategoryItem(val id: String, val name: String) { else -> Icons.Default.Category } - val group: CategoryGroup + val featured: Boolean get() = when (id) { - "AI Chat" -> CategoryGroups.tools - "App Manager" -> CategoryGroups.device - "App Store & Updater" -> CategoryGroups.device - "Battery" -> CategoryGroups.device - "Bookmark" -> CategoryGroups.storage - "Browser" -> CategoryGroups.network - "Calculator" -> CategoryGroups.tools - "Calendar & Agenda" -> CategoryGroups.productivity - "Clock" -> CategoryGroups.productivity - "Cloud Storage & File Sync" -> CategoryGroups.storage - "Connectivity" -> CategoryGroups.network - "Contact" -> CategoryGroups.communication - "Development" -> CategoryGroups.interests - "DNS & Hosts" -> CategoryGroups.network - "Draw" -> CategoryGroups.interests - "Ebook Reader" -> CategoryGroups.media - "Email" -> CategoryGroups.communication - "File Encryption & Vault" -> CategoryGroups.storage - "File Transfer" -> CategoryGroups.storage - "Finance Manager" -> CategoryGroups.wallets - "Firewall" -> CategoryGroups.network - "Flashlight" -> CategoryGroups.tools - "Forum" -> CategoryGroups.communication - "Gallery" -> CategoryGroups.storage - "Games" -> CategoryGroups.media - "Graphics" -> CategoryGroups.interests - "Habit Tracker" -> CategoryGroups.productivity - "Icon Pack" -> CategoryGroups.device - "Internet" -> CategoryGroups.network - "Inventory" -> CategoryGroups.tools - "Keyboard & IME" -> CategoryGroups.device - "Launcher" -> CategoryGroups.device - "Local Media Player" -> CategoryGroups.media - "Location Tracker & Sharer" -> CategoryGroups.tools - "Messaging" -> CategoryGroups.communication - "Money" -> CategoryGroups.wallets - "Multimedia" -> CategoryGroups.media - "Music Practice Tool" -> CategoryGroups.interests - "Navigation" -> CategoryGroups.tools - "Network Analyzer" -> CategoryGroups.network - "News" -> CategoryGroups.interests - "Note" -> CategoryGroups.storage - "Online Media Player" -> CategoryGroups.media - "Pass Wallet" -> CategoryGroups.wallets - "Password & 2FA" -> CategoryGroups.device - "Phone & SMS" -> CategoryGroups.communication - "Podcast" -> CategoryGroups.media - "Public Transport" -> CategoryGroups.tools - "Radio" -> CategoryGroups.media - "Reading" -> CategoryGroups.media - "Recipe Manager" -> CategoryGroups.interests - "Religion" -> CategoryGroups.interests - "Remote Controller" -> CategoryGroups.tools - "Science & Education" -> CategoryGroups.interests - "Security" -> CategoryGroups.device - "Shopping List" -> CategoryGroups.tools - "Social Network" -> CategoryGroups.communication - "Sports & Health" -> CategoryGroups.interests - "System" -> CategoryGroups.device - "Task" -> CategoryGroups.productivity - "Text Editor" -> CategoryGroups.productivity - "Theming" -> CategoryGroups.device - "Time" -> CategoryGroups.productivity - "Translation & Dictionary" -> CategoryGroups.tools - "Voice & Video Chat" -> CategoryGroups.communication - "Unit Convertor" -> CategoryGroups.tools - "VPN & Proxy" -> CategoryGroups.network - "Wallet" -> CategoryGroups.wallets - "Wallpaper" -> CategoryGroups.device - "Weather" -> CategoryGroups.tools - "Workout" -> CategoryGroups.interests - "Writing" -> CategoryGroups.productivity - else -> CategoryGroups.misc + "Browser" -> true + "Calendar & Agenda" -> true + "Cloud Storage & File Sync" -> true + "Email" -> true + "File Encryption & Vault" -> true + "File Transfer" -> true + "Firewall" -> true + "Games" -> true + "Local Media Player" -> true + "Location Tracker & Sharer" -> true + "Messaging" -> true + "Navigation" -> true + "News" -> true + "Password & 2FA" -> true + "Podcast" -> true + "Public Transport" -> true + "Radio" -> true + "Social Network" -> true + "Translation & Dictionary" -> true + "Voice & Video Chat" -> true + "VPN & Proxy" -> true + "Weather" -> true + else -> false } } diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt index 4eb791d9e..8398d3b5c 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt @@ -1,86 +1,71 @@ package org.fdroid.ui.categories import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment.Companion.CenterVertically import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.navigation3.runtime.NavKey import org.fdroid.R import org.fdroid.ui.FDroidContent -import org.fdroid.ui.lists.AppListType -import org.fdroid.ui.navigation.NavigationKey @Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) fun CategoryList( - categoryMap: Map>?, + categoryList: List?, onNav: (NavKey) -> Unit, modifier: Modifier = Modifier, ) { - AnimatedVisibility(!categoryMap.isNullOrEmpty()) { - Column(modifier = modifier.padding(top = 20.dp)) { - Text( - text = stringResource(R.string.main_menu__categories), - style = MaterialTheme.typography.titleLarge, - modifier = Modifier.padding(bottom = 8.dp, start = 16.dp, end = 16.dp), - ) - // we'll sort the groups here, because before we didn't have the context to get names - val res = LocalResources.current - val sortedMap = - remember(categoryMap) { - val comparator = compareBy { res.getString(it.name) } - categoryMap?.toSortedMap(comparator) - } - sortedMap?.forEach { (group, categories) -> + val onAllCategories = { /* TODO */ } + AnimatedVisibility(!categoryList.isNullOrEmpty()) { + Column(modifier = modifier) { + Row( + verticalAlignment = CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = + Modifier.fillMaxWidth().clickable(onClick = onAllCategories).padding(horizontal = 16.dp), + ) { Text( - text = stringResource(group.name), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier.fillMaxWidth().padding(16.dp, 2.dp), + text = stringResource(R.string.categories_features), + style = MaterialTheme.typography.titleMediumEmphasized, + fontSize = 20.sp, ) - ChipFlowRow(modifier = Modifier.padding(start = 16.dp, bottom = 12.dp)) { - categories.forEach { category -> - CategoryChip( - categoryItem = category, - onClick = { - val type = AppListType.Category(category.name, category.id) - val navKey = NavigationKey.AppList(type) - onNav(navKey) - }, - ) - } - } + TextButton(onClick = onAllCategories) { Text(stringResource(R.string.see_all)) } + } + if (categoryList != null) { + // FIXME: A LazyColumn would be better, but we can't use this inside a scrollable column + Column { categoryList.forEach { categoryItem -> CategoryRow(categoryItem, onNav) } } } } } } -@Preview @Composable +@PreviewLightDark fun CategoryListPreview() { FDroidContent { val categories = - mapOf( - CategoryGroups.productivity to - listOf( - CategoryItem("App Store & Updater", "App Store & Updater"), - CategoryItem("Browser", "Browser"), - CategoryItem("Calendar & Agenda", "Calendar & Agenda"), - ), - CategoryGroups.media to - listOf( - CategoryItem("Cloud Storage & File Sync", "Cloud Storage & File Sync"), - CategoryItem("Connectivity", "Connectivity"), - CategoryItem("Development", "Development"), - CategoryItem("doesn't exist", "Foo bar"), - ), + listOf( + CategoryItem("App Store & Updater", "App Store & Updater"), + CategoryItem("Browser", "Browser"), + CategoryItem("Calendar & Agenda", "Calendar & Agenda"), + CategoryItem("Cloud Storage & File Sync", "Cloud Storage & File Sync"), + CategoryItem("Connectivity", "Connectivity"), + CategoryItem("Development", "Development"), + CategoryItem("doesn't exist", "Foo bar"), ) CategoryList(categories, {}) } diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt new file mode 100644 index 000000000..f9d0dbe8d --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt @@ -0,0 +1,40 @@ +package org.fdroid.ui.categories + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.NavKey +import org.fdroid.ui.lists.AppListType +import org.fdroid.ui.navigation.NavigationKey + +@Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun CategoryRow(categoryItem: CategoryItem, onNav: (NavKey) -> Unit) { + ListItem( + leadingContent = { + Icon( + imageVector = categoryItem.imageVector, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.semantics { hideFromAccessibility() }, + ) + }, + onClick = { + val type = AppListType.Category(categoryItem.name, categoryItem.id) + val navKey = NavigationKey.AppList(type) + onNav(navKey) + }, + modifier = Modifier.padding(horizontal = 16.dp), + ) { + Text(text = categoryItem.name, maxLines = 2, overflow = TextOverflow.Ellipsis) + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt index 89cf54f67..b8f2d13b0 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/AppCarousel.kt @@ -1,6 +1,7 @@ package org.fdroid.ui.discover import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -11,11 +12,14 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -27,6 +31,7 @@ import androidx.compose.ui.semantics.hideFromAccessibility import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import org.fdroid.ui.FDroidContent import org.fdroid.ui.utils.AsyncShimmerImage import org.fdroid.ui.utils.InstalledBadge @@ -41,20 +46,28 @@ fun AppCarousel( onTitleTap: () -> Unit, onAppTap: (AppDiscoverItem) -> Unit, ) { - Column(verticalArrangement = spacedBy(8.dp), modifier = modifier) { + Column(verticalArrangement = spacedBy(10.dp), modifier = modifier) { Row( verticalAlignment = CenterVertically, - modifier = - Modifier.fillMaxWidth() - .clickable(onClick = onTitleTap) - .padding(horizontal = 16.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth().clickable(onClick = onTitleTap).padding(horizontal = 16.dp), ) { - Text(text = title, style = MaterialTheme.typography.headlineSmall) - Icon( - imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, - modifier = Modifier.semantics { hideFromAccessibility() }, - contentDescription = null, - ) + Text(text = title, style = MaterialTheme.typography.titleMediumEmphasized, fontSize = 20.sp) + IconButton( + onClick = onTitleTap, + shape = CircleShape, + colors = + IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.surfaceContainer + ), + modifier = Modifier.size(48.dp).padding(6.dp), + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + modifier = Modifier.semantics { hideFromAccessibility() }, + contentDescription = null, + ) + } } LazyRow( contentPadding = PaddingValues(horizontal = 16.dp), @@ -82,7 +95,13 @@ fun AppBox(app: AppDiscoverItem, onAppTap: (AppDiscoverItem) -> Unit) { }, ) } - Text(text = app.name, style = MaterialTheme.typography.bodySmall, minLines = 2, maxLines = 2) + Text( + text = app.name, + style = MaterialTheme.typography.bodySmall, + minLines = 2, + maxLines = 2, + lineHeight = 14.sp, + ) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt index 70f491a94..5e6a3b462 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt @@ -1,19 +1,21 @@ package org.fdroid.ui.discover import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavKey import org.fdroid.R import org.fdroid.ui.categories.CategoryList import org.fdroid.ui.lists.AppListType -@OptIn(ExperimentalMaterial3Api::class) @Composable +@OptIn(ExperimentalMaterial3Api::class) fun DiscoverContent( discoverModel: LoadedDiscoverModel, onListTap: (AppListType) -> Unit, @@ -21,7 +23,7 @@ fun DiscoverContent( onNav: (NavKey) -> Unit, modifier: Modifier = Modifier, ) { - Column(modifier = modifier) { + Column(verticalArrangement = spacedBy(18.dp), modifier = modifier) { AnimatedVisibility(discoverModel.newApps.isNotEmpty()) { val listNew = AppListType.New(stringResource(R.string.app_list_new)) AppCarousel( @@ -53,7 +55,7 @@ fun DiscoverContent( ) } CategoryList( - categoryMap = discoverModel.categories, + categoryList = discoverModel.categories, onNav = onNav, modifier = Modifier.fillMaxWidth(), ) diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt index 141ef8b15..9af011d42 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt @@ -2,7 +2,6 @@ package org.fdroid.ui.discover import org.fdroid.download.NetworkState import org.fdroid.repo.RepoUpdateState -import org.fdroid.ui.categories.CategoryGroup import org.fdroid.ui.categories.CategoryItem sealed class DiscoverModel @@ -20,6 +19,6 @@ data class LoadedDiscoverModel( val newApps: List, val recentlyUpdatedApps: List, val mostDownloadedApps: List?, - val categories: Map>?, + val categories: List?, val hasRepoIssues: Boolean, ) : DiscoverModel() diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt index 68a1c0f63..21bdbb840 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt @@ -88,7 +88,7 @@ fun DiscoverPresenter( newApps = newApps ?: emptyList(), recentlyUpdatedApps = recentlyUpdatedApps ?: emptyList(), mostDownloadedApps = mostDownloadedApps, - categories = categories?.groupBy { it.group }, + categories = categories, hasRepoIssues = hasRepoIssuesFlow.collectAsState(false).value, ) } else { diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt index bb66e6baf..75609d1aa 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt @@ -71,8 +71,13 @@ constructor( private val categories = db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> categories - .map { category -> - CategoryItem(id = category.id, name = category.getName(localeList) ?: "Unknown Category") + .mapNotNull { category -> + val item = + CategoryItem( + id = category.id, + name = category.getName(localeList) ?: "Unknown Category", + ) + if (item.featured) item else null } .sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) } } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index e3b6c2e80..24a6bcf90 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -686,16 +686,6 @@ تطبيقات بواسطة %s ابحث… لم يُعثر على أي تطبيقات\n\nحاول استخدام كلمات بحث أقل أو أضف المزيد من المستودعات - إنتاجية - أدوات - المالية والمحافظ - الترفيه والإعلام - اتصال - الجهاز - الشبكة - الملفات والتخزين - اهتمامات - متنوّع بواسطة %1$s آخر تحديث: %1$s آخر تحديث: %1$s (%2$s) diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 8fb0852fc..8dca95640 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -628,16 +628,6 @@ Приложения от %s Търсене… Не са намерени приложения\n\nСъкратете търсенето или добавете повече хранилища - Продуктивност - Инструменти - Финанси и портфейли - Забавление и медия - Разговори - Устройство - Мрежа - Файлове и хранилища - Интереси - Разни От %1$s Последно обновяване: %1$s Последно обновяване: %1$s (%2$s) diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index 48d62c959..a500e2ecf 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -510,8 +510,6 @@ সমস্ত অ্যাপস সর্বাধিক ডাউনলোডকৃত অনুসন্ধান করুন - সরঞ্জামসমূহ - পছন্দসমূহ F-Droid সাধারণ F-Nightly F-Nightly Basic diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index 2b51c25ff..c865c65b4 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -639,16 +639,6 @@ Aplicacions per %s Cerca… No s\'han trobat aplicacions\n\nProva d\'emprar menys termes de cerca o d\'afegir més repositoris - Productivitat - Eines - Finances i carteres - Entreteniment i mitjans - Comunicació - Dispositiu - Xarxa - Arxius i emmagatzematge - Interessos - Miscel·lània Per %1$s Últimes actualitzacions: %1$s Últimes actualitzacions: %1$s (%2$s) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 9195620ec..d3f866a41 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -642,16 +642,6 @@ Aplikace od vývojáře %s Hledat… Nenalezeny žádné aplikace\n\nZkuste použít méně slov nebo přidat další repozitáře - Produktivita - Nástroje - Finance a peněženky - Zábava a média - Komunikace - Zařízení - Síť - Soubory a úložiště - Zájmy - Různé Od %1$s Naposledy aktualizováno: %1$s Naposledy aktualizováno: %1$s (%2$s) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 48025cf24..274c75124 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -622,33 +622,23 @@ Alle apps Nye apps Eksportér appliste - Værktøjer Af %1$s - Kommunikation - Netværk Donér Installeret version: Teknisk information - Enhed Denne app har anti-funktioner Forbereder installation… Pakkenavn: - Produktivitet Sidst opdateret: %1$s - Underholdning & Medier Nyligt opdateret Mest hentede - Interesser Sidst opdateret: %1$s (%2$s) Nyheder - Filer & Lagring - Diverse F-Droid er brudt sammen F-Droid er truet. Google ændrer hvordan du kan installere apps på din enhed. Vi har brug for din hjælp. https://keepandroidopen.org Hent apps ved brug af IPFS-webendpoints Ingen apps fundet\n\nPrøv færre søgeord eller tilføj flere repositories - Økonomi og tegnebøger Udviklerkontakt Henter app-repositories… diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 38427ffee..e8b147775 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -638,16 +638,6 @@ Apps von %s Suchen … Keine Apps gefunden\n\nVersuche es mit weniger Suchbegriffen oder füge weitere Repositorys hinzu - Produktivität - Werkzeuge - Finanzen und Wallets - Unterhaltung und Medien - Kommunikation - Gerät - Netzwerk - Dateien und Speicher - Interessen - Verschiedenes Von %1$s Zuletzt aktualisiert: %1$s Zuletzt aktualisiert: %1$s (%2$s) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index 612673fc7..ca841166a 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -627,16 +627,6 @@ Εφαρμογές από %s Αναζήτησε… Δεν βρέθηκαν εφαρμογές\n\nΔοκιμάστε να χρησιμοποιήσετε λιγότερους όρους αναζήτησης ή προσθέστε περισσότερα αποθετήρια - Παραγωγικότητα - Εργαλεία - Οικονομικά & Πορτοφόλια - Ψυχαγωγία & Μέσα - Επικοινωνία - Συσκευή - Δίκτυο - Αρχεία & Αποθηκευτικός Χώρος - Ενδιαφέροντα - Διάφορα Από %1$s Τελευταία ενημέρωση: %1$s Τελευταία ενημέρωση: %1$s (%2$s) diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index d0c622c40..8096b928d 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -647,16 +647,6 @@ Apps by %s Search… No apps found\n\nTry to use less search terms or add more repositories - Productivity - Tools - Finances & Wallets - Entertainment & Media - Communication - Device - Network - Files & Storage - Interests - Miscellaneous By %1$s Last updated: %1$s Last updated: %1$s (%2$s) diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index a633fe869..f0c33e10e 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -628,16 +628,6 @@ Aplikaĵoj fare de %s Serĉi… Trovis neniun aplikaĵon\n\nProvu uzi malpli da serĉvortoj aŭ aldoni pliajn deponejojn - Oficejo - Utilaĵoj - Financoj - Distro kaj aŭdvidaĵoj - Komunikado - Aparato - Reto - Dosieroj kaj konservado - Interesiĝoj - Diversaĵoj Fare de %1$s Lasta ĝisdatigo: %1$s Lasta ĝisdatigo: %1$s (%2$s) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 04144e5ed..9cf153289 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -665,16 +665,6 @@ Aplicaciones por %s Buscar… No se encontraron apps\n\nIntenta usar menos términos de búsqueda o agrega más repositorios - Productividad - Herramientas - Financias y Carteras - Entretenimiento y Medios - Comunicación - Dispositivo - Red - Archivos y Almacenamiento - Intereses - Misceláneo Por %1$s Últimamente actualizado: %1$s Últimamente actualizado: %1$s (%2$s) diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 6ddce00b9..79bdb80d0 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -644,16 +644,6 @@ Rakendused arendajalt %s Otsi… Rakendusi ei leidu\n\nProovi kasutada laiemaid otsingutingimusi või lisa veel rakenduste hoidlaid - Tarbetarkvara - Tarvikud - Rahandus ja rahataskud - Meelelahutus ja meedia - Suhtlus - Nutiseade ja süsteem - Võrgundus - Failid ja andmeruum - Huvid ja hobid - Varia Arendajalt %1$s Viimati uuendatud: %1$s Viimati uuendatud: %1$s (%2$s) diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 630addd63..ba49a9e1c 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -653,16 +653,6 @@ %s araberako aplikazioak Bilatu… Ez da aplikaziorik aurkitu\n\nSaiatu bilaketa termino gutxiagorekin edo biltegi gehiagorekin - Produktibitatea - Tresnak - Finantza eta diru-zorroak - Entretenimendu eta multimedia - Komunikazioa - Gailua - Sarea - Fitxategiak eta biltegiratzea - Interesak - Denetarik %1$s-ek egina Azken eguneraketa: %1$s Azken eguneraketa: %1$s (%2$s) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index f4aa4cbd5..292fd2d8a 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -630,15 +630,6 @@ کاره‌های %s جست‌وجو… هیچ کارهٔ مطابقی وجود ندارد\n\nتلاش برای استفاده از عبارت‌های جست‌وجوی کم‌تر یا افزودن مخزن‌های بیش‌تر - بهره‌وری - ابزارها - مالی و کیف‌پول - سرگرمی و رسانه - ارتباطات - افزاره - شبکه - پرونده‌ها و ذخیره‌سازی - متفرّقه به دست %1$s آخرین به‌روز رسانی: %1$s آخرین به‌روز رسانی: %1$s‏ (%2$s) @@ -650,7 +641,6 @@ اطّلاعات فنی نام بسته: نگارش نصب شده: - سهام پالایش گزیده دسته diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 3234f25bb..91bc02bd5 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -617,13 +617,6 @@ Kaikki sovellukset Tekjiän %s sovellukset Etsi… - Raha ja lompakot - Viihde ja media - Viestintä - Laite - Verkko - Tiedostot ja tallennus - Sekalaiset Tekijältä %1$s Viimeksi päivitetty: %1$s Viimeksi päivitetty: %1$s (%2$s) @@ -643,8 +636,6 @@ Sovellukset, joissa on ongelmia Piilota tapaus Ongelma sovelluksen \"%1$s\" versiossa ohitetaan - Tuottavuus - Työkalut F-Öinen F-Öinen Perus Löydä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c935b9966..ecb8649a2 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -655,16 +655,6 @@ Applications de %s Rechercher… Aucune application trouvée\n\nEssayez d\'utiliser moins de termes de recherche ou d\'ajouter d\'autres dépôts - Productivité - Outils - Finances & Portefeuilles - Divertissement & Médias - Communication - Appareil - Réseau - Fichiers & Stockage - Intérêts - Divers De %1$s Dernière mise à jour : %1$s Dernière mise à jour : %1$s (%2$s) diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index 2922ad326..a2c8671e1 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -685,16 +685,6 @@ Aipeanna ó %s Cuardaigh… Níor aimsíodh aon aipeanna\n\nBain triail as níos lú téarmaí cuardaigh nó cuir níos mó stórtha leis - Táirgiúlacht - Uirlisí - Airgeadas & Sparán - Siamsaíocht & Meáin - Cumarsáid - Gléas - Líonra - Comhaid & Stóráil - Leasanna - Ilghnéitheach Faoi %1$s Nuashonraithe go deireanach: %1$s Nuashonraithe go deireanach: %1$s (%2$s) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 07f7f59ff..8db61c66c 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -591,7 +591,4 @@ Aplicacións por %s Buscar… Non se atoparon aplicacións\n\nIntente usar menos termos de busca ou engadir máis repositorios - Produtividade - Ferramentas - Finanzas e carteiras diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index 35816a64c..f002672e0 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -673,16 +673,6 @@ יישמונים על ידי %s חפש… לא נמצאו יישומונים\n\nנסו להשתמש בפחות מילות חיפוש או להוסיף עוד מאגרים - הפקה - כלים - כלכלה וארנקים - בידור ומדיה - תקשורת - מכשיר - רשת - קבצים ואחסון - תחומי עניין - שונות על ידי %1$s עדכון אחרון: %1$s עדכון אחרון: %1$s (%2$s) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 907c8d2d5..2b9e7e0c6 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -647,16 +647,6 @@ Aplikacije od %s Pretraga … Nije pronađena nijedna aplikacija\n\nPokušaj koristiti manje pojmova za pretraživanje ili dodaj više repozitorija - Produktivnost - Alati - Financije i novčanici - Zabava i mediji - Komunikacija - Uređaj - Mreža - Datoteke i spremišta - Interesi - Razno Autor: %1$s Zadnje aktualiziranje: %1$s Zadnje aktualiziranje: %1$s (%2$s) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 7ab8d8498..8e7fd42e7 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -630,16 +630,6 @@ Alkalmazások %s szerint Keres… Nincs találat\n\nPróbálj meg kevesebb keresési kifejezést használni, vagy adjon hozzá több alkalmazás-tárolót - Termelékenység - Eszközök - Pénzügyek és pénztárcák - Szórakozás és média - Kommunikáció - Eszköz - Hálózat - Fájlok és tárolók - Érdeklődési kör - Egyéb %1$s -től Utolsó frissítés: %1$s Utolsó frissítés: %1$s (%2$s) diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 45b35d7ee..017a25d79 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -633,16 +633,6 @@ Aplikasi oleh %s Cari… Tidak ada aplikasi ditemukan\n\nCoba gunakan lebih sedikit kata kunci atau tambahkan lebih banyak repositori - Produktifitas - Alat - Keuangan & Dompet - Hiburan & Media - Komunikasi - Perangkat - Jaringan - Fail & Penyimpanan - Minat - Lain-Lain Oleh %1$s Terakhir diperbarui: %1$s Terakhir diperbarui: %1$s (%2$s) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 37eaab009..3561af7a5 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -629,16 +629,6 @@ Forrit frá %s Leita… Engin forrit fundust\n\nPrófaðu með færri leitarorð eða bættu við gagnasöfnum - Framleiðni - Verkfæri - Fjármál og veski - Afþreying og miðlun - Samskipti - Tæki - Netkerfi - Skrár og geymslurými - Áhugamál - Ýmislegt Frá %1$s Síðast uppfært: %1$s Síðast uppfært: %1$s (%2$s) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 97c1f51ec..66f915dff 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -663,16 +663,6 @@ App per %s Cerca… Nessuna app trovata\n\nProva a usare meno termini di ricerca o aggiungere più repository - Produttività - Strumenti - Finanze e Portafogli - Intrattenimento & Media - Comunicazione - Dispositivo - Rete - Archiviazione - Interessi - Miscellanea Da %1$s Ultimo aggiornamento: %1$s Ultimo aggiornamento: %1$s (%2$s) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 3966fd893..00b767bdf 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -634,16 +634,6 @@ %s によるアプリ 検索… アプリが見つかりませんでした\n\nリポジトリを追加するか検索語を減らしてみて下さい - 生産性 - ツール - 財布&財務 - エンタメ&メディア - 交流 - デバイス - 通信 - ファイル&ストレージ - 趣味 - その他 %1$s による 最終更新: %1$s 最終更新: %1$s (%2$s) diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index 2123d1285..d04c43872 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -395,13 +395,10 @@ Seggelmes Nadi… https://keepandroidopen.org - Aẓeṭṭa %d n wesnas %d n yesnasen - Ifecka - Ibenk Mudd tawsa Taggayt Aẓeṭṭa diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index ad887e082..8550dbd92 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -625,16 +625,6 @@ %s의 앱 검색… 앱을 찾을 수 없음\n\n검색어를 줄이거나 다른 저장소를 추가하세요 - 생산성 - 도구 - 금융 & 지갑 - 엔터테인먼트 & 미디어 - 통신 - 기기 - 네트워크 - 파일 & 저장소 - 관심분야 - 기타 개발자: %1$s 마지막 업데이트: %1$s 마지막 업데이트: %1$s (%2$s) diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index cae87bfcb..588978db1 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -601,16 +601,6 @@ %s sukurtos programėlės Ieškoti… Nerastos programėlės\n\nPamėginkite naudoti mažiau paieškos žodžių arba įtraukite daugiau saugyklų - Produktyvumas - Įrankiai - Finansai ir piniginės - Pramogos ir multimedija - Komunikacija - Įrenginys - Tinklas - Failai ir saugykla - Pomėgiai - Kiti Sukurta %1$s Paskutinį kartą atnaujinta: %1$s Paskutinį kartą atnaujinta: %1$s (%2$s) diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 48f96dbf6..9eabffbeb 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -639,15 +639,6 @@ Lietotnes no %s Meklēt… Neviena lietotne netika atrasta\n\nJāmēģina izmantot vienkāršāks meklēšanas vaicājums vai jāpievieno vairāk glabātavu - Ražīgums - Rīki - Nauda un maki - Izklaide un mediji - Saziņa - Ierīce - Tīkls - Datnes un krātuve - Dažādi No %1$s Pēdējoreiz atjaunināta: %1$s Pēdējoreiz atjaunināta: %1$s (%2$s) @@ -662,7 +653,6 @@ Lietotnes ar nepilnībām Paslēpt nepilnību Nepilnība šajā “%1$s” versijā netiks ņemta vērā - Intereses Lejupielādēt lietotnes no IPFS tīmekļa galamērķiem Lai uzsāktu, jāvienādo lietotņu glabātavas diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index e03e44546..8963f18f8 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -296,16 +296,6 @@ Апликации од%s Пребарувај… Не се пронајдени апликации\n\nПробај да употребиш помали термини или додај нови репо - Продуктивност - Алатки - Финансии и новчаник - Забава и медија - Комуникација - Уред - Мрежа - Датотеки и меморија - Интереси - Останато Направено од %1$s Последно надоградено: %1$s Последно надоградено: %1$s­%2$s diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 5b46c02da..0f9e78794 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -644,16 +644,6 @@ Apper fra %s Søk… Ingen apper funnet\n\nPrøv å bruke færre søkeord eller legg til flere pakkebrønner - Produktivitet - Verktøy - Finans & Lommebøker - Underholdning & Medier - Kommunikasjon - Enhet - Nettverk - Filer & Lagring - Interesser - Diverse Fra %1$s Sist oppdatert: %1$s Sist oppdatert: %1$s (%2$s) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index c5d22d18d..0992923ec 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -636,16 +636,6 @@ Zoeken… App gegevensbanken binnenhalen… Geen apps gevonden\n\nProbeer minder zoektermen of voeg meer gegevensbanken toe - Productiviteit - Hulpmiddelen - Financiën & Wallets - Entertainment & Media - Communicatie - Apparaat - Netwerk - Bestanden & Opslag - Interesses - Diversen Door %1$s Laatst geüpdatet: %1$s Laatst geüpdatet: %1$s (%2$s) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 22d5134d1..96a0156c4 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -673,16 +673,6 @@ Aplikacje od %s Szukaj… Nie znaleziono aplikacji\n\nSpróbuj użyć mniejszej liczby terminów wyszukiwania lub dodaj więcej repozytoriów - Produktywność - Narzędzia - Finanse i portfele - Rozrywka i multimedia - Komunikacja - Urządzenie - Sieć - Pliki i pamięć - Zainteresowania - Różne Przez %1$s Ostatnia aktualizacja: %1$s Ostatnia aktualizacja: %1$s (%2$s) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index e5211b485..120edb959 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -645,14 +645,6 @@ Apps por %s Buscar… Nenhum app encontrado\n\nTente usar menos termos na busca ou adicione mais repositórios - Produtividade - Ferramentas - Finanças & Carteiras - Entretenimento & Mídia - Comunicações - Dispositivo - Arquivos & Armazenamento - Interesses Por %1$s Última atualização: %1$s (%2$s) Preparando instalação… @@ -663,8 +655,6 @@ Informações técnicas Nome do pacote: Versão instalada: - Rede - Diversos Última atualização: %1$s Baixe aplicativos usando endpoints web do IPFS Para começar, sincronize os repositórios de apps diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index ccf14c408..44147e720 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -640,16 +640,6 @@ Apps por %s Procurar… Nenhuma aplicação encontrada\n\nTente utilizar menos termos de pesquisa ou adicione mais repositórios - Produtividade - Ferramentas - Finanças & carteiras - Entretenimento & média - Comunicação - Dispositivo - Rede - Ficheiros & armazenamento - Interesses - Diversos Por %1$s Última atualização: %1$s Última atualização: %1$s (%2$s) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 32ba51497..c1929c710 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -654,16 +654,6 @@ escolhido Categoria Limpar - Produtividade - Ferramentas - Finanças & carteiras - Entretenimento & média - Comunicação - Dispositivo - Rede - Ficheiros & armazenamento - Interesses - Diversos Por %1$s Última atualização: %1$s Última atualização: %1$s (%2$s) diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 91a759cfc..34c8ee6d6 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -650,16 +650,6 @@ Aplicații de la %s Căutare… Nu s-au găsit aplicații\n\nÎncercați să utilizați mai puțini termeni de căutare sau adăugați mai multe depozite - Productivitate - Unelte - Finanțe și Portofele - Divertisment și media - Comunicare - Dispozitiv - Rețea - Fișiere și stocare - Pasiuni - Diverse De către %1$s Ultima actualizare: %1$s Ultima actualizare: %1$s (%2$s) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 42a2b193d..69e7cecb3 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -654,16 +654,6 @@ Приложения от %s Поиск… Приложений не найдено\n\nПопробуйте использовать меньше поисковых запросов или добавить больше репозиториев - Производительность - Инструменты - Финансы и кошельки - Развлечения и СМИ - Коммуникация - Устройство - Сеть - Файлы и хранилище - Интересы - Разное От %1$s Последнее обновление: %1$s Последнее обновление: %1$s (%2$s) diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index f63ae2b2d..ddd9f22e8 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -578,16 +578,6 @@ Aplikácie od vyvojára %s Vyhľadať… Nenašli sa žiadne aplikácie\n\nSkúste použiť menej slov alebo pridať ďalšie repozitáre - Produktivita - Nástroje - Financie a peňaženky - Zábava a média - Komunikácia - Zariadenie - Sieť - Súbory a uložisko - Záujmy - Rôzne Od %1$s Naposledy aktualizované: %1$s Naposledy aktualizované: %1$s (%2$s) diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 160e6c1eb..2e10477cd 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -552,16 +552,6 @@ Aplikacije avtorja %s Iskanje … Ni najdenih aplikacij\n\nPoskusite uporabiti manj iskalnih izrazov ali dodajte več repozitorijev - Produktivnost - Orodja - Finance in denarnice - Zabava in mediji - Komunikacija - Naprava - Omrežje - Datoteke in shranjevanje - Interesi - Razno Od %1$s Zadnja posodobitev: %1$s Zadnja posodobitev: %1$s (%2$s) diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index e5f40f5e3..294f41371 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -627,15 +627,6 @@ Aplikacione nga %s Kërkoni… S’u gjetën aplikacione\n\nProvoni të përdorni më pak terma kërkimi, ose të shtoni më tepër depo - Mjete - Financa & Kuleta - Spektakël & Media - Komunikim - Pajisje - Rrjet - Kartela & Depozitim - Interesa - Të ndryshme Nga %1$s Përditësuar së fundi më: %1$s Ky aplikacion përmban anti-veçori @@ -648,7 +639,6 @@ Problemi me këtë version të “%1$s” do të shpërfillet Fshihe Eksportoni listë aplikacionesh - Prodhimtari Shkarkoni aplikacione duke përdorur pikëmbarime web IPFS Që t’ia fillohet, njëkohësoni depot e aplikacioneve Jeni jashtë linje. Që t’ia fillohet, lidhuni në Internet. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 1efb8dc81..9ec33c0be 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -626,16 +626,6 @@ Апликације од %s Претражи… Нема пронађених апликација.\n\nПокушајте да користите мање појмова за претрагу или додајте још складишта - Продуктивност - Алати - Финансије и новчаници - Забава и садржај - Комуникација - Уређај - Мрежа - Датотеке и складиште - Интересовања - Разно Од %1$s Последње ажурирање: %1$s Последње ажурирано: %1$s (%2$s) diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index f6aa08b7a..4527b52c4 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -644,16 +644,6 @@ Appar från %s Sök… Inga appar hittades\n\nFörsök att använda färre söktermer eller lägga till fler förråd - Produktivitet - Verktyg - Ekonomi & plånböcker - Underhållning & media - Kommunikation - Enhet - Nätverk - Filer & lagring - Intressen - Diverse Av %1$s Senast uppdaterad: %1$s Senast uppdaterad: %1$s (%2$s) diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 4429586d2..1b97f2a92 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -600,16 +600,6 @@ Zilizosasishwa hivi karibuni Programu zote Tafuta - Uzalishaji - Zana - Fedha na Pochi - Burudani na Vyombo vya Habari - Mawasiliano - Kifaa - Mtandao - Faili na Hifadhi - Maslahi - Mengineyo Ilisasishwa mwisho: %1$s Nini kipya Changia diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 159aca14e..3a3a56990 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -630,11 +630,6 @@ Keşfet Uygulamalarım Gizle - Üretkenlik - Araçlar - Eğlence & Medya - İletişim - İlgi Alanları %1$s tarafından F-Nightly F-Nightly Temel @@ -651,11 +646,6 @@ %s tarafından uygulamalar Ara… Herhangi bir uygulama bulunamadı \n\nDaha az arama terimi kullanmayı ya da daha fazla depo eklemeyi deneyin - Finans ve Cüzdanlar - Cihaz - - Dosyalar ve Depolama - Çeşitli En son güncelleme: %1$s En son güncelleme: %1$s (%2$s) Yüklemeye hazırlanılıyor… diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 0365920b9..2d33f886c 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -655,16 +655,6 @@ Додатки від %s Пошук… Не знайдено жодних програм\n\nСпробуйте використовувати менше пошукових термінів або додати більше репозиторіїв - Продуктивність - Інструменти - Фінанси та гаманці - Розваги та медіа - Комунікація - Пристрій - Мережа - Файли та зберігання - Інтереси - Різне Від %1$s Останні оновлено: %1$s Останні оновлено: %1$s (%2$s) diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index ffc3639ee..82ab286da 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -79,7 +79,6 @@ Versiyasi va UUID\'larni serverlarga jo\'natish O\'rnatish tarixini jo\'natish O\'rnatilgan versiya: - Qurulma Yaxshirish Haqida Mening ilovalarim diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index bda4dcd6c..fc70f2245 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -602,16 +602,6 @@ Thu gọn F-Droid đã bị treo Thông tin kỹ thuật - Năng suất - Công cụ - Tài chính & Ví - Giải trí & Media - Liên lạc - Thiết bị - Kết nối mạng - Tệp & Lưu trữ - Sở thích - Linh tinh Bởi %1$s Lần cuối cập nhật: %1$s Lần cuối cập nhật: %1$s (%2$s) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 45cbf0726..0ddbfe6c6 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -627,16 +627,6 @@ %s 的应用 搜索… 没找到应用\n\n试试减少搜索词或添加更多存储库 - 生产力 - 工具 - 财务和钱包 - 娱乐和媒体 - 通信 - 设备 - 网络 - 文件和存储 - 兴趣 - 杂项 由 %1$s 开发 上次更新:%1$s 上次更新: %1$s (%2$s) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 26b33b752..d505d18c2 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -621,16 +621,6 @@ %s 的應用程式 搜尋… 找不到應用程式\n\n請嘗試減少搜尋關鍵字或新增軟體庫或 - 生產性 - 工具 - 財經與錢包 - 娛樂與媒體 - 通訊 - 裝置 - 網路 - 檔案與儲存 - 興趣 - 其他 最後更新: %1$s 最後更新: %1$s (%2$s) 正在準備安裝… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 54c3a5624..4cf230cca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,7 +50,9 @@ Filter selected + Featured Categories Category + See all Hide anti-features Filter Use filters to only show apps from specific categories or repositories. You can also change the sort order. @@ -58,17 +60,6 @@ Clear Hide - Productivity - Tools - Finances & Wallets - Entertainment & Media - Communication - Device - Network - Files & Storage - Interests - Miscellaneous - By %1$s Last updated: %1$s diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt index b5926d394..a5b1b93ec 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt @@ -6,7 +6,6 @@ import com.android.tools.screenshot.PreviewTest import org.fdroid.download.NetworkState import org.fdroid.repo.RepoUpdateProgress import org.fdroid.ui.ScreenshotTest -import org.fdroid.ui.categories.CategoryGroups import org.fdroid.ui.categories.CategoryItem @Composable @@ -97,15 +96,12 @@ private fun getLoadedModel(): LoadedDiscoverModel { AppDiscoverItem(packageName = "at.bitfire.davdroid", name = "DAVx⁵", isInstalled = false), ) val categories = - mapOf( - CategoryGroups.communication to - listOf( - CategoryItem(id = "Contact", name = "Contact"), - CategoryItem(id = "Email", name = "Email"), - CategoryItem(id = "Forum", name = "Forum"), - CategoryItem(id = "Messaging", name = "Messaging"), - CategoryItem(id = "Phone & SMS", name = "Phone & SMS"), - ) + listOf( + CategoryItem(id = "Contact", name = "Contact"), + CategoryItem(id = "Email", name = "Email"), + CategoryItem(id = "Forum", name = "Forum"), + CategoryItem(id = "Messaging", name = "Messaging"), + CategoryItem(id = "Phone & SMS", name = "Phone & SMS"), ) return LoadedDiscoverModel( newApps = newApps, diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png index 2a344adb7..b26af6832 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3bffaab8294637b504398af638e91187ff23640466f9775668ec00490436effe -size 151906 +oid sha256:ec431c87484108cf9f5680ba6fd5877dc7b8cf9d53aecd8016b89db2ca2582e3 +size 142401 From ad4da58d60fbc46ceba8c78100945ea14d1502b5 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 21 Apr 2026 16:11:27 -0300 Subject: [PATCH 19/38] Add a dedicated screen showing all categories --- app/src/main/kotlin/org/fdroid/ui/Main.kt | 10 ++ .../org/fdroid/ui/categories/Categories.kt | 94 +++++++++++++++++++ .../ui/categories/CategoriesViewModel.kt | 28 ++++++ .../org/fdroid/ui/categories/CategoryList.kt | 3 +- .../org/fdroid/ui/navigation/NavigationKey.kt | 2 + 5 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt create mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt diff --git a/app/src/main/kotlin/org/fdroid/ui/Main.kt b/app/src/main/kotlin/org/fdroid/ui/Main.kt index 59f4e0c30..76bab88fa 100644 --- a/app/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/app/src/main/kotlin/org/fdroid/ui/Main.kt @@ -16,6 +16,8 @@ import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import org.fdroid.ui.apps.myAppsEntry +import org.fdroid.ui.categories.Categories +import org.fdroid.ui.categories.CategoriesViewModel import org.fdroid.ui.details.NoAppSelected import org.fdroid.ui.details.appDetailsEntry import org.fdroid.ui.discover.discoverEntry @@ -78,6 +80,14 @@ fun Main(onListeningForIntent: () -> Unit = {}) { onSearchCleared = viewModel::onSearchCleared, ) } + entry(NavigationKey.Categories) { + val viewModel = hiltViewModel() + Categories( + categories = viewModel.categories.collectAsStateWithLifecycle(null).value, + onNav = { navKey -> navigator.navigate(navKey) }, + onBackClicked = { navigator.goBack() }, + ) + } entry(NavigationKey.Settings) { val viewModel = hiltViewModel() Settings( diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt b/app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt new file mode 100644 index 000000000..00910b209 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt @@ -0,0 +1,94 @@ +package org.fdroid.ui.categories + +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.NavKey +import org.fdroid.R +import org.fdroid.ui.FDroidContent +import org.fdroid.ui.utils.BackButton +import org.fdroid.ui.utils.BigLoadingIndicator + +@Composable +@OptIn(ExperimentalMaterial3Api::class) +fun Categories( + categories: List?, + onNav: (NavKey) -> Unit, + onBackClicked: () -> Unit, + modifier: Modifier = Modifier, +) { + val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) + Scaffold( + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(R.string.main_menu__categories), + maxLines = 1, + overflow = TextOverflow.MiddleEllipsis, + ) + }, + navigationIcon = { BackButton(onClick = onBackClicked) }, + scrollBehavior = scrollBehavior, + ) + }, + modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + ) { paddingValues -> + val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() } + if (categories == null) { + BigLoadingIndicator(modifier.padding(paddingValues)) + } else { + LazyColumn( + state = listState, + contentPadding = paddingValues, + verticalArrangement = spacedBy(8.dp), + ) { + items(items = categories, key = { it.id }, contentType = { "C" }) { categoryItem -> + CategoryRow(categoryItem, onNav) + } + } + } + } +} + +@Composable +@PreviewLightDark +private fun PreviewLoading() { + FDroidContent { + Categories(null, {}, {}) + } +} + +@Composable +@PreviewLightDark +private fun Preview() { + FDroidContent { + val categories = + listOf( + CategoryItem("App Store & Updater", "App Store & Updater"), + CategoryItem("Browser", "Browser"), + CategoryItem("Calendar & Agenda", "Calendar & Agenda"), + CategoryItem("Cloud Storage & File Sync", "Cloud Storage & File Sync"), + CategoryItem("Connectivity", "Connectivity"), + CategoryItem("Development", "Development"), + CategoryItem("doesn't exist", "Foo bar"), + ) + Categories(categories, {}, {}) + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt new file mode 100644 index 000000000..93d4e0262 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt @@ -0,0 +1,28 @@ +package org.fdroid.ui.categories + +import android.app.Application +import androidx.core.os.LocaleListCompat +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.asFlow +import dagger.hilt.android.lifecycle.HiltViewModel +import java.text.Collator +import java.util.Locale +import javax.inject.Inject +import kotlinx.coroutines.flow.map +import org.fdroid.database.FDroidDatabase + +@HiltViewModel +class CategoriesViewModel @Inject constructor(app: Application, db: FDroidDatabase) : + AndroidViewModel(app) { + private val collator = Collator.getInstance(Locale.getDefault()) + private val localeList = LocaleListCompat.getDefault() + + val categories = + db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> + categories + .map { category -> + CategoryItem(id = category.id, name = category.getName(localeList) ?: "Unknown Category") + } + .sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) } + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt index 8398d3b5c..7ce609cce 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.unit.sp import androidx.navigation3.runtime.NavKey import org.fdroid.R import org.fdroid.ui.FDroidContent +import org.fdroid.ui.navigation.NavigationKey @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) @@ -29,7 +30,7 @@ fun CategoryList( onNav: (NavKey) -> Unit, modifier: Modifier = Modifier, ) { - val onAllCategories = { /* TODO */ } + val onAllCategories = { onNav(NavigationKey.Categories) } AnimatedVisibility(!categoryList.isNullOrEmpty()) { Column(modifier = modifier) { Row( diff --git a/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt b/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt index 4322902a2..2d0b11491 100644 --- a/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt +++ b/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt @@ -39,6 +39,8 @@ sealed interface NavigationKey : NavKey { @Serializable data class AppList(val type: AppListType) : NavigationKey + @Serializable data object Categories : NavigationKey + @Serializable data object Repos : NavigationKey @Serializable data class RepoDetails(val repoId: Long) : NavigationKey From c0f960833a5b248de177a2c3825e76335ac3a691 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Apr 2026 09:39:46 -0300 Subject: [PATCH 20/38] Use different search icon for list word filter --- app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt | 6 +++--- app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt | 4 ++-- app/src/main/res/values/strings.xml | 1 + .../fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png | 4 ++-- .../ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png | 4 ++-- .../MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png | 4 ++-- .../MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png | 4 ++-- .../MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png | 4 ++-- .../lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png | 4 ++-- .../ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png | 4 ++-- .../ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png | 4 ++-- 11 files changed, 22 insertions(+), 21 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt index 177c32577..878aceccd 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt @@ -7,9 +7,9 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ManageSearch import androidx.compose.material.icons.automirrored.filled.Sort import androidx.compose.material.icons.filled.AccessTime -import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.SortByAlpha import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem @@ -96,8 +96,8 @@ fun MyApps( title = { Text(stringResource(R.string.menu_apps_my)) }, actions = { TopAppBarButton( - imageVector = Icons.Filled.Search, - contentDescription = stringResource(R.string.menu_search), + imageVector = Icons.AutoMirrored.Default.ManageSearch, + contentDescription = stringResource(R.string.search_my_apps_hint), onClick = { searchActive = true }, ) var sortByMenuExpanded by remember { mutableStateOf(false) } diff --git a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt index db68890c9..aed48dc13 100644 --- a/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/lists/AppList.kt @@ -17,8 +17,8 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ManageSearch import androidx.compose.material.icons.filled.FilterList -import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api @@ -132,7 +132,7 @@ fun AppList( }, actions = { TopAppBarButton( - imageVector = Icons.Filled.Search, + imageVector = Icons.AutoMirrored.Default.ManageSearch, contentDescription = stringResource(R.string.menu_search), onClick = { searchActive = true }, ) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4cf230cca..7135c96ca 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -487,6 +487,7 @@ This often occurs with apps installed via Google Play or other sources, if they Show extra info and enable extra settings Search apps + Search installed apps App compatibility Include incompatible versions diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png index a3fe519ee..dc032ca1c 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsEmpty_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1945387654f00f1a8e91757b6d8ae700d35b1fc1b078118f7866a938cc5b5598 -size 44161 +oid sha256:17943eea5fc8b02e415e2bbd5c4db5477cb9b2168ca2018cfc3db7193c483fe7 +size 44031 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png index 65f12ee8b..aae4197da 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsFullList_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1501c756d19f294b3f71ee089a1e728b3ade04b91372617248f68cb60aecfe1f -size 158114 +oid sha256:11d1141d017dd1bc3013d8aa16dc466e0fad8863fec8084acd8e49549fa78ae6 +size 157993 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png index 309a6b33d..95c8a682c 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsInstalledAndIssues_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:619eb7e9478b45a8b1ada0b65335fad718cb728f500ab04c77bb292933df5c84 -size 114633 +oid sha256:9e8e34dc95d9b2b264fafa4bcf601936dfcb707d11abe3cc05000ae5d0bd0d14 +size 114468 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png index dac56789a..62f886c83 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsOnlyInstalledAppsAndNoInternet_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:d915f7d6abf21e98310c1806371c071eb04e62d9104a5f6c06d933f1fffbb237 -size 88468 +oid sha256:9c23a46a628d49fe90c7bf494bb0deb7a86121780d8e608e7ff8bf75c7cb63af +size 88320 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png index 8774b310a..d45d6c05e 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/apps/MyAppsTestKt/MyAppsUpdatesOnlyAndNoInternet_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8c6ea083fcbe5e69a47e88498ab197b4eab85ab129750290bb634815cf9166b7 -size 83745 +oid sha256:5684aa8762d69b4ce448a526e6d8f0ad2c65dd690a40c8764384e596eaf0ef7a +size 83590 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png index 6e3ea2dd7..284615bf6 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListLoadingTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3e0ccc94cd164fd9b0b5b5419d4961f4ad3f5d7467f656ed2564426266edf424 -size 30001 +oid sha256:b4be452a3d0bf27035f605f143e409c2a62f5f19bcde05d53b25d5f176ac7d5c +size 29881 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png index 4ba2abea2..26ceedb37 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListNightTest_1def2e7f_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:eedc635af9bb23df02184f47f4359eb5c9a73d62e972c224c4791a3af61b6b49 -size 81100 +oid sha256:700f140725b0f3e42ef8fe40ed2193bb1235ec65e0ef4504a8e4af365230b566 +size 80956 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png index 596d60e99..6c941d4a7 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/lists/AppListTestKt/AppListTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2890befec321390ce0fd43965f1495b45550a6f258b38429c432100823e2370b -size 78311 +oid sha256:03a251fcbc37a99def2b5544e0e245e587e3bfdc507495a9adcf3548c6f247c6 +size 78178 From 85b27fe1edd50629e530253bb1469a9e8ebba232 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Apr 2026 10:00:32 -0300 Subject: [PATCH 21/38] Fix issues related to blank searches Now we require at least 3 chars to search or one ideographic character. Also, we ignore blank searches and don't save those in recent searches. --- app/src/main/kotlin/org/fdroid/search/SearchHelper.kt | 3 +++ app/src/main/kotlin/org/fdroid/search/SearchManager.kt | 5 +---- .../kotlin/org/fdroid/ui/search/AppSearchInputField.kt | 10 +++++----- .../main/kotlin/org/fdroid/ui/search/SearchResults.kt | 6 +++--- .../kotlin/org/fdroid/ui/search/SearchViewModel.kt | 3 ++- 5 files changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt b/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt index 5471848cd..a90b1c46d 100644 --- a/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt +++ b/app/src/main/kotlin/org/fdroid/search/SearchHelper.kt @@ -6,6 +6,9 @@ import java.text.Normalizer.Form.NFKD object SearchHelper { private val normalizerRegex = "\\p{M}".toRegex() + fun CharSequence.isSearchable(): Boolean = + isNotBlank() && (length >= 3 || any { Character.isIdeographic(it.code) }) + /** Normalizes the string by removing any diacritics that may appear. */ fun String.normalize(): String { if (Normalizer.isNormalized(this, NFKD)) return this diff --git a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt index 2b60d747a..5e42da63a 100644 --- a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt +++ b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt @@ -28,16 +28,13 @@ import org.fdroid.download.PackageName import org.fdroid.download.getImageModel import org.fdroid.index.RepoManager import org.fdroid.install.InstalledAppsCache +import org.fdroid.search.SearchHelper.normalize import org.fdroid.settings.SettingsManager import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.lists.AppListItem -import org.fdroid.search.SearchHelper.normalize import org.fdroid.ui.search.SearchResults import org.fdroid.utils.IoDispatcher -/** The minimum amount of characters we start auto-searching for. */ -const val SEARCH_THRESHOLD = 2 - @Singleton class SearchManager @Inject diff --git a/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt b/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt index 62aad39ee..74c2a1b1b 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/AppSearchInputField.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.fdroid.R -import org.fdroid.search.SEARCH_THRESHOLD +import org.fdroid.search.SearchHelper.isSearchable @Composable @OptIn(ExperimentalMaterial3Api::class, FlowPreview::class) @@ -41,10 +41,10 @@ fun AppSearchInputField( snapshotFlow { textFieldState.text } .distinctUntilChanged() .debounce(500) - .collectLatest { - if (it.isEmpty()) { + .collectLatest { query -> + if (query.isEmpty()) { onSearchCleared() - } else if (it.length >= SEARCH_THRESHOLD) { + } else if (query.isSearchable()) { onSearch(textFieldState.text.toString()) } } @@ -54,7 +54,7 @@ fun AppSearchInputField( searchBarState = searchBarState, textFieldState = textFieldState, textStyle = MaterialTheme.typography.bodyLarge, - onSearch = { scope.launch { onSearch(it) } }, + onSearch = { if (it.isSearchable()) scope.launch { onSearch(it) } }, placeholder = { Text(stringResource(R.string.search_placeholder)) }, trailingIcon = { if (textFieldState.text.isNotEmpty()) { diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt index 544e22107..5470ca4ef 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import org.fdroid.R -import org.fdroid.search.SEARCH_THRESHOLD import org.fdroid.search.SavedSearch +import org.fdroid.search.SearchHelper.isSearchable import org.fdroid.ui.categories.CategoryChip import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.categories.ChipFlowRow @@ -55,7 +55,7 @@ fun SearchResults( val listState = rememberSaveable(searchResults, saver = LazyListState.Saver) { LazyListState(0, 0) } if (searchResults == null) { - if (textFieldState.text.length >= SEARCH_THRESHOLD) { + if (textFieldState.text.isSearchable()) { BigLoadingIndicator(modifier.padding(paddingValues).imePadding()) } else { if (!savedSearches.isNullOrEmpty()) { @@ -68,7 +68,7 @@ fun SearchResults( ) } } - } else if (searchResults.apps.isEmpty() && textFieldState.text.length >= SEARCH_THRESHOLD) { + } else if (searchResults.apps.isEmpty() && textFieldState.text.isSearchable()) { Column(modifier = modifier.padding(paddingValues)) { if (searchResults.categories.isNotEmpty()) { CategoriesFlowRow(searchResults.categories, onNav) diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt index 5ad895b42..90c81ebc1 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import jakarta.inject.Inject import kotlinx.coroutines.launch +import org.fdroid.search.SearchHelper.isSearchable import org.fdroid.search.SearchManager @HiltViewModel @@ -16,7 +17,7 @@ constructor(app: Application, private val searchManager: SearchManager) : Androi val searchResults = searchManager.searchResults val savedSearchesFlow = searchManager.savedSearches - suspend fun search(term: String) = searchManager.search(term) + suspend fun search(term: String) = if (term.isSearchable()) searchManager.search(term) else Unit fun onSearchCleared() = searchManager.onSearchCleared() From 37dc5bec0fdd3d0ec135d4a7353b17bb8d91a40b Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Apr 2026 10:41:32 -0300 Subject: [PATCH 22/38] Show all categories in empty state of search screen --- .../kotlin/org/fdroid/search/SearchManager.kt | 6 +- app/src/main/kotlin/org/fdroid/ui/Main.kt | 1 + .../org/fdroid/ui/search/GlobalSearch.kt | 14 +- .../org/fdroid/ui/search/PastSearches.kt | 83 +++----- .../org/fdroid/ui/search/SearchResults.kt | 30 ++- .../org/fdroid/ui/search/SearchViewModel.kt | 1 + .../org/fdroid/ui/search/GlobalSearchTest.kt | 194 +++++++++--------- .../GlobalSearchEmptyStateTest_2ed8e27d_0.png | 4 +- 8 files changed, 167 insertions(+), 166 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt index 5e42da63a..123f90873 100644 --- a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt +++ b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt @@ -52,7 +52,9 @@ constructor( private val collator = Collator.getInstance(Locale.getDefault()) private val _searchResults = MutableStateFlow(null) private val _savedSearches = MutableStateFlow?>(null) - private val categories = + private var searchJob: SearchJob? = null + + val categories = db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> categories .map { category -> @@ -60,8 +62,6 @@ constructor( } .sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) } } - private var searchJob: SearchJob? = null - val searchResults = _searchResults.asStateFlow() val savedSearches = _savedSearches.asStateFlow() diff --git a/app/src/main/kotlin/org/fdroid/ui/Main.kt b/app/src/main/kotlin/org/fdroid/ui/Main.kt index 76bab88fa..e541a57f8 100644 --- a/app/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/app/src/main/kotlin/org/fdroid/ui/Main.kt @@ -73,6 +73,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) { GlobalSearch( searchResults = viewModel.searchResults.collectAsStateWithLifecycle().value, savedSearches = viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value, + categories = viewModel.categories.collectAsStateWithLifecycle(null).value, onSearch = viewModel::search, onClearSavedSearches = viewModel::onClearSearchHistory, onNav = { navKey -> navigator.navigate(navKey) }, diff --git a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt index 197c5c266..9b4d5aa73 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview import org.fdroid.search.SavedSearch import org.fdroid.ui.FDroidContent +import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.navigation.NavigationKey import org.fdroid.ui.utils.appListItems import org.fdroid.ui.utils.categoryItems @@ -27,6 +28,7 @@ fun GlobalSearch( textFieldState: TextFieldState = rememberTextFieldState(), searchResults: SearchResults?, savedSearches: List? = null, + categories: List? = null, onSearch: suspend (String) -> Unit, onClearSavedSearches: () -> Unit, onNav: (NavigationKey) -> Unit, @@ -60,6 +62,7 @@ fun GlobalSearch( searchResults = searchResults, textFieldState = textFieldState, savedSearches = savedSearches, + categories = categories, onClearSavedSearches = onClearSavedSearches, onNav = onNav, ) @@ -72,7 +75,15 @@ fun GlobalSearch( private fun AppsSearchLoadingPreview() { FDroidContent { val textFieldState = rememberTextFieldState("foo bar") - Box(Modifier.fillMaxSize()) { GlobalSearch(textFieldState, null, null, {}, {}, {}, {}, {}) } + Box(Modifier.fillMaxSize()) { GlobalSearch( + textFieldState, + null, + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) } } } @@ -89,6 +100,7 @@ private fun AppsSearchEmptyStatePreview() { textFieldState = textFieldState, searchResults = null, savedSearches = savedSearches, + categories = categoryItems, onSearch = {}, onClearSavedSearches = {}, onNav = {}, diff --git a/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt b/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt index 09271a0fd..f6d536017 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/PastSearches.kt @@ -1,17 +1,11 @@ package org.fdroid.ui.search import androidx.compose.foundation.background -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListScope import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons @@ -23,6 +17,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.res.stringResource @@ -32,51 +27,41 @@ import org.fdroid.R import org.fdroid.search.SavedSearch import org.fdroid.ui.FDroidContent -@Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun PastSearches( +fun LazyListScope.pastSearches( savedSearches: List, onSearch: (String) -> Unit, onClearSavedSearches: () -> Unit, - modifier: Modifier = Modifier, - paddingValues: PaddingValues = PaddingValues(), ) { - LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = paddingValues) { - item { - Row { - Text( - text = stringResource(R.string.search_history), - style = MaterialTheme.typography.labelLarge, - modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp).weight(1f), + item { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.search_history), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier.padding(horizontal = 16.dp).weight(1f), + ) + TextButton(onClick = onClearSavedSearches, modifier = Modifier.padding(horizontal = 8.dp)) { + Text(stringResource(R.string.clear)) + } + } + } + items(savedSearches) { item -> + ListItem( + leadingContent = { + Icon( + Icons.Default.History, + contentDescription = null, + modifier = + Modifier.clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainer) + .padding(8.dp), ) - TextButton( - onClick = onClearSavedSearches, - modifier = Modifier.padding(horizontal = 8.dp), - ) { - Text(stringResource(R.string.clear)) - } - } + }, + onClick = { onSearch(item.query) }, + modifier = Modifier.fillMaxWidth().animateItem(), + ) { + Text(item.query) } - items(savedSearches) { item -> - ListItem( - leadingContent = { - Icon( - Icons.Default.History, - contentDescription = null, - modifier = - Modifier.clip(CircleShape) - .background(MaterialTheme.colorScheme.surfaceContainer) - .padding(8.dp), - ) - }, - onClick = { onSearch(item.query) }, - modifier = Modifier.fillMaxWidth().animateItem(), - ) { - Text(item.query) - } - } - item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } - item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } } @@ -86,10 +71,8 @@ private fun Preview() { FDroidContent { val savedSearches = listOf(SavedSearch(1, "foo"), SavedSearch(2, "foo bar"), SavedSearch(3, "foobar")) - PastSearches( - savedSearches = savedSearches, - onClearSavedSearches = {}, - onSearch = {}, - ) + LazyColumn { + pastSearches(savedSearches = savedSearches, onClearSavedSearches = {}, onSearch = {}) + } } } diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt index 5470ca4ef..25de71bbb 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt @@ -47,6 +47,7 @@ fun SearchResults( searchResults: SearchResults?, textFieldState: TextFieldState, savedSearches: List?, + categories: List?, onClearSavedSearches: () -> Unit, onNav: (NavigationKey) -> Unit, modifier: Modifier = Modifier, @@ -58,14 +59,19 @@ fun SearchResults( if (textFieldState.text.isSearchable()) { BigLoadingIndicator(modifier.padding(paddingValues).imePadding()) } else { - if (!savedSearches.isNullOrEmpty()) { - PastSearches( - savedSearches = savedSearches, - onSearch = { textFieldState.edit { replace(0, length, it) } }, - onClearSavedSearches = onClearSavedSearches, - modifier = modifier, - paddingValues = paddingValues, - ) + LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = paddingValues) { + if (!savedSearches.isNullOrEmpty()) { + pastSearches( + savedSearches = savedSearches, + onSearch = { textFieldState.edit { replace(0, length, it) } }, + onClearSavedSearches = onClearSavedSearches, + ) + } + if (!categories.isNullOrEmpty()) { + item { CategoriesFlowRow(categories, onNav, modifier = Modifier.padding(top = 16.dp)) } + } + item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.systemBars)) } + item { Spacer(Modifier.windowInsetsBottomHeight(WindowInsets.ime)) } } } } else if (searchResults.apps.isEmpty() && textFieldState.text.isSearchable()) { @@ -121,8 +127,12 @@ fun SearchResults( } @Composable -private fun CategoriesFlowRow(categories: List, onNav: (NavigationKey) -> Unit) { - Column(modifier = Modifier.padding(horizontal = 8.dp)) { +private fun CategoriesFlowRow( + categories: List, + onNav: (NavigationKey) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier.padding(horizontal = 8.dp)) { Text( text = stringResource(R.string.main_menu__categories), style = MaterialTheme.typography.labelLarge, diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt index 90c81ebc1..2b4238668 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt @@ -16,6 +16,7 @@ constructor(app: Application, private val searchManager: SearchManager) : Androi val searchResults = searchManager.searchResults val savedSearchesFlow = searchManager.savedSearches + val categories = searchManager.categories suspend fun search(term: String) = if (term.isSearchable()) searchManager.search(term) else Unit diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt index 2d9fb24ca..3e8acaf62 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt @@ -13,112 +13,107 @@ import org.fdroid.ui.utils.categoryItems @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true) -fun GlobalSearchEmptyTest() = - ScreenshotTest { - val textFieldState = rememberTextFieldState() - GlobalSearch( - textFieldState = textFieldState, - searchResults = null, - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) - } +fun GlobalSearchEmptyTest() = ScreenshotTest { + val textFieldState = rememberTextFieldState() + GlobalSearch( + textFieldState = textFieldState, + searchResults = null, + savedSearches = emptyList(), + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) +} @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true) -fun GlobalSearchEmptyStateTest() = - ScreenshotTest { - val savedSearches = - listOf(SavedSearch(1, "Browser"), SavedSearch(2, "media player"), SavedSearch(3, "email app")) - val textFieldState = rememberTextFieldState() - GlobalSearch( - textFieldState = textFieldState, - searchResults = null, - savedSearches = savedSearches, - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) - } +fun GlobalSearchEmptyStateTest() = ScreenshotTest { + val savedSearches = + listOf(SavedSearch(1, "Browser"), SavedSearch(2, "media player"), SavedSearch(3, "email app")) + val textFieldState = rememberTextFieldState() + GlobalSearch( + textFieldState = textFieldState, + searchResults = null, + savedSearches = savedSearches, + categories = categoryItems, + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) +} @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true) -fun GlobalSearchLoadingTest() = - ScreenshotTest { - val textFieldState = rememberTextFieldState("foo bar") - GlobalSearch( - textFieldState = textFieldState, - searchResults = null, - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) - } +fun GlobalSearchLoadingTest() = ScreenshotTest { + val textFieldState = rememberTextFieldState("foo bar") + GlobalSearch( + textFieldState = textFieldState, + searchResults = null, + savedSearches = emptyList(), + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) +} @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true) -fun GlobalSearchNoResultsTest() = - ScreenshotTest { - val textFieldState = rememberTextFieldState("foo") - GlobalSearch( - textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), emptyList()), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) - } +fun GlobalSearchNoResultsTest() = ScreenshotTest { + val textFieldState = rememberTextFieldState("foo") + GlobalSearch( + textFieldState = textFieldState, + searchResults = SearchResults(emptyList(), emptyList()), + savedSearches = emptyList(), + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) +} @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true) -fun GlobalSearchResultsTest() = - ScreenshotTest { - val textFieldState = rememberTextFieldState("foo bar") - GlobalSearch( - textFieldState = textFieldState, - searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) - } +fun GlobalSearchResultsTest() = ScreenshotTest { + val textFieldState = rememberTextFieldState("foo bar") + GlobalSearch( + textFieldState = textFieldState, + searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), + savedSearches = emptyList(), + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) +} @Composable @PreviewTest @Preview(showBackground = true, showSystemUi = true) -fun GlobalSearchOnlyCategoriesTest() = - ScreenshotTest { - val textFieldState = rememberTextFieldState("foo bar") - GlobalSearch( - textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) - } +fun GlobalSearchOnlyCategoriesTest() = ScreenshotTest { + val textFieldState = rememberTextFieldState("foo bar") + GlobalSearch( + textFieldState = textFieldState, + searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9)), + savedSearches = emptyList(), + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) +} @Composable @PreviewTest @@ -127,17 +122,16 @@ fun GlobalSearchOnlyCategoriesTest() = showSystemUi = true, uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL, ) -fun GlobalSearchResultsNightTest() = - ScreenshotTest { - val textFieldState = rememberTextFieldState("foo bar") - GlobalSearch( - textFieldState = textFieldState, - searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) - } +fun GlobalSearchResultsNightTest() = ScreenshotTest { + val textFieldState = rememberTextFieldState("foo bar") + GlobalSearch( + textFieldState = textFieldState, + searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), + savedSearches = emptyList(), + onSearch = {}, + onClearSavedSearches = {}, + onNav = {}, + onBack = {}, + onSearchCleared = {}, + ) +} diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png index 0737be26d..0d65142d8 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6eb1c82199e7d0ace698bdc95b523bef51a761eaa22698dfefae3ff5f2419ca3 -size 56180 +oid sha256:e926d11d01b2d8786e9af5077f60d47b51d23605e016559c94b998c85edd44f0 +size 119038 From 5359d49648fb5775c4c5f345e06ef553cd6fd9ae Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Apr 2026 17:40:56 -0300 Subject: [PATCH 23/38] Refactor global search to take fewer parameters --- .../ui/screenshots/SearchScreenshotTest.kt | 7 +-- app/src/main/kotlin/org/fdroid/ui/Main.kt | 21 +++++-- .../org/fdroid/ui/search/GlobalSearch.kt | 60 +++++-------------- .../kotlin/org/fdroid/ui/search/SearchInfo.kt | 19 ++++++ .../org/fdroid/ui/search/SearchViewModel.kt | 13 ++-- .../org/fdroid/ui/utils/PreviewUtils.kt | 26 ++++++++ .../org/fdroid/ui/search/GlobalSearchTest.kt | 49 ++++----------- 7 files changed, 100 insertions(+), 95 deletions(-) create mode 100644 app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt index efba3c953..949e08ffc 100644 --- a/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt +++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/SearchScreenshotTest.kt @@ -7,6 +7,7 @@ import org.fdroid.ui.lists.AppListItem import org.fdroid.ui.navigation.NavigationKey import org.fdroid.ui.search.GlobalSearch import org.fdroid.ui.search.SearchResults +import org.fdroid.ui.utils.getSearchInfo import org.junit.Test import org.junit.runner.RunWith import org.junit.runners.Parameterized @@ -28,14 +29,10 @@ class SearchScreenshotTest(localeName: String) : LocalizedScreenshotTest(localeN apps = getSearchResultItems(localeList), ) GlobalSearch( + info = getSearchInfo(results, emptyList()), textFieldState = rememberTextFieldState("work"), - searchResults = results, - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/Main.kt b/app/src/main/kotlin/org/fdroid/ui/Main.kt index e541a57f8..56f07e8a0 100644 --- a/app/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/app/src/main/kotlin/org/fdroid/ui/Main.kt @@ -15,9 +15,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation3.runtime.NavEntry import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider +import org.fdroid.search.SavedSearch import org.fdroid.ui.apps.myAppsEntry import org.fdroid.ui.categories.Categories import org.fdroid.ui.categories.CategoriesViewModel +import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.details.NoAppSelected import org.fdroid.ui.details.appDetailsEntry import org.fdroid.ui.discover.discoverEntry @@ -34,6 +36,9 @@ import org.fdroid.ui.navigation.toEntries import org.fdroid.ui.navigation.topLevelRoutes import org.fdroid.ui.repositories.repoEntry import org.fdroid.ui.search.GlobalSearch +import org.fdroid.ui.search.SearchActions +import org.fdroid.ui.search.SearchInfo +import org.fdroid.ui.search.SearchResults import org.fdroid.ui.search.SearchViewModel import org.fdroid.ui.settings.Settings import org.fdroid.ui.settings.SettingsViewModel @@ -71,14 +76,18 @@ fun Main(onListeningForIntent: () -> Unit = {}) { ) { val viewModel = hiltViewModel() GlobalSearch( - searchResults = viewModel.searchResults.collectAsStateWithLifecycle().value, - savedSearches = viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value, - categories = viewModel.categories.collectAsStateWithLifecycle(null).value, - onSearch = viewModel::search, - onClearSavedSearches = viewModel::onClearSearchHistory, + info = + object : SearchInfo { + override val searchResults: SearchResults? = + viewModel.searchResults.collectAsStateWithLifecycle().value + override val savedSearches: List? = + viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value + override val categories: List? = + viewModel.categories.collectAsStateWithLifecycle(null).value + override val actions: SearchActions = viewModel + }, onNav = { navKey -> navigator.navigate(navKey) }, onBack = { navigator.goBack() }, - onSearchCleared = viewModel::onSearchCleared, ) } entry(NavigationKey.Categories) { diff --git a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt index 9b4d5aa73..44c3d1062 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt @@ -17,33 +17,28 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview import org.fdroid.search.SavedSearch import org.fdroid.ui.FDroidContent -import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.navigation.NavigationKey import org.fdroid.ui.utils.appListItems import org.fdroid.ui.utils.categoryItems +import org.fdroid.ui.utils.getSearchInfo @Composable @OptIn(ExperimentalMaterial3Api::class) fun GlobalSearch( + info: SearchInfo, textFieldState: TextFieldState = rememberTextFieldState(), - searchResults: SearchResults?, - savedSearches: List? = null, - categories: List? = null, - onSearch: suspend (String) -> Unit, - onClearSavedSearches: () -> Unit, onNav: (NavigationKey) -> Unit, onBack: () -> Unit, - onSearchCleared: () -> Unit, ) { + val searchResults = info.searchResults Scaffold( topBar = { TopSearchBar( searchFieldState = textFieldState, shouldRequestFocus = // only show keyboard if there are no results to show - searchResults == null || - searchResults.apps.isEmpty() && searchResults.categories.isEmpty(), - onSearch = onSearch, - onSearchCleared = onSearchCleared, + searchResults == null || searchResults.apps.isEmpty() && searchResults.categories.isEmpty(), + onSearch = info.actions::onSearch, + onSearchCleared = info.actions::onSearchCleared, onHideSearch = onBack, ) } @@ -61,9 +56,9 @@ fun GlobalSearch( paddingValues = paddingValues, searchResults = searchResults, textFieldState = textFieldState, - savedSearches = savedSearches, - categories = categories, - onClearSavedSearches = onClearSavedSearches, + savedSearches = info.savedSearches, + categories = info.categories, + onClearSavedSearches = info.actions::onClearSearchHistory, onNav = onNav, ) } @@ -75,15 +70,9 @@ fun GlobalSearch( private fun AppsSearchLoadingPreview() { FDroidContent { val textFieldState = rememberTextFieldState("foo bar") - Box(Modifier.fillMaxSize()) { GlobalSearch( - textFieldState, - null, - onSearch = {}, - onClearSavedSearches = {}, - onNav = {}, - onBack = {}, - onSearchCleared = {}, - ) } + Box(Modifier.fillMaxSize()) { + GlobalSearch(info = getSearchInfo(), textFieldState = textFieldState, onNav = {}, onBack = {}) + } } } @@ -97,15 +86,10 @@ private fun AppsSearchEmptyStatePreview() { listOf(SavedSearch(1, "foo"), SavedSearch(2, "foo bar"), SavedSearch(3, "foobar")) Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(null, savedSearches, categoryItems), textFieldState = textFieldState, - searchResults = null, - savedSearches = savedSearches, - categories = categoryItems, - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } @@ -119,14 +103,10 @@ private fun AppsSearchNoResultsPreview() { val textFieldState = rememberTextFieldState("foo") Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(SearchResults(emptyList(), emptyList()), emptyList(), emptyList()), textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), emptyList()), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } @@ -140,14 +120,10 @@ private fun AppsSearchOnlyCategoriesPreview() { val textFieldState = rememberTextFieldState() Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(SearchResults(emptyList(), categoryItems.subList(3, 5)), emptyList()), textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), categoryItems.subList(3, 5)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } @@ -161,14 +137,10 @@ private fun AppsSearchPreview() { val textFieldState = rememberTextFieldState() Box(Modifier.fillMaxSize()) { GlobalSearch( + info = getSearchInfo(SearchResults(appListItems, categoryItems.subList(0, 4)), emptyList()), textFieldState = textFieldState, - searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt new file mode 100644 index 000000000..152da6ff1 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt @@ -0,0 +1,19 @@ +package org.fdroid.ui.search + +import org.fdroid.search.SavedSearch +import org.fdroid.ui.categories.CategoryItem + +interface SearchInfo { + val searchResults: SearchResults? + val savedSearches: List? + val categories: List? + val actions: SearchActions +} + +interface SearchActions { + suspend fun onSearch(term: String) + + fun onSearchCleared() + + fun onClearSearchHistory() +} diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt index 2b4238668..25ba59b17 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt @@ -12,15 +12,20 @@ import org.fdroid.search.SearchManager @HiltViewModel class SearchViewModel @Inject -constructor(app: Application, private val searchManager: SearchManager) : AndroidViewModel(app) { +constructor(app: Application, private val searchManager: SearchManager) : + AndroidViewModel(app), SearchActions { val searchResults = searchManager.searchResults val savedSearchesFlow = searchManager.savedSearches val categories = searchManager.categories - suspend fun search(term: String) = if (term.isSearchable()) searchManager.search(term) else Unit + override suspend fun onSearch(term: String) { + if (term.isSearchable()) searchManager.search(term) + } - fun onSearchCleared() = searchManager.onSearchCleared() + override fun onSearchCleared() = searchManager.onSearchCleared() - fun onClearSearchHistory() = viewModelScope.launch { searchManager.onClearSearchHistory() } + override fun onClearSearchHistory() { + viewModelScope.launch { searchManager.onClearSearchHistory() } + } } diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 1d55eb87d..cd23062fd 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -22,6 +22,7 @@ import org.fdroid.index.v2.SignerV2 import org.fdroid.install.InstallConfirmationState import org.fdroid.install.InstallState import org.fdroid.repo.RepoUpdateProgress +import org.fdroid.search.SavedSearch import org.fdroid.ui.apps.AppUpdateItem import org.fdroid.ui.apps.AppWithIssueItem import org.fdroid.ui.apps.InstalledAppItem @@ -49,6 +50,9 @@ import org.fdroid.ui.repositories.details.RepoDetailsActions import org.fdroid.ui.repositories.details.RepoDetailsInfo import org.fdroid.ui.repositories.details.RepoDetailsModel import org.fdroid.ui.repositories.details.UserMirrorItem +import org.fdroid.ui.search.SearchActions +import org.fdroid.ui.search.SearchInfo +import org.fdroid.ui.search.SearchResults object Names { val randomName: String @@ -634,3 +638,25 @@ fun getRepository( password = password, lastError = lastError, ) + +fun getSearchInfo( + searchResults: SearchResults? = null, + savedSearches: List? = null, + categories: List? = null, +) = + object : SearchInfo { + override val searchResults: SearchResults? = searchResults + + override val savedSearches: List? = savedSearches + + override val categories: List? = categories + + override val actions: SearchActions = + object : SearchActions { + override suspend fun onSearch(term: String) {} + + override fun onSearchCleared() {} + + override fun onClearSearchHistory() {} + } + } diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt index 3e8acaf62..06bea13e6 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/search/GlobalSearchTest.kt @@ -9,6 +9,7 @@ import org.fdroid.search.SavedSearch import org.fdroid.ui.ScreenshotTest import org.fdroid.ui.utils.appListItems import org.fdroid.ui.utils.categoryItems +import org.fdroid.ui.utils.getSearchInfo @Composable @PreviewTest @@ -16,14 +17,10 @@ import org.fdroid.ui.utils.categoryItems fun GlobalSearchEmptyTest() = ScreenshotTest { val textFieldState = rememberTextFieldState() GlobalSearch( + info = getSearchInfo(savedSearches = emptyList()), textFieldState = textFieldState, - searchResults = null, - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } @@ -35,15 +32,15 @@ fun GlobalSearchEmptyStateTest() = ScreenshotTest { listOf(SavedSearch(1, "Browser"), SavedSearch(2, "media player"), SavedSearch(3, "email app")) val textFieldState = rememberTextFieldState() GlobalSearch( + info = + getSearchInfo( + searchResults = null, + savedSearches = savedSearches, + categories = categoryItems, + ), textFieldState = textFieldState, - searchResults = null, - savedSearches = savedSearches, - categories = categoryItems, - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } @@ -53,14 +50,10 @@ fun GlobalSearchEmptyStateTest() = ScreenshotTest { fun GlobalSearchLoadingTest() = ScreenshotTest { val textFieldState = rememberTextFieldState("foo bar") GlobalSearch( + info = getSearchInfo(savedSearches = emptyList()), textFieldState = textFieldState, - searchResults = null, - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } @@ -70,14 +63,10 @@ fun GlobalSearchLoadingTest() = ScreenshotTest { fun GlobalSearchNoResultsTest() = ScreenshotTest { val textFieldState = rememberTextFieldState("foo") GlobalSearch( + info = getSearchInfo(searchResults = SearchResults(emptyList(), emptyList())), textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), emptyList()), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } @@ -87,14 +76,10 @@ fun GlobalSearchNoResultsTest() = ScreenshotTest { fun GlobalSearchResultsTest() = ScreenshotTest { val textFieldState = rememberTextFieldState("foo bar") GlobalSearch( + info = getSearchInfo(searchResults = SearchResults(appListItems, categoryItems.subList(0, 4))), textFieldState = textFieldState, - searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } @@ -104,14 +89,10 @@ fun GlobalSearchResultsTest() = ScreenshotTest { fun GlobalSearchOnlyCategoriesTest() = ScreenshotTest { val textFieldState = rememberTextFieldState("foo bar") GlobalSearch( + info = getSearchInfo(searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9))), textFieldState = textFieldState, - searchResults = SearchResults(emptyList(), categoryItems.subList(5, 9)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } @@ -125,13 +106,9 @@ fun GlobalSearchOnlyCategoriesTest() = ScreenshotTest { fun GlobalSearchResultsNightTest() = ScreenshotTest { val textFieldState = rememberTextFieldState("foo bar") GlobalSearch( + info = getSearchInfo(searchResults = SearchResults(appListItems, categoryItems.subList(0, 4))), textFieldState = textFieldState, - searchResults = SearchResults(appListItems, categoryItems.subList(0, 4)), - savedSearches = emptyList(), - onSearch = {}, - onClearSavedSearches = {}, onNav = {}, onBack = {}, - onSearchCleared = {}, ) } From 16b4021b30e75084ecfe8b34dcfa0260abb654a2 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 22 Apr 2026 17:58:35 -0300 Subject: [PATCH 24/38] Show keyboard when tapping Search in bottom nav a second time --- app/src/main/kotlin/org/fdroid/ui/Main.kt | 21 ++++++++++++++++++- .../org/fdroid/ui/navigation/BottomBar.kt | 4 ++-- .../org/fdroid/ui/search/GlobalSearch.kt | 8 +++++-- .../kotlin/org/fdroid/ui/search/SearchInfo.kt | 2 ++ .../org/fdroid/ui/search/SearchResults.kt | 7 ++++++- .../org/fdroid/ui/utils/PreviewUtils.kt | 3 +++ 6 files changed, 39 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/Main.kt b/app/src/main/kotlin/org/fdroid/ui/Main.kt index 56f07e8a0..4f78e0d2a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/app/src/main/kotlin/org/fdroid/ui/Main.kt @@ -8,7 +8,10 @@ import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -64,6 +67,7 @@ fun Main(onListeningForIntent: () -> Unit = {}) { calculatePaneScaffoldDirective(windowAdaptiveInfo).copy(horizontalPartitionSpacerSize = 2.dp) } val isBigScreen = directive.maxHorizontalPartitions > 1 + var showSearchKeyboard by remember { mutableStateOf(false) } val entryProvider: (NavKey) -> NavEntry = entryProvider { discoverEntry(navigator) @@ -84,7 +88,12 @@ fun Main(onListeningForIntent: () -> Unit = {}) { viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value override val categories: List? = viewModel.categories.collectAsStateWithLifecycle(null).value + override val showKeyboard: Boolean = showSearchKeyboard override val actions: SearchActions = viewModel + + override fun onKeyboardShown() { + showSearchKeyboard = false + } }, onNav = { navKey -> navigator.navigate(navKey) }, onBack = { navigator.goBack() }, @@ -139,7 +148,17 @@ fun Main(onListeningForIntent: () -> Unit = {}) { isBigScreen = isBigScreen, showBottomBar = !isBigScreen && navigator.last is MainNavKey, currentNavKey = navigationState.topLevelRoute, - onNav = navigator::navigate, + onNav = { + // don't navigate, if we are already on the selected top level route + if (navigationState.topLevelRoute == it) { + if (navigationState.topLevelRoute == NavigationKey.Search) { + // double tapping the search brings up the keyboard + showSearchKeyboard = true + } + } else { + navigator.navigate(it) + } + }, onBack = navigator::goBack, ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt b/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt index ea5314991..ea35ffdbe 100644 --- a/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt +++ b/app/src/main/kotlin/org/fdroid/ui/navigation/BottomBar.kt @@ -45,7 +45,7 @@ fun BottomBar(model: MainModel, currentNavKey: NavKey, onNav: (MainNavKey) -> Un selectedTextColor = MaterialTheme.colorScheme.primary, selectedIconColor = contentColorFor(MaterialTheme.colorScheme.primary), ), - onClick = { if (dest != currentNavKey) onNav(dest) }, + onClick = { onNav(dest) }, modifier = Modifier.semantics { if (dest == NavigationKey.MyApps) { @@ -83,7 +83,7 @@ fun NavigationRail( selectedTextColor = MaterialTheme.colorScheme.primary, selectedIconColor = contentColorFor(MaterialTheme.colorScheme.primary), ), - onClick = { if (dest != currentNavKey) onNav(dest) }, + onClick = { onNav(dest) }, modifier = Modifier.semantics { if (dest == NavigationKey.MyApps) { diff --git a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt index 44c3d1062..71256c600 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.tooling.preview.Preview @@ -30,13 +31,16 @@ fun GlobalSearch( onNav: (NavigationKey) -> Unit, onBack: () -> Unit, ) { + LaunchedEffect(info.showKeyboard) { + // auto-reset showKeyboard, so it functions like an event + if (info.showKeyboard) info.onKeyboardShown() + } val searchResults = info.searchResults Scaffold( topBar = { TopSearchBar( searchFieldState = textFieldState, - shouldRequestFocus = // only show keyboard if there are no results to show - searchResults == null || searchResults.apps.isEmpty() && searchResults.categories.isEmpty(), + shouldRequestFocus = info.showKeyboard, onSearch = info.actions::onSearch, onSearchCleared = info.actions::onSearchCleared, onHideSearch = onBack, diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt index 152da6ff1..428dc3a23 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt @@ -7,7 +7,9 @@ interface SearchInfo { val searchResults: SearchResults? val savedSearches: List? val categories: List? + val showKeyboard: Boolean val actions: SearchActions + fun onKeyboardShown() } interface SearchActions { diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt index 25de71bbb..1d99d8671 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchResults.kt @@ -23,6 +23,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -59,11 +60,15 @@ fun SearchResults( if (textFieldState.text.isSearchable()) { BigLoadingIndicator(modifier.padding(paddingValues).imePadding()) } else { + val keyboardController = LocalSoftwareKeyboardController.current LazyColumn(modifier = modifier.fillMaxSize(), contentPadding = paddingValues) { if (!savedSearches.isNullOrEmpty()) { pastSearches( savedSearches = savedSearches, - onSearch = { textFieldState.edit { replace(0, length, it) } }, + onSearch = { + textFieldState.edit { replace(0, length, it) } + keyboardController?.hide() + }, onClearSavedSearches = onClearSavedSearches, ) } diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index cd23062fd..9a9db89fc 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -650,6 +650,7 @@ fun getSearchInfo( override val savedSearches: List? = savedSearches override val categories: List? = categories + override val showKeyboard: Boolean = false override val actions: SearchActions = object : SearchActions { @@ -659,4 +660,6 @@ fun getSearchInfo( override fun onClearSearchHistory() {} } + + override fun onKeyboardShown() {} } From 0bd7f0cc6fbea588c400a3bdd667b02b1da10ff7 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 08:55:16 -0300 Subject: [PATCH 25/38] Also search in category descriptions this is currently limited to the current locale, so requires translations to exist for that. --- app/src/main/kotlin/org/fdroid/search/SearchManager.kt | 10 ++++++++-- .../kotlin/org/fdroid/ui/categories/CategoryItem.kt | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt index 123f90873..862bc0bed 100644 --- a/app/src/main/kotlin/org/fdroid/search/SearchManager.kt +++ b/app/src/main/kotlin/org/fdroid/search/SearchManager.kt @@ -58,7 +58,11 @@ constructor( db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> categories .map { category -> - CategoryItem(id = category.id, name = category.getName(localeList) ?: "Unknown Category") + CategoryItem( + id = category.id, + name = category.getName(localeList) ?: "Unknown Category", + description = category.getDescription(localeList), + ) } .sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) } } @@ -113,7 +117,9 @@ constructor( val timedCategories = measureTimedValue { categories.first().filter { // normalization removed diacritics, so searches without them work - it.name.normalize().contains(sanitized.normalize(), ignoreCase = true) + val normalized = sanitized.normalize() + it.name.normalize().contains(normalized, ignoreCase = true) || + (it.description?.normalize()?.contains(normalized, ignoreCase = true) ?: false) } } _searchResults.value = SearchResults(timedApps.value, timedCategories.value) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 2d593ff45..2b4b2bbd3 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -74,7 +74,7 @@ import androidx.compose.material.icons.filled.Wallpaper import androidx.compose.material.icons.filled.WbSunny import androidx.compose.ui.graphics.vector.ImageVector -data class CategoryItem(val id: String, val name: String) { +data class CategoryItem(val id: String, val name: String, val description: String? = null) { val imageVector: ImageVector get() = when (id) { From cde669a3c1402d0ed597aa3fa65b4ac3cc18e752 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 09:08:43 -0300 Subject: [PATCH 26/38] Update categories in discover when locales changes this is so the translations for category names changes dynamically, after the user changes the locale. this isn't done everywhere, because changing locales is a rare action and the most likely place for this to happen is settings. Then the Discover screen is the only one that is on the stack that can still have outdated translations. --- .../fdroid/ui/discover/DiscoverViewModel.kt | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt index 75609d1aa..6c0d6d6a6 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt @@ -2,6 +2,11 @@ package org.fdroid.ui.discover import android.annotation.SuppressLint import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.Intent.ACTION_LOCALE_CHANGED +import android.content.IntentFilter import androidx.core.os.LocaleListCompat import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.SavedStateHandle @@ -16,10 +21,13 @@ import java.util.Locale import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream @@ -49,8 +57,8 @@ constructor( private val log = KotlinLogging.logger {} private val moleculeScope = CoroutineScope(viewModelScope.coroutineContext + AndroidUiDispatcher.Main) - private val collator = Collator.getInstance(Locale.getDefault()) + private val localeListFlow = MutableStateFlow(LocaleListCompat.getDefault()) private val newAppsFlow = db.getAppDao().getNewAppsFlow() private val recentlyUpdatedAppsFlow = db.getAppDao().getRecentlyUpdatedAppsFlow() private val mostDownloadedApps = @@ -68,8 +76,10 @@ constructor( db.getAppDao().getAppsFlow(packageNames).collect { apps -> emit(apps) } } .flowOn(Dispatchers.IO) + private val dbCategories = db.getRepositoryDao().getLiveCategories().asFlow() private val categories = - db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> + combine(localeListFlow, dbCategories) { localeList, categories -> + val collator = Collator.getInstance(Locale.getDefault()) categories .mapNotNull { category -> val item = @@ -86,7 +96,6 @@ constructor( repos?.any { it.enabled && it.errorCount >= 5 } ?: false } - val localeList = LocaleListCompat.getDefault() val discoverModel: StateFlow by lazy(LazyThreadSafetyMode.NONE) { @SuppressLint("StateFlowValueCalledInComposition") // see comment below @@ -108,4 +117,24 @@ constructor( ) } } + + private val localeChangedReceiver = LocaleChangedReceiver() + + init { + app.registerReceiver(localeChangedReceiver, IntentFilter(ACTION_LOCALE_CHANGED)) + } + + override fun onCleared() { + app.unregisterReceiver(localeChangedReceiver) + } + + private inner class LocaleChangedReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == ACTION_LOCALE_CHANGED) { + val localeList = LocaleListCompat.getDefault() + log.info { "Locale list has changed: $localeList" } + localeListFlow.update { localeList } + } + } + } } From 9acddc871d254aec5e294d10ad790c9548e83b88 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 09:20:26 -0300 Subject: [PATCH 27/38] Bring back category groups to discover screen --- .../ui/screenshots/DiscoverScreenshotTest.kt | 7 +- app/src/main/kotlin/org/fdroid/ui/Main.kt | 10 -- .../org/fdroid/ui/categories/Categories.kt | 94 --------------- .../ui/categories/CategoriesViewModel.kt | 28 ----- .../org/fdroid/ui/categories/CategoryGroup.kt | 96 ++++++++++++++++ .../fdroid/ui/categories/CategoryGroupRow.kt | 107 ++++++++++++++++++ .../org/fdroid/ui/categories/CategoryItem.kt | 99 ++++++++++++---- .../org/fdroid/ui/categories/CategoryList.kt | 72 ++++++------ .../org/fdroid/ui/categories/CategoryRow.kt | 40 ------- .../org/fdroid/ui/categories/ChipFlowRow.kt | 4 +- .../org/fdroid/ui/discover/DiscoverContent.kt | 2 +- .../org/fdroid/ui/discover/DiscoverModel.kt | 3 +- .../fdroid/ui/discover/DiscoverPresenter.kt | 2 +- .../fdroid/ui/discover/DiscoverViewModel.kt | 9 +- .../org/fdroid/ui/navigation/NavigationKey.kt | 2 - app/src/main/res/values-ar/strings.xml | 10 ++ app/src/main/res/values-bg/strings.xml | 10 ++ app/src/main/res/values-bn-rBD/strings.xml | 2 + app/src/main/res/values-ca/strings.xml | 10 ++ app/src/main/res/values-cs/strings.xml | 10 ++ app/src/main/res/values-da/strings.xml | 10 ++ app/src/main/res/values-de/strings.xml | 10 ++ app/src/main/res/values-el/strings.xml | 10 ++ app/src/main/res/values-en-rGB/strings.xml | 10 ++ app/src/main/res/values-eo/strings.xml | 10 ++ app/src/main/res/values-es/strings.xml | 10 ++ app/src/main/res/values-et/strings.xml | 10 ++ app/src/main/res/values-eu/strings.xml | 10 ++ app/src/main/res/values-fa/strings.xml | 10 ++ app/src/main/res/values-fi/strings.xml | 9 ++ app/src/main/res/values-fr/strings.xml | 10 ++ app/src/main/res/values-ga/strings.xml | 10 ++ app/src/main/res/values-gl/strings.xml | 3 + app/src/main/res/values-he/strings.xml | 10 ++ app/src/main/res/values-hr/strings.xml | 10 ++ app/src/main/res/values-hu/strings.xml | 10 ++ app/src/main/res/values-id/strings.xml | 10 ++ app/src/main/res/values-is/strings.xml | 10 ++ app/src/main/res/values-it/strings.xml | 10 ++ app/src/main/res/values-ja/strings.xml | 10 ++ app/src/main/res/values-kab/strings.xml | 3 + app/src/main/res/values-ko/strings.xml | 10 ++ app/src/main/res/values-lt/strings.xml | 10 ++ app/src/main/res/values-lv/strings.xml | 10 ++ app/src/main/res/values-mk/strings.xml | 10 ++ app/src/main/res/values-nb/strings.xml | 10 ++ app/src/main/res/values-nl/strings.xml | 10 ++ app/src/main/res/values-pl/strings.xml | 10 ++ app/src/main/res/values-pt-rBR/strings.xml | 10 ++ app/src/main/res/values-pt-rPT/strings.xml | 10 ++ app/src/main/res/values-pt/strings.xml | 10 ++ app/src/main/res/values-ro/strings.xml | 10 ++ app/src/main/res/values-ru/strings.xml | 10 ++ app/src/main/res/values-sk/strings.xml | 10 ++ app/src/main/res/values-sl/strings.xml | 10 ++ app/src/main/res/values-sq/strings.xml | 10 ++ app/src/main/res/values-sr/strings.xml | 10 ++ app/src/main/res/values-sv/strings.xml | 10 ++ app/src/main/res/values-sw/strings.xml | 10 ++ app/src/main/res/values-tr/strings.xml | 10 ++ app/src/main/res/values-uk/strings.xml | 10 ++ app/src/main/res/values-uz/strings.xml | 1 + app/src/main/res/values-vi/strings.xml | 10 ++ app/src/main/res/values-zh-rCN/strings.xml | 10 ++ app/src/main/res/values-zh-rTW/strings.xml | 10 ++ app/src/main/res/values/strings.xml | 24 +++- .../org/fdroid/ui/discover/DiscoverTest.kt | 16 ++- .../DiscoverTest_2ed8e27d_0.png | 4 +- 68 files changed, 829 insertions(+), 258 deletions(-) delete mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt delete mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt create mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt create mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroupRow.kt delete mode 100644 app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt diff --git a/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt b/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt index ee3670d6c..fd7e553ff 100644 --- a/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt +++ b/app/src/androidTest/java/org/fdroid/ui/screenshots/DiscoverScreenshotTest.kt @@ -1,6 +1,5 @@ package org.fdroid.ui.screenshots -import java.text.Collator import org.fdroid.ui.discover.Discover import org.fdroid.ui.discover.LoadedDiscoverModel import org.junit.Test @@ -17,16 +16,12 @@ class DiscoverScreenshotTest(localeName: String) : LocalizedScreenshotTest(local @Test fun appDetails() = screenshotTest("1_Discover") { localeList -> - val collator = Collator.getInstance(localeList.get(0)) val model = LoadedDiscoverModel( newApps = getNewApps(localeList), recentlyUpdatedApps = getRecentlyUpdatedApps(localeList), mostDownloadedApps = getMostDownloadedApps(localeList), - categories = - getCategoryItems(localeList).filter { it.featured }.sortedWith { c1, c2 -> - collator.compare(c1.name, c2.name) - }, + categories = getCategoryItems(localeList).groupBy { it.group }, hasRepoIssues = false, ) Discover(discoverModel = model, onListTap = {}, onAppTap = {}, onNav = {}) diff --git a/app/src/main/kotlin/org/fdroid/ui/Main.kt b/app/src/main/kotlin/org/fdroid/ui/Main.kt index 4f78e0d2a..6e8c7bf33 100644 --- a/app/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/app/src/main/kotlin/org/fdroid/ui/Main.kt @@ -20,8 +20,6 @@ import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.entryProvider import org.fdroid.search.SavedSearch import org.fdroid.ui.apps.myAppsEntry -import org.fdroid.ui.categories.Categories -import org.fdroid.ui.categories.CategoriesViewModel import org.fdroid.ui.categories.CategoryItem import org.fdroid.ui.details.NoAppSelected import org.fdroid.ui.details.appDetailsEntry @@ -99,14 +97,6 @@ fun Main(onListeningForIntent: () -> Unit = {}) { onBack = { navigator.goBack() }, ) } - entry(NavigationKey.Categories) { - val viewModel = hiltViewModel() - Categories( - categories = viewModel.categories.collectAsStateWithLifecycle(null).value, - onNav = { navKey -> navigator.navigate(navKey) }, - onBackClicked = { navigator.goBack() }, - ) - } entry(NavigationKey.Settings) { val viewModel = hiltViewModel() Settings( diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt b/app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt deleted file mode 100644 index 00910b209..000000000 --- a/app/src/main/kotlin/org/fdroid/ui/categories/Categories.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.fdroid.ui.categories - -import androidx.compose.foundation.layout.Arrangement.spacedBy -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults.enterAlwaysScrollBehavior -import androidx.compose.material3.rememberTopAppBarState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.ui.Modifier -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.PreviewLightDark -import androidx.compose.ui.unit.dp -import androidx.navigation3.runtime.NavKey -import org.fdroid.R -import org.fdroid.ui.FDroidContent -import org.fdroid.ui.utils.BackButton -import org.fdroid.ui.utils.BigLoadingIndicator - -@Composable -@OptIn(ExperimentalMaterial3Api::class) -fun Categories( - categories: List?, - onNav: (NavKey) -> Unit, - onBackClicked: () -> Unit, - modifier: Modifier = Modifier, -) { - val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) - Scaffold( - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(R.string.main_menu__categories), - maxLines = 1, - overflow = TextOverflow.MiddleEllipsis, - ) - }, - navigationIcon = { BackButton(onClick = onBackClicked) }, - scrollBehavior = scrollBehavior, - ) - }, - modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - ) { paddingValues -> - val listState = rememberSaveable(saver = LazyListState.Saver) { LazyListState() } - if (categories == null) { - BigLoadingIndicator(modifier.padding(paddingValues)) - } else { - LazyColumn( - state = listState, - contentPadding = paddingValues, - verticalArrangement = spacedBy(8.dp), - ) { - items(items = categories, key = { it.id }, contentType = { "C" }) { categoryItem -> - CategoryRow(categoryItem, onNav) - } - } - } - } -} - -@Composable -@PreviewLightDark -private fun PreviewLoading() { - FDroidContent { - Categories(null, {}, {}) - } -} - -@Composable -@PreviewLightDark -private fun Preview() { - FDroidContent { - val categories = - listOf( - CategoryItem("App Store & Updater", "App Store & Updater"), - CategoryItem("Browser", "Browser"), - CategoryItem("Calendar & Agenda", "Calendar & Agenda"), - CategoryItem("Cloud Storage & File Sync", "Cloud Storage & File Sync"), - CategoryItem("Connectivity", "Connectivity"), - CategoryItem("Development", "Development"), - CategoryItem("doesn't exist", "Foo bar"), - ) - Categories(categories, {}, {}) - } -} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt deleted file mode 100644 index 93d4e0262..000000000 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoriesViewModel.kt +++ /dev/null @@ -1,28 +0,0 @@ -package org.fdroid.ui.categories - -import android.app.Application -import androidx.core.os.LocaleListCompat -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.asFlow -import dagger.hilt.android.lifecycle.HiltViewModel -import java.text.Collator -import java.util.Locale -import javax.inject.Inject -import kotlinx.coroutines.flow.map -import org.fdroid.database.FDroidDatabase - -@HiltViewModel -class CategoriesViewModel @Inject constructor(app: Application, db: FDroidDatabase) : - AndroidViewModel(app) { - private val collator = Collator.getInstance(Locale.getDefault()) - private val localeList = LocaleListCompat.getDefault() - - val categories = - db.getRepositoryDao().getLiveCategories().asFlow().map { categories -> - categories - .map { category -> - CategoryItem(id = category.id, name = category.getName(localeList) ?: "Unknown Category") - } - .sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) } - } -} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt new file mode 100644 index 000000000..dc9959a73 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroup.kt @@ -0,0 +1,96 @@ +package org.fdroid.ui.categories + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Category +import androidx.compose.material.icons.filled.Construction +import androidx.compose.material.icons.filled.Factory +import androidx.compose.material.icons.filled.FavoriteBorder +import androidx.compose.material.icons.filled.Forum +import androidx.compose.material.icons.filled.NetworkCheck +import androidx.compose.material.icons.filled.PhoneAndroid +import androidx.compose.material.icons.filled.SdStorage +import androidx.compose.material.icons.filled.VideogameAsset +import androidx.compose.material.icons.filled.Wallet +import androidx.compose.ui.graphics.vector.ImageVector +import org.fdroid.R + +data class CategoryGroup( + val id: String, + @get:StringRes val name: Int, + @get:StringRes val summary: Int, + val imageVector: ImageVector, +) + +object CategoryGroups { + val productivity = + CategoryGroup( + id = "productivity", + name = R.string.category_group_productivity, + summary = R.string.category_group_summary_productivity, + imageVector = Icons.Default.Factory, + ) + val tools = + CategoryGroup( + id = "tools", + name = R.string.category_group_tools, + summary = R.string.category_group_summary_tools, + imageVector = Icons.Default.Construction, + ) + val wallets = + CategoryGroup( + id = "wallets", + name = R.string.category_group_wallets, + summary = R.string.category_group_summary_wallets, + imageVector = Icons.Default.Wallet, + ) + val media = + CategoryGroup( + id = "media", + name = R.string.category_group_media, + summary = R.string.category_group_summary_media, + imageVector = Icons.Default.VideogameAsset, + ) + val communication = + CategoryGroup( + id = "communication", + name = R.string.category_group_communication, + summary = R.string.category_group_summary_communication, + imageVector = Icons.Default.Forum, + ) + val device = + CategoryGroup( + id = "device", + name = R.string.category_group_device, + summary = R.string.category_group_summary_device, + imageVector = Icons.Default.PhoneAndroid, + ) + val network = + CategoryGroup( + id = "network", + name = R.string.category_group_network, + summary = R.string.category_group_summary_network, + imageVector = Icons.Default.NetworkCheck, + ) + val storage = + CategoryGroup( + id = "storage", + name = R.string.category_group_storage, + summary = R.string.category_group_summary_storage, + imageVector = Icons.Default.SdStorage, + ) + val interests = + CategoryGroup( + id = "interests", + name = R.string.category_group_interests, + summary = R.string.category_group_summary_interests, + imageVector = Icons.Default.FavoriteBorder, + ) + val misc = + CategoryGroup( + id = "misc", + name = R.string.category_group_misc, + summary = R.string.category_group_summary_misc, + imageVector = Icons.Default.Category, + ) +} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroupRow.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroupRow.kt new file mode 100644 index 000000000..58e3ff5f7 --- /dev/null +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryGroupRow.kt @@ -0,0 +1,107 @@ +package org.fdroid.ui.categories + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment.Companion.Center +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.hideFromAccessibility +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.PreviewLightDark +import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.NavKey +import org.fdroid.ui.FDroidContent +import org.fdroid.ui.lists.AppListType +import org.fdroid.ui.navigation.NavigationKey +import org.fdroid.ui.utils.ExpandIconArrow + +@Composable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +fun CategoryGroupRow( + group: CategoryGroup, + categories: List, + onNav: (NavKey) -> Unit, +) { + val isPreview = LocalInspectionMode.current + var expanded by rememberSaveable { mutableStateOf(isPreview) } + val onClick = { expanded = !expanded } + ListItem( + leadingContent = { + Box( + contentAlignment = Center, + modifier = + Modifier.size(40.dp) + .clip(MaterialTheme.shapes.small) + .background(MaterialTheme.colorScheme.primaryContainer), + ) { + Icon( + imageVector = group.imageVector, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.semantics { hideFromAccessibility() }, + ) + } + }, + supportingContent = { + Text(text = stringResource(group.summary), maxLines = 2, overflow = TextOverflow.Ellipsis) + }, + trailingContent = { IconButton(onClick = onClick) { ExpandIconArrow(expanded) } }, + onClick = onClick, + ) { + Text(text = stringResource(group.name), maxLines = 2, overflow = TextOverflow.Ellipsis) + } + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically(), + ) { + ChipFlowRow(modifier = Modifier.padding(8.dp)) { + categories.forEach { categoryItem -> + CategoryChip( + categoryItem = categoryItem, + onClick = { + val type = AppListType.Category(categoryItem.name, categoryItem.id) + val navKey = NavigationKey.AppList(type) + onNav(navKey) + }, + ) + } + } + } +} + +@Composable +@PreviewLightDark +private fun Preview() { + FDroidContent { + val categories = + listOf( + CategoryItem("App Store & Updater", "App Store & Updater"), + CategoryItem("Browser", "Browser"), + CategoryItem("Calendar & Agenda", "Calendar & Agenda"), + ) + Column { CategoryGroupRow(CategoryGroups.productivity, categories) {} } + } +} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 2b4b2bbd3..83fe8fe19 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -13,6 +13,7 @@ import androidx.compose.material.icons.filled.AppBlocking import androidx.compose.material.icons.filled.Apps import androidx.compose.material.icons.filled.BatteryChargingFull import androidx.compose.material.icons.filled.Bookmarks +import androidx.compose.material.icons.filled.BrowserUpdated import androidx.compose.material.icons.filled.Brush import androidx.compose.material.icons.filled.Calculate import androidx.compose.material.icons.filled.CalendarMonth @@ -153,31 +154,81 @@ data class CategoryItem(val id: String, val name: String, val description: Strin else -> Icons.Default.Category } - val featured: Boolean + val group: CategoryGroup get() = when (id) { - "Browser" -> true - "Calendar & Agenda" -> true - "Cloud Storage & File Sync" -> true - "Email" -> true - "File Encryption & Vault" -> true - "File Transfer" -> true - "Firewall" -> true - "Games" -> true - "Local Media Player" -> true - "Location Tracker & Sharer" -> true - "Messaging" -> true - "Navigation" -> true - "News" -> true - "Password & 2FA" -> true - "Podcast" -> true - "Public Transport" -> true - "Radio" -> true - "Social Network" -> true - "Translation & Dictionary" -> true - "Voice & Video Chat" -> true - "VPN & Proxy" -> true - "Weather" -> true - else -> false + "AI Chat" -> CategoryGroups.tools + "App Manager" -> CategoryGroups.device + "App Store & Updater" -> CategoryGroups.device + "Battery" -> CategoryGroups.device + "Bookmark" -> CategoryGroups.storage + "Browser" -> CategoryGroups.network + "Calculator" -> CategoryGroups.tools + "Calendar & Agenda" -> CategoryGroups.productivity + "Clock" -> CategoryGroups.productivity + "Cloud Storage & File Sync" -> CategoryGroups.storage + "Connectivity" -> CategoryGroups.network + "Contact" -> CategoryGroups.communication + "Development" -> CategoryGroups.interests + "DNS & Hosts" -> CategoryGroups.network + "Draw" -> CategoryGroups.interests + "Ebook Reader" -> CategoryGroups.media + "Email" -> CategoryGroups.communication + "File Encryption & Vault" -> CategoryGroups.storage + "File Transfer" -> CategoryGroups.storage + "Finance Manager" -> CategoryGroups.wallets + "Firewall" -> CategoryGroups.network + "Flashlight" -> CategoryGroups.tools + "Forum" -> CategoryGroups.communication + "Gallery" -> CategoryGroups.storage + "Games" -> CategoryGroups.media + "Graphics" -> CategoryGroups.interests + "Habit Tracker" -> CategoryGroups.productivity + "Icon Pack" -> CategoryGroups.device + "Internet" -> CategoryGroups.network + "Inventory" -> CategoryGroups.tools + "Keyboard & IME" -> CategoryGroups.device + "Launcher" -> CategoryGroups.device + "Local Media Player" -> CategoryGroups.media + "Location Tracker & Sharer" -> CategoryGroups.tools + "Messaging" -> CategoryGroups.communication + "Money" -> CategoryGroups.wallets + "Multimedia" -> CategoryGroups.media + "Music Practice Tool" -> CategoryGroups.interests + "Navigation" -> CategoryGroups.tools + "Network Analyzer" -> CategoryGroups.network + "News" -> CategoryGroups.interests + "Note" -> CategoryGroups.storage + "Online Media Player" -> CategoryGroups.media + "Pass Wallet" -> CategoryGroups.wallets + "Password & 2FA" -> CategoryGroups.device + "Phone & SMS" -> CategoryGroups.communication + "Podcast" -> CategoryGroups.media + "Public Transport" -> CategoryGroups.tools + "Radio" -> CategoryGroups.media + "Reading" -> CategoryGroups.media + "Recipe Manager" -> CategoryGroups.interests + "Religion" -> CategoryGroups.interests + "Remote Controller" -> CategoryGroups.tools + "Science & Education" -> CategoryGroups.interests + "Security" -> CategoryGroups.device + "Shopping List" -> CategoryGroups.tools + "Social Network" -> CategoryGroups.communication + "Sports & Health" -> CategoryGroups.interests + "System" -> CategoryGroups.device + "Task" -> CategoryGroups.productivity + "Text Editor" -> CategoryGroups.productivity + "Theming" -> CategoryGroups.device + "Time" -> CategoryGroups.productivity + "Translation & Dictionary" -> CategoryGroups.tools + "Voice & Video Chat" -> CategoryGroups.communication + "Unit Convertor" -> CategoryGroups.tools + "VPN & Proxy" -> CategoryGroups.network + "Wallet" -> CategoryGroups.wallets + "Wallpaper" -> CategoryGroups.device + "Weather" -> CategoryGroups.tools + "Workout" -> CategoryGroups.interests + "Writing" -> CategoryGroups.productivity + else -> CategoryGroups.misc } } diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt index 7ce609cce..5985c4d18 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryList.kt @@ -1,19 +1,18 @@ package org.fdroid.ui.categories import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment.Companion.CenterVertically +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp @@ -21,34 +20,37 @@ import androidx.compose.ui.unit.sp import androidx.navigation3.runtime.NavKey import org.fdroid.R import org.fdroid.ui.FDroidContent -import org.fdroid.ui.navigation.NavigationKey @Composable @OptIn(ExperimentalMaterial3ExpressiveApi::class) fun CategoryList( - categoryList: List?, + categoryMap: Map>?, onNav: (NavKey) -> Unit, modifier: Modifier = Modifier, ) { - val onAllCategories = { onNav(NavigationKey.Categories) } - AnimatedVisibility(!categoryList.isNullOrEmpty()) { + AnimatedVisibility(!categoryMap.isNullOrEmpty()) { Column(modifier = modifier) { - Row( - verticalAlignment = CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = - Modifier.fillMaxWidth().clickable(onClick = onAllCategories).padding(horizontal = 16.dp), - ) { - Text( - text = stringResource(R.string.categories_features), - style = MaterialTheme.typography.titleMediumEmphasized, - fontSize = 20.sp, - ) - TextButton(onClick = onAllCategories) { Text(stringResource(R.string.see_all)) } - } - if (categoryList != null) { + Text( + text = stringResource(R.string.main_menu__categories), + style = MaterialTheme.typography.titleMediumEmphasized, + fontSize = 20.sp, + modifier = Modifier.padding(horizontal = 16.dp), + ) + // we'll sort the groups here, because before we didn't have the context to get names + val res = LocalResources.current + val sortedMap = + remember(categoryMap) { + val comparator = compareBy { res.getString(it.name) } + categoryMap?.toSortedMap(comparator) + } + OutlinedCard(modifier = Modifier.padding(16.dp)) { // FIXME: A LazyColumn would be better, but we can't use this inside a scrollable column - Column { categoryList.forEach { categoryItem -> CategoryRow(categoryItem, onNav) } } + Column { + sortedMap?.forEach { (group, categories) -> + CategoryGroupRow(group, categories, onNav) + HorizontalDivider(modifier = Modifier.fillMaxWidth()) + } + } } } } @@ -59,14 +61,20 @@ fun CategoryList( fun CategoryListPreview() { FDroidContent { val categories = - listOf( - CategoryItem("App Store & Updater", "App Store & Updater"), - CategoryItem("Browser", "Browser"), - CategoryItem("Calendar & Agenda", "Calendar & Agenda"), - CategoryItem("Cloud Storage & File Sync", "Cloud Storage & File Sync"), - CategoryItem("Connectivity", "Connectivity"), - CategoryItem("Development", "Development"), - CategoryItem("doesn't exist", "Foo bar"), + mapOf( + CategoryGroups.productivity to + listOf( + CategoryItem("App Store & Updater", "App Store & Updater"), + CategoryItem("Browser", "Browser"), + CategoryItem("Calendar & Agenda", "Calendar & Agenda"), + ), + CategoryGroups.media to + listOf( + CategoryItem("Cloud Storage & File Sync", "Cloud Storage & File Sync"), + CategoryItem("Connectivity", "Connectivity"), + CategoryItem("Development", "Development"), + CategoryItem("doesn't exist", "Foo bar"), + ), ) CategoryList(categories, {}) } diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt deleted file mode 100644 index f9d0dbe8d..000000000 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryRow.kt +++ /dev/null @@ -1,40 +0,0 @@ -package org.fdroid.ui.categories - -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.hideFromAccessibility -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.navigation3.runtime.NavKey -import org.fdroid.ui.lists.AppListType -import org.fdroid.ui.navigation.NavigationKey - -@Composable -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -fun CategoryRow(categoryItem: CategoryItem, onNav: (NavKey) -> Unit) { - ListItem( - leadingContent = { - Icon( - imageVector = categoryItem.imageVector, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.semantics { hideFromAccessibility() }, - ) - }, - onClick = { - val type = AppListType.Category(categoryItem.name, categoryItem.id) - val navKey = NavigationKey.AppList(type) - onNav(navKey) - }, - modifier = Modifier.padding(horizontal = 16.dp), - ) { - Text(text = categoryItem.name, maxLines = 2, overflow = TextOverflow.Ellipsis) - } -} diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt b/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt index 9c7d4f280..7fc4beab9 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/ChipFlowRow.kt @@ -37,7 +37,7 @@ fun ChipFlowRowFewItemsPreview() { CategoryItem("doesn't exist", "Oops"), ) - FDroidContent { ChipFlowRow { categories.map { category -> CategoryChip(category, {}) } } } + FDroidContent { ChipFlowRow { categories.forEach { category -> CategoryChip(category, {}) } } } } @Preview @@ -61,5 +61,5 @@ fun ChipFlowRowManyItemsPreview() { CategoryItem("Science & Education", "Science & Education"), ) - FDroidContent { ChipFlowRow { categories.map { category -> CategoryChip(category, {}) } } } + FDroidContent { ChipFlowRow { categories.forEach { category -> CategoryChip(category, {}) } } } } diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt index 5e6a3b462..b248393ca 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverContent.kt @@ -55,7 +55,7 @@ fun DiscoverContent( ) } CategoryList( - categoryList = discoverModel.categories, + categoryMap = discoverModel.categories, onNav = onNav, modifier = Modifier.fillMaxWidth(), ) diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt index 9af011d42..141ef8b15 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverModel.kt @@ -2,6 +2,7 @@ package org.fdroid.ui.discover import org.fdroid.download.NetworkState import org.fdroid.repo.RepoUpdateState +import org.fdroid.ui.categories.CategoryGroup import org.fdroid.ui.categories.CategoryItem sealed class DiscoverModel @@ -19,6 +20,6 @@ data class LoadedDiscoverModel( val newApps: List, val recentlyUpdatedApps: List, val mostDownloadedApps: List?, - val categories: List?, + val categories: Map>?, val hasRepoIssues: Boolean, ) : DiscoverModel() diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt index 21bdbb840..68a1c0f63 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverPresenter.kt @@ -88,7 +88,7 @@ fun DiscoverPresenter( newApps = newApps ?: emptyList(), recentlyUpdatedApps = recentlyUpdatedApps ?: emptyList(), mostDownloadedApps = mostDownloadedApps, - categories = categories, + categories = categories?.groupBy { it.group }, hasRepoIssues = hasRepoIssuesFlow.collectAsState(false).value, ) } else { diff --git a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt index 6c0d6d6a6..76fc974e4 100644 --- a/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/discover/DiscoverViewModel.kt @@ -81,13 +81,8 @@ constructor( combine(localeListFlow, dbCategories) { localeList, categories -> val collator = Collator.getInstance(Locale.getDefault()) categories - .mapNotNull { category -> - val item = - CategoryItem( - id = category.id, - name = category.getName(localeList) ?: "Unknown Category", - ) - if (item.featured) item else null + .map { category -> + CategoryItem(id = category.id, name = category.getName(localeList) ?: "Unknown Category") } .sortedWith { c1, c2 -> collator.compare(c1.name, c2.name) } } diff --git a/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt b/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt index 2d0b11491..4322902a2 100644 --- a/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt +++ b/app/src/main/kotlin/org/fdroid/ui/navigation/NavigationKey.kt @@ -39,8 +39,6 @@ sealed interface NavigationKey : NavKey { @Serializable data class AppList(val type: AppListType) : NavigationKey - @Serializable data object Categories : NavigationKey - @Serializable data object Repos : NavigationKey @Serializable data class RepoDetails(val repoId: Long) : NavigationKey diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 24a6bcf90..e3b6c2e80 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -686,6 +686,16 @@ تطبيقات بواسطة %s ابحث… لم يُعثر على أي تطبيقات\n\nحاول استخدام كلمات بحث أقل أو أضف المزيد من المستودعات + إنتاجية + أدوات + المالية والمحافظ + الترفيه والإعلام + اتصال + الجهاز + الشبكة + الملفات والتخزين + اهتمامات + متنوّع بواسطة %1$s آخر تحديث: %1$s آخر تحديث: %1$s (%2$s) diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml index 8dca95640..8fb0852fc 100644 --- a/app/src/main/res/values-bg/strings.xml +++ b/app/src/main/res/values-bg/strings.xml @@ -628,6 +628,16 @@ Приложения от %s Търсене… Не са намерени приложения\n\nСъкратете търсенето или добавете повече хранилища + Продуктивност + Инструменти + Финанси и портфейли + Забавление и медия + Разговори + Устройство + Мрежа + Файлове и хранилища + Интереси + Разни От %1$s Последно обновяване: %1$s Последно обновяване: %1$s (%2$s) diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml index a500e2ecf..48d62c959 100644 --- a/app/src/main/res/values-bn-rBD/strings.xml +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -510,6 +510,8 @@ সমস্ত অ্যাপস সর্বাধিক ডাউনলোডকৃত অনুসন্ধান করুন + সরঞ্জামসমূহ + পছন্দসমূহ F-Droid সাধারণ F-Nightly F-Nightly Basic diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index c865c65b4..2b51c25ff 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -639,6 +639,16 @@ Aplicacions per %s Cerca… No s\'han trobat aplicacions\n\nProva d\'emprar menys termes de cerca o d\'afegir més repositoris + Productivitat + Eines + Finances i carteres + Entreteniment i mitjans + Comunicació + Dispositiu + Xarxa + Arxius i emmagatzematge + Interessos + Miscel·lània Per %1$s Últimes actualitzacions: %1$s Últimes actualitzacions: %1$s (%2$s) diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index d3f866a41..9195620ec 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -642,6 +642,16 @@ Aplikace od vývojáře %s Hledat… Nenalezeny žádné aplikace\n\nZkuste použít méně slov nebo přidat další repozitáře + Produktivita + Nástroje + Finance a peněženky + Zábava a média + Komunikace + Zařízení + Síť + Soubory a úložiště + Zájmy + Různé Od %1$s Naposledy aktualizováno: %1$s Naposledy aktualizováno: %1$s (%2$s) diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 274c75124..48025cf24 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -622,23 +622,33 @@ Alle apps Nye apps Eksportér appliste + Værktøjer Af %1$s + Kommunikation + Netværk Donér Installeret version: Teknisk information + Enhed Denne app har anti-funktioner Forbereder installation… Pakkenavn: + Produktivitet Sidst opdateret: %1$s + Underholdning & Medier Nyligt opdateret Mest hentede + Interesser Sidst opdateret: %1$s (%2$s) Nyheder + Filer & Lagring + Diverse F-Droid er brudt sammen F-Droid er truet. Google ændrer hvordan du kan installere apps på din enhed. Vi har brug for din hjælp. https://keepandroidopen.org Hent apps ved brug af IPFS-webendpoints Ingen apps fundet\n\nPrøv færre søgeord eller tilføj flere repositories + Økonomi og tegnebøger Udviklerkontakt Henter app-repositories… diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index e8b147775..38427ffee 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -638,6 +638,16 @@ Apps von %s Suchen … Keine Apps gefunden\n\nVersuche es mit weniger Suchbegriffen oder füge weitere Repositorys hinzu + Produktivität + Werkzeuge + Finanzen und Wallets + Unterhaltung und Medien + Kommunikation + Gerät + Netzwerk + Dateien und Speicher + Interessen + Verschiedenes Von %1$s Zuletzt aktualisiert: %1$s Zuletzt aktualisiert: %1$s (%2$s) diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml index ca841166a..612673fc7 100644 --- a/app/src/main/res/values-el/strings.xml +++ b/app/src/main/res/values-el/strings.xml @@ -627,6 +627,16 @@ Εφαρμογές από %s Αναζήτησε… Δεν βρέθηκαν εφαρμογές\n\nΔοκιμάστε να χρησιμοποιήσετε λιγότερους όρους αναζήτησης ή προσθέστε περισσότερα αποθετήρια + Παραγωγικότητα + Εργαλεία + Οικονομικά & Πορτοφόλια + Ψυχαγωγία & Μέσα + Επικοινωνία + Συσκευή + Δίκτυο + Αρχεία & Αποθηκευτικός Χώρος + Ενδιαφέροντα + Διάφορα Από %1$s Τελευταία ενημέρωση: %1$s Τελευταία ενημέρωση: %1$s (%2$s) diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 8096b928d..d0c622c40 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -647,6 +647,16 @@ Apps by %s Search… No apps found\n\nTry to use less search terms or add more repositories + Productivity + Tools + Finances & Wallets + Entertainment & Media + Communication + Device + Network + Files & Storage + Interests + Miscellaneous By %1$s Last updated: %1$s Last updated: %1$s (%2$s) diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index f0c33e10e..a633fe869 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -628,6 +628,16 @@ Aplikaĵoj fare de %s Serĉi… Trovis neniun aplikaĵon\n\nProvu uzi malpli da serĉvortoj aŭ aldoni pliajn deponejojn + Oficejo + Utilaĵoj + Financoj + Distro kaj aŭdvidaĵoj + Komunikado + Aparato + Reto + Dosieroj kaj konservado + Interesiĝoj + Diversaĵoj Fare de %1$s Lasta ĝisdatigo: %1$s Lasta ĝisdatigo: %1$s (%2$s) diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 9cf153289..04144e5ed 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -665,6 +665,16 @@ Aplicaciones por %s Buscar… No se encontraron apps\n\nIntenta usar menos términos de búsqueda o agrega más repositorios + Productividad + Herramientas + Financias y Carteras + Entretenimiento y Medios + Comunicación + Dispositivo + Red + Archivos y Almacenamiento + Intereses + Misceláneo Por %1$s Últimamente actualizado: %1$s Últimamente actualizado: %1$s (%2$s) diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index 79bdb80d0..6ddce00b9 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -644,6 +644,16 @@ Rakendused arendajalt %s Otsi… Rakendusi ei leidu\n\nProovi kasutada laiemaid otsingutingimusi või lisa veel rakenduste hoidlaid + Tarbetarkvara + Tarvikud + Rahandus ja rahataskud + Meelelahutus ja meedia + Suhtlus + Nutiseade ja süsteem + Võrgundus + Failid ja andmeruum + Huvid ja hobid + Varia Arendajalt %1$s Viimati uuendatud: %1$s Viimati uuendatud: %1$s (%2$s) diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index ba49a9e1c..630addd63 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -653,6 +653,16 @@ %s araberako aplikazioak Bilatu… Ez da aplikaziorik aurkitu\n\nSaiatu bilaketa termino gutxiagorekin edo biltegi gehiagorekin + Produktibitatea + Tresnak + Finantza eta diru-zorroak + Entretenimendu eta multimedia + Komunikazioa + Gailua + Sarea + Fitxategiak eta biltegiratzea + Interesak + Denetarik %1$s-ek egina Azken eguneraketa: %1$s Azken eguneraketa: %1$s (%2$s) diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml index 292fd2d8a..f4aa4cbd5 100644 --- a/app/src/main/res/values-fa/strings.xml +++ b/app/src/main/res/values-fa/strings.xml @@ -630,6 +630,15 @@ کاره‌های %s جست‌وجو… هیچ کارهٔ مطابقی وجود ندارد\n\nتلاش برای استفاده از عبارت‌های جست‌وجوی کم‌تر یا افزودن مخزن‌های بیش‌تر + بهره‌وری + ابزارها + مالی و کیف‌پول + سرگرمی و رسانه + ارتباطات + افزاره + شبکه + پرونده‌ها و ذخیره‌سازی + متفرّقه به دست %1$s آخرین به‌روز رسانی: %1$s آخرین به‌روز رسانی: %1$s‏ (%2$s) @@ -641,6 +650,7 @@ اطّلاعات فنی نام بسته: نگارش نصب شده: + سهام پالایش گزیده دسته diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 91bc02bd5..3234f25bb 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -617,6 +617,13 @@ Kaikki sovellukset Tekjiän %s sovellukset Etsi… + Raha ja lompakot + Viihde ja media + Viestintä + Laite + Verkko + Tiedostot ja tallennus + Sekalaiset Tekijältä %1$s Viimeksi päivitetty: %1$s Viimeksi päivitetty: %1$s (%2$s) @@ -636,6 +643,8 @@ Sovellukset, joissa on ongelmia Piilota tapaus Ongelma sovelluksen \"%1$s\" versiossa ohitetaan + Tuottavuus + Työkalut F-Öinen F-Öinen Perus Löydä diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index ecb8649a2..c935b9966 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -655,6 +655,16 @@ Applications de %s Rechercher… Aucune application trouvée\n\nEssayez d\'utiliser moins de termes de recherche ou d\'ajouter d\'autres dépôts + Productivité + Outils + Finances & Portefeuilles + Divertissement & Médias + Communication + Appareil + Réseau + Fichiers & Stockage + Intérêts + Divers De %1$s Dernière mise à jour : %1$s Dernière mise à jour : %1$s (%2$s) diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml index a2c8671e1..2922ad326 100644 --- a/app/src/main/res/values-ga/strings.xml +++ b/app/src/main/res/values-ga/strings.xml @@ -685,6 +685,16 @@ Aipeanna ó %s Cuardaigh… Níor aimsíodh aon aipeanna\n\nBain triail as níos lú téarmaí cuardaigh nó cuir níos mó stórtha leis + Táirgiúlacht + Uirlisí + Airgeadas & Sparán + Siamsaíocht & Meáin + Cumarsáid + Gléas + Líonra + Comhaid & Stóráil + Leasanna + Ilghnéitheach Faoi %1$s Nuashonraithe go deireanach: %1$s Nuashonraithe go deireanach: %1$s (%2$s) diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 8db61c66c..07f7f59ff 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -591,4 +591,7 @@ Aplicacións por %s Buscar… Non se atoparon aplicacións\n\nIntente usar menos termos de busca ou engadir máis repositorios + Produtividade + Ferramentas + Finanzas e carteiras diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index f002672e0..35816a64c 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -673,6 +673,16 @@ יישמונים על ידי %s חפש… לא נמצאו יישומונים\n\nנסו להשתמש בפחות מילות חיפוש או להוסיף עוד מאגרים + הפקה + כלים + כלכלה וארנקים + בידור ומדיה + תקשורת + מכשיר + רשת + קבצים ואחסון + תחומי עניין + שונות על ידי %1$s עדכון אחרון: %1$s עדכון אחרון: %1$s (%2$s) diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index 2b9e7e0c6..907c8d2d5 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -647,6 +647,16 @@ Aplikacije od %s Pretraga … Nije pronađena nijedna aplikacija\n\nPokušaj koristiti manje pojmova za pretraživanje ili dodaj više repozitorija + Produktivnost + Alati + Financije i novčanici + Zabava i mediji + Komunikacija + Uređaj + Mreža + Datoteke i spremišta + Interesi + Razno Autor: %1$s Zadnje aktualiziranje: %1$s Zadnje aktualiziranje: %1$s (%2$s) diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index 8e7fd42e7..7ab8d8498 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -630,6 +630,16 @@ Alkalmazások %s szerint Keres… Nincs találat\n\nPróbálj meg kevesebb keresési kifejezést használni, vagy adjon hozzá több alkalmazás-tárolót + Termelékenység + Eszközök + Pénzügyek és pénztárcák + Szórakozás és média + Kommunikáció + Eszköz + Hálózat + Fájlok és tárolók + Érdeklődési kör + Egyéb %1$s -től Utolsó frissítés: %1$s Utolsó frissítés: %1$s (%2$s) diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 017a25d79..45b35d7ee 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -633,6 +633,16 @@ Aplikasi oleh %s Cari… Tidak ada aplikasi ditemukan\n\nCoba gunakan lebih sedikit kata kunci atau tambahkan lebih banyak repositori + Produktifitas + Alat + Keuangan & Dompet + Hiburan & Media + Komunikasi + Perangkat + Jaringan + Fail & Penyimpanan + Minat + Lain-Lain Oleh %1$s Terakhir diperbarui: %1$s Terakhir diperbarui: %1$s (%2$s) diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml index 3561af7a5..37eaab009 100644 --- a/app/src/main/res/values-is/strings.xml +++ b/app/src/main/res/values-is/strings.xml @@ -629,6 +629,16 @@ Forrit frá %s Leita… Engin forrit fundust\n\nPrófaðu með færri leitarorð eða bættu við gagnasöfnum + Framleiðni + Verkfæri + Fjármál og veski + Afþreying og miðlun + Samskipti + Tæki + Netkerfi + Skrár og geymslurými + Áhugamál + Ýmislegt Frá %1$s Síðast uppfært: %1$s Síðast uppfært: %1$s (%2$s) diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 66f915dff..97c1f51ec 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -663,6 +663,16 @@ App per %s Cerca… Nessuna app trovata\n\nProva a usare meno termini di ricerca o aggiungere più repository + Produttività + Strumenti + Finanze e Portafogli + Intrattenimento & Media + Comunicazione + Dispositivo + Rete + Archiviazione + Interessi + Miscellanea Da %1$s Ultimo aggiornamento: %1$s Ultimo aggiornamento: %1$s (%2$s) diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 00b767bdf..3966fd893 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -634,6 +634,16 @@ %s によるアプリ 検索… アプリが見つかりませんでした\n\nリポジトリを追加するか検索語を減らしてみて下さい + 生産性 + ツール + 財布&財務 + エンタメ&メディア + 交流 + デバイス + 通信 + ファイル&ストレージ + 趣味 + その他 %1$s による 最終更新: %1$s 最終更新: %1$s (%2$s) diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml index d04c43872..2123d1285 100644 --- a/app/src/main/res/values-kab/strings.xml +++ b/app/src/main/res/values-kab/strings.xml @@ -395,10 +395,13 @@ Seggelmes Nadi… https://keepandroidopen.org + Aẓeṭṭa %d n wesnas %d n yesnasen + Ifecka + Ibenk Mudd tawsa Taggayt Aẓeṭṭa diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 8550dbd92..ad887e082 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -625,6 +625,16 @@ %s의 앱 검색… 앱을 찾을 수 없음\n\n검색어를 줄이거나 다른 저장소를 추가하세요 + 생산성 + 도구 + 금융 & 지갑 + 엔터테인먼트 & 미디어 + 통신 + 기기 + 네트워크 + 파일 & 저장소 + 관심분야 + 기타 개발자: %1$s 마지막 업데이트: %1$s 마지막 업데이트: %1$s (%2$s) diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index 588978db1..cae87bfcb 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -601,6 +601,16 @@ %s sukurtos programėlės Ieškoti… Nerastos programėlės\n\nPamėginkite naudoti mažiau paieškos žodžių arba įtraukite daugiau saugyklų + Produktyvumas + Įrankiai + Finansai ir piniginės + Pramogos ir multimedija + Komunikacija + Įrenginys + Tinklas + Failai ir saugykla + Pomėgiai + Kiti Sukurta %1$s Paskutinį kartą atnaujinta: %1$s Paskutinį kartą atnaujinta: %1$s (%2$s) diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml index 9eabffbeb..48f96dbf6 100644 --- a/app/src/main/res/values-lv/strings.xml +++ b/app/src/main/res/values-lv/strings.xml @@ -639,6 +639,15 @@ Lietotnes no %s Meklēt… Neviena lietotne netika atrasta\n\nJāmēģina izmantot vienkāršāks meklēšanas vaicājums vai jāpievieno vairāk glabātavu + Ražīgums + Rīki + Nauda un maki + Izklaide un mediji + Saziņa + Ierīce + Tīkls + Datnes un krātuve + Dažādi No %1$s Pēdējoreiz atjaunināta: %1$s Pēdējoreiz atjaunināta: %1$s (%2$s) @@ -653,6 +662,7 @@ Lietotnes ar nepilnībām Paslēpt nepilnību Nepilnība šajā “%1$s” versijā netiks ņemta vērā + Intereses Lejupielādēt lietotnes no IPFS tīmekļa galamērķiem Lai uzsāktu, jāvienādo lietotņu glabātavas diff --git a/app/src/main/res/values-mk/strings.xml b/app/src/main/res/values-mk/strings.xml index 8963f18f8..e03e44546 100644 --- a/app/src/main/res/values-mk/strings.xml +++ b/app/src/main/res/values-mk/strings.xml @@ -296,6 +296,16 @@ Апликации од%s Пребарувај… Не се пронајдени апликации\n\nПробај да употребиш помали термини или додај нови репо + Продуктивност + Алатки + Финансии и новчаник + Забава и медија + Комуникација + Уред + Мрежа + Датотеки и меморија + Интереси + Останато Направено од %1$s Последно надоградено: %1$s Последно надоградено: %1$s­%2$s diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 0f9e78794..5b46c02da 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -644,6 +644,16 @@ Apper fra %s Søk… Ingen apper funnet\n\nPrøv å bruke færre søkeord eller legg til flere pakkebrønner + Produktivitet + Verktøy + Finans & Lommebøker + Underholdning & Medier + Kommunikasjon + Enhet + Nettverk + Filer & Lagring + Interesser + Diverse Fra %1$s Sist oppdatert: %1$s Sist oppdatert: %1$s (%2$s) diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 0992923ec..c5d22d18d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -636,6 +636,16 @@ Zoeken… App gegevensbanken binnenhalen… Geen apps gevonden\n\nProbeer minder zoektermen of voeg meer gegevensbanken toe + Productiviteit + Hulpmiddelen + Financiën & Wallets + Entertainment & Media + Communicatie + Apparaat + Netwerk + Bestanden & Opslag + Interesses + Diversen Door %1$s Laatst geüpdatet: %1$s Laatst geüpdatet: %1$s (%2$s) diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 96a0156c4..22d5134d1 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -673,6 +673,16 @@ Aplikacje od %s Szukaj… Nie znaleziono aplikacji\n\nSpróbuj użyć mniejszej liczby terminów wyszukiwania lub dodaj więcej repozytoriów + Produktywność + Narzędzia + Finanse i portfele + Rozrywka i multimedia + Komunikacja + Urządzenie + Sieć + Pliki i pamięć + Zainteresowania + Różne Przez %1$s Ostatnia aktualizacja: %1$s Ostatnia aktualizacja: %1$s (%2$s) diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 120edb959..e5211b485 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -645,6 +645,14 @@ Apps por %s Buscar… Nenhum app encontrado\n\nTente usar menos termos na busca ou adicione mais repositórios + Produtividade + Ferramentas + Finanças & Carteiras + Entretenimento & Mídia + Comunicações + Dispositivo + Arquivos & Armazenamento + Interesses Por %1$s Última atualização: %1$s (%2$s) Preparando instalação… @@ -655,6 +663,8 @@ Informações técnicas Nome do pacote: Versão instalada: + Rede + Diversos Última atualização: %1$s Baixe aplicativos usando endpoints web do IPFS Para começar, sincronize os repositórios de apps diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml index 44147e720..ccf14c408 100644 --- a/app/src/main/res/values-pt-rPT/strings.xml +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -640,6 +640,16 @@ Apps por %s Procurar… Nenhuma aplicação encontrada\n\nTente utilizar menos termos de pesquisa ou adicione mais repositórios + Produtividade + Ferramentas + Finanças & carteiras + Entretenimento & média + Comunicação + Dispositivo + Rede + Ficheiros & armazenamento + Interesses + Diversos Por %1$s Última atualização: %1$s Última atualização: %1$s (%2$s) diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index c1929c710..32ba51497 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -654,6 +654,16 @@ escolhido Categoria Limpar + Produtividade + Ferramentas + Finanças & carteiras + Entretenimento & média + Comunicação + Dispositivo + Rede + Ficheiros & armazenamento + Interesses + Diversos Por %1$s Última atualização: %1$s Última atualização: %1$s (%2$s) diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 34c8ee6d6..91a759cfc 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -650,6 +650,16 @@ Aplicații de la %s Căutare… Nu s-au găsit aplicații\n\nÎncercați să utilizați mai puțini termeni de căutare sau adăugați mai multe depozite + Productivitate + Unelte + Finanțe și Portofele + Divertisment și media + Comunicare + Dispozitiv + Rețea + Fișiere și stocare + Pasiuni + Diverse De către %1$s Ultima actualizare: %1$s Ultima actualizare: %1$s (%2$s) diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 69e7cecb3..42a2b193d 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -654,6 +654,16 @@ Приложения от %s Поиск… Приложений не найдено\n\nПопробуйте использовать меньше поисковых запросов или добавить больше репозиториев + Производительность + Инструменты + Финансы и кошельки + Развлечения и СМИ + Коммуникация + Устройство + Сеть + Файлы и хранилище + Интересы + Разное От %1$s Последнее обновление: %1$s Последнее обновление: %1$s (%2$s) diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index ddd9f22e8..f63ae2b2d 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -578,6 +578,16 @@ Aplikácie od vyvojára %s Vyhľadať… Nenašli sa žiadne aplikácie\n\nSkúste použiť menej slov alebo pridať ďalšie repozitáre + Produktivita + Nástroje + Financie a peňaženky + Zábava a média + Komunikácia + Zariadenie + Sieť + Súbory a uložisko + Záujmy + Rôzne Od %1$s Naposledy aktualizované: %1$s Naposledy aktualizované: %1$s (%2$s) diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml index 2e10477cd..160e6c1eb 100644 --- a/app/src/main/res/values-sl/strings.xml +++ b/app/src/main/res/values-sl/strings.xml @@ -552,6 +552,16 @@ Aplikacije avtorja %s Iskanje … Ni najdenih aplikacij\n\nPoskusite uporabiti manj iskalnih izrazov ali dodajte več repozitorijev + Produktivnost + Orodja + Finance in denarnice + Zabava in mediji + Komunikacija + Naprava + Omrežje + Datoteke in shranjevanje + Interesi + Razno Od %1$s Zadnja posodobitev: %1$s Zadnja posodobitev: %1$s (%2$s) diff --git a/app/src/main/res/values-sq/strings.xml b/app/src/main/res/values-sq/strings.xml index 294f41371..e5f40f5e3 100644 --- a/app/src/main/res/values-sq/strings.xml +++ b/app/src/main/res/values-sq/strings.xml @@ -627,6 +627,15 @@ Aplikacione nga %s Kërkoni… S’u gjetën aplikacione\n\nProvoni të përdorni më pak terma kërkimi, ose të shtoni më tepër depo + Mjete + Financa & Kuleta + Spektakël & Media + Komunikim + Pajisje + Rrjet + Kartela & Depozitim + Interesa + Të ndryshme Nga %1$s Përditësuar së fundi më: %1$s Ky aplikacion përmban anti-veçori @@ -639,6 +648,7 @@ Problemi me këtë version të “%1$s” do të shpërfillet Fshihe Eksportoni listë aplikacionesh + Prodhimtari Shkarkoni aplikacione duke përdorur pikëmbarime web IPFS Që t’ia fillohet, njëkohësoni depot e aplikacioneve Jeni jashtë linje. Që t’ia fillohet, lidhuni në Internet. diff --git a/app/src/main/res/values-sr/strings.xml b/app/src/main/res/values-sr/strings.xml index 9ec33c0be..1efb8dc81 100644 --- a/app/src/main/res/values-sr/strings.xml +++ b/app/src/main/res/values-sr/strings.xml @@ -626,6 +626,16 @@ Апликације од %s Претражи… Нема пронађених апликација.\n\nПокушајте да користите мање појмова за претрагу или додајте још складишта + Продуктивност + Алати + Финансије и новчаници + Забава и садржај + Комуникација + Уређај + Мрежа + Датотеке и складиште + Интересовања + Разно Од %1$s Последње ажурирање: %1$s Последње ажурирано: %1$s (%2$s) diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index 4527b52c4..f6aa08b7a 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -644,6 +644,16 @@ Appar från %s Sök… Inga appar hittades\n\nFörsök att använda färre söktermer eller lägga till fler förråd + Produktivitet + Verktyg + Ekonomi & plånböcker + Underhållning & media + Kommunikation + Enhet + Nätverk + Filer & lagring + Intressen + Diverse Av %1$s Senast uppdaterad: %1$s Senast uppdaterad: %1$s (%2$s) diff --git a/app/src/main/res/values-sw/strings.xml b/app/src/main/res/values-sw/strings.xml index 1b97f2a92..4429586d2 100644 --- a/app/src/main/res/values-sw/strings.xml +++ b/app/src/main/res/values-sw/strings.xml @@ -600,6 +600,16 @@ Zilizosasishwa hivi karibuni Programu zote Tafuta + Uzalishaji + Zana + Fedha na Pochi + Burudani na Vyombo vya Habari + Mawasiliano + Kifaa + Mtandao + Faili na Hifadhi + Maslahi + Mengineyo Ilisasishwa mwisho: %1$s Nini kipya Changia diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 3a3a56990..159aca14e 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -630,6 +630,11 @@ Keşfet Uygulamalarım Gizle + Üretkenlik + Araçlar + Eğlence & Medya + İletişim + İlgi Alanları %1$s tarafından F-Nightly F-Nightly Temel @@ -646,6 +651,11 @@ %s tarafından uygulamalar Ara… Herhangi bir uygulama bulunamadı \n\nDaha az arama terimi kullanmayı ya da daha fazla depo eklemeyi deneyin + Finans ve Cüzdanlar + Cihaz + + Dosyalar ve Depolama + Çeşitli En son güncelleme: %1$s En son güncelleme: %1$s (%2$s) Yüklemeye hazırlanılıyor… diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2d33f886c..0365920b9 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -655,6 +655,16 @@ Додатки від %s Пошук… Не знайдено жодних програм\n\nСпробуйте використовувати менше пошукових термінів або додати більше репозиторіїв + Продуктивність + Інструменти + Фінанси та гаманці + Розваги та медіа + Комунікація + Пристрій + Мережа + Файли та зберігання + Інтереси + Різне Від %1$s Останні оновлено: %1$s Останні оновлено: %1$s (%2$s) diff --git a/app/src/main/res/values-uz/strings.xml b/app/src/main/res/values-uz/strings.xml index 82ab286da..ffc3639ee 100644 --- a/app/src/main/res/values-uz/strings.xml +++ b/app/src/main/res/values-uz/strings.xml @@ -79,6 +79,7 @@ Versiyasi va UUID\'larni serverlarga jo\'natish O\'rnatish tarixini jo\'natish O\'rnatilgan versiya: + Qurulma Yaxshirish Haqida Mening ilovalarim diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index fc70f2245..bda4dcd6c 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -602,6 +602,16 @@ Thu gọn F-Droid đã bị treo Thông tin kỹ thuật + Năng suất + Công cụ + Tài chính & Ví + Giải trí & Media + Liên lạc + Thiết bị + Kết nối mạng + Tệp & Lưu trữ + Sở thích + Linh tinh Bởi %1$s Lần cuối cập nhật: %1$s Lần cuối cập nhật: %1$s (%2$s) diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 0ddbfe6c6..45cbf0726 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -627,6 +627,16 @@ %s 的应用 搜索… 没找到应用\n\n试试减少搜索词或添加更多存储库 + 生产力 + 工具 + 财务和钱包 + 娱乐和媒体 + 通信 + 设备 + 网络 + 文件和存储 + 兴趣 + 杂项 由 %1$s 开发 上次更新:%1$s 上次更新: %1$s (%2$s) diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index d505d18c2..26b33b752 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -621,6 +621,16 @@ %s 的應用程式 搜尋… 找不到應用程式\n\n請嘗試減少搜尋關鍵字或新增軟體庫或 + 生產性 + 工具 + 財經與錢包 + 娛樂與媒體 + 通訊 + 裝置 + 網路 + 檔案與儲存 + 興趣 + 其他 最後更新: %1$s 最後更新: %1$s (%2$s) 正在準備安裝… diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7135c96ca..a0132f570 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -50,9 +50,7 @@ Filter selected - Featured Categories Category - See all Hide anti-features Filter Use filters to only show apps from specific categories or repositories. You can also change the sort order. @@ -60,6 +58,28 @@ Clear Hide + Calendars, tasks, clocks & notes + Navigation, weather, calculators & converters + Finance, money & pass managers + Music, podcasts, games & ebook readers + Email, messaging & social networks + Launcher, keyboard, security & theming + Browser, VPNs, proxies & firewall + Files, gallery, cloud sync & encryption + Science, sports, drawing & recipes + Uncategorized + + Productivity + Tools + Finances & Wallets + Entertainment & Media + Communication + Device + Network + Files & Storage + Interests + Miscellaneous + By %1$s Last updated: %1$s diff --git a/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt b/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt index a5b1b93ec..b5926d394 100644 --- a/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt +++ b/app/src/screenshotTest/kotlin/org/fdroid/ui/discover/DiscoverTest.kt @@ -6,6 +6,7 @@ import com.android.tools.screenshot.PreviewTest import org.fdroid.download.NetworkState import org.fdroid.repo.RepoUpdateProgress import org.fdroid.ui.ScreenshotTest +import org.fdroid.ui.categories.CategoryGroups import org.fdroid.ui.categories.CategoryItem @Composable @@ -96,12 +97,15 @@ private fun getLoadedModel(): LoadedDiscoverModel { AppDiscoverItem(packageName = "at.bitfire.davdroid", name = "DAVx⁵", isInstalled = false), ) val categories = - listOf( - CategoryItem(id = "Contact", name = "Contact"), - CategoryItem(id = "Email", name = "Email"), - CategoryItem(id = "Forum", name = "Forum"), - CategoryItem(id = "Messaging", name = "Messaging"), - CategoryItem(id = "Phone & SMS", name = "Phone & SMS"), + mapOf( + CategoryGroups.communication to + listOf( + CategoryItem(id = "Contact", name = "Contact"), + CategoryItem(id = "Email", name = "Email"), + CategoryItem(id = "Forum", name = "Forum"), + CategoryItem(id = "Messaging", name = "Messaging"), + CategoryItem(id = "Phone & SMS", name = "Phone & SMS"), + ) ) return LoadedDiscoverModel( newApps = newApps, diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png index b26af6832..3e42ba9f7 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/discover/DiscoverTestKt/DiscoverTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec431c87484108cf9f5680ba6fd5877dc7b8cf9d53aecd8016b89db2ca2582e3 -size 142401 +oid sha256:3e6fec5c784a921bf2e1f6d3460e8b9815d437cd28f0b23c5ff0c0c5164418f8 +size 163366 From b371da282c15a971dccb504ae9e192e4929202c6 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 10:57:45 -0300 Subject: [PATCH 28/38] Add Remote Access category --- app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt index 83fe8fe19..5dfbf364f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt +++ b/app/src/main/kotlin/org/fdroid/ui/categories/CategoryItem.kt @@ -131,6 +131,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Reading" -> AutoMirrored.Default.MenuBook "Recipe Manager" -> Icons.Default.RestaurantMenu "Religion" -> Icons.Default.Church + "Remote Access" -> Icons.Default.BrowserUpdated "Remote Controller" -> Icons.Default.SettingsRemote "Science & Education" -> Icons.Default.Science "Security" -> Icons.Default.Security @@ -209,6 +210,7 @@ data class CategoryItem(val id: String, val name: String, val description: Strin "Reading" -> CategoryGroups.media "Recipe Manager" -> CategoryGroups.interests "Religion" -> CategoryGroups.interests + "Remote Access" -> CategoryGroups.network "Remote Controller" -> CategoryGroups.tools "Science & Education" -> CategoryGroups.interests "Security" -> CategoryGroups.device From f9bc9aca893fa3dda65a2c4617b64eff6d260473 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 11:21:35 -0300 Subject: [PATCH 29/38] Re-enable DnsCacheTest as it miraculously works again --- .../java/org/fdroid/download/DnsCacheTest.kt | 2 -- .../kotlin/org/fdroid/download/DnsWithCache.kt | 14 +++++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt b/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt index 13bdd9eff..ddad92633 100644 --- a/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt +++ b/app/src/androidTest/java/org/fdroid/download/DnsCacheTest.kt @@ -9,7 +9,6 @@ import kotlin.test.assertNotNull import kotlin.test.assertNull import kotlin.test.assertTrue import org.fdroid.settings.SettingsManager -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith @@ -82,7 +81,6 @@ class DnsCacheTest { } @Test - @Ignore("failing") // TODO fun preloadCacheTest() { // test setup settings.useDnsCache = true diff --git a/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt b/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt index 04aaa068b..8dada7485 100644 --- a/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt +++ b/app/src/main/kotlin/org/fdroid/download/DnsWithCache.kt @@ -26,8 +26,8 @@ constructor(private val settingsManager: SettingsManager, private val cache: Dns } /** - * these methods provide a way to pre-load the cache with values from other sources such as - * the mirror info from the index, further reducing the needs to do DNS lookups + * these methods provide a way to preload the cache with values from other sources such as the + * mirror info from the index, further reducing the needs to do DNS lookups */ fun populateCacheWithStrings(hostname: String, ipv4List: List, ipv6List: List) { val ipv4ConvertedList = stringListToIpList(ipv4List) @@ -35,7 +35,11 @@ constructor(private val settingsManager: SettingsManager, private val cache: Dns populateCacheWithIps(hostname, ipv4ConvertedList, ipv6ConvertedList) } - fun populateCacheWithIps(hostname: String, ipv4List: List, ipv6List: List) { + fun populateCacheWithIps( + hostname: String, + ipv4List: List, + ipv6List: List, + ) { // at this time, the DNS cache only supports a single collection because that's what the DNS // lookup method returns. since these values are being inserted manually, it's possible that // the cache might end up with values that wouldn't have been returned by the lookup method. @@ -55,12 +59,12 @@ constructor(private val settingsManager: SettingsManager, private val cache: Dns try { InetAddress.getByName(it) } catch (e: UnknownHostException) { - log.warn { "Unexpected format for IP address: $it" } + log.error(e) { "Unexpected format for IP address: $it" } null } } } catch (e: Exception) { - log.warn { "Failed to parse list of IP addresses, returning empty list" } + log.error(e) { "Failed to parse list of IP addresses, returning empty list" } return emptyList() } } From 656c1aaf5a8ddd16dfdc913335ce5259fde7a0f8 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 12:06:25 -0300 Subject: [PATCH 30/38] Add setting to auto-show search keyboard --- .../org/fdroid/settings/SettingsConstants.kt | 3 ++ .../org/fdroid/settings/SettingsManager.kt | 11 +++++++ app/src/main/kotlin/org/fdroid/ui/Main.kt | 2 ++ .../org/fdroid/ui/search/GlobalSearch.kt | 33 ++++++++++++++++++- .../kotlin/org/fdroid/ui/search/SearchInfo.kt | 3 ++ .../org/fdroid/ui/search/SearchViewModel.kt | 13 ++++++-- .../org/fdroid/ui/utils/PreviewUtils.kt | 3 ++ app/src/main/res/values/strings.xml | 1 + .../GlobalSearchEmptyStateTest_2ed8e27d_0.png | 4 +-- .../GlobalSearchEmptyTest_2ed8e27d_0.png | 4 +-- 10 files changed, 70 insertions(+), 7 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt index f6f1423e6..51e4a0ee0 100644 --- a/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt +++ b/app/src/main/kotlin/org/fdroid/settings/SettingsConstants.kt @@ -58,6 +58,9 @@ object SettingsConstants { const val PREF_KEY_WARN_WHEN_METERED = "warnWhenMetered" const val PREF_DEFAULT_WARN_WHEN_METERED = true + const val PREF_KEY_SHOW_SEARCH_KEYBOARD = "showSearchKeyboard" + const val PREF_DEFAULT_SHOW_SEARCH_KEYBOARD = false + const val PREF_KEY_SHOW_INCOMPATIBLE = "incompatibleVersions" const val PREF_DEFAULT_SHOW_INCOMPATIBLE = true diff --git a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt index 2c3ac978a..215eeb172 100644 --- a/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt +++ b/app/src/main/kotlin/org/fdroid/settings/SettingsManager.kt @@ -33,6 +33,7 @@ import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PREVENT_SCREENSHOTS import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_PROXY import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_REPO_UPDATES import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_SHOW_INCOMPATIBLE +import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_SHOW_SEARCH_KEYBOARD import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_THEME import org.fdroid.settings.SettingsConstants.PREF_DEFAULT_WARN_WHEN_METERED import org.fdroid.settings.SettingsConstants.PREF_DNS_CACHE @@ -50,6 +51,7 @@ import org.fdroid.settings.SettingsConstants.PREF_KEY_PREVENT_SCREENSHOTS import org.fdroid.settings.SettingsConstants.PREF_KEY_PROXY import org.fdroid.settings.SettingsConstants.PREF_KEY_REPO_UPDATES import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_INCOMPATIBLE +import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_SEARCH_KEYBOARD import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME import org.fdroid.settings.SettingsConstants.PREF_KEY_WARN_WHEN_METERED import org.fdroid.settings.SettingsConstants.PREF_USE_DNS_CACHE @@ -194,6 +196,10 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val MutableStateFlow(prefs.getBoolean(PREF_KEY_WARN_WHEN_METERED, PREF_DEFAULT_WARN_WHEN_METERED)) val warnWhenMeteredFlow = _warnWhenMeteredFlow.asStateFlow() + private val _showSearchKeyboardFlow = + MutableStateFlow(prefs.getBoolean(PREF_KEY_SHOW_SEARCH_KEYBOARD, PREF_DEFAULT_SHOW_SEARCH_KEYBOARD)) + val showSearchKeyboardFlow = _showSearchKeyboardFlow.asStateFlow() + val filterIncompatible: Boolean get() = !prefs.getBoolean(PREF_KEY_SHOW_INCOMPATIBLE, PREF_DEFAULT_SHOW_INCOMPATIBLE) @@ -217,6 +223,11 @@ class SettingsManager @Inject constructor(@param:ApplicationContext private val _warnWhenMeteredFlow.update { false } } + fun setShowSearchKeyboard(show: Boolean) { + prefs.edit { putBoolean(PREF_KEY_SHOW_SEARCH_KEYBOARD, show) } + _showSearchKeyboardFlow.update { show } + } + fun saveAppListFilter(sortOrder: AppListSortOrder, filterIncompatible: Boolean) { prefs.edit { putBoolean(PREF_KEY_SHOW_INCOMPATIBLE, !filterIncompatible) diff --git a/app/src/main/kotlin/org/fdroid/ui/Main.kt b/app/src/main/kotlin/org/fdroid/ui/Main.kt index 6e8c7bf33..99b9bd33a 100644 --- a/app/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/app/src/main/kotlin/org/fdroid/ui/Main.kt @@ -86,6 +86,8 @@ fun Main(onListeningForIntent: () -> Unit = {}) { viewModel.savedSearchesFlow.collectAsStateWithLifecycle().value override val categories: List? = viewModel.categories.collectAsStateWithLifecycle(null).value + override val autoShowKeyboard: Boolean = + viewModel.autoShowKeyboard.collectAsStateWithLifecycle().value override val showKeyboard: Boolean = showSearchKeyboard override val actions: SearchActions = viewModel diff --git a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt index 71256c600..8a489e492 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/GlobalSearch.kt @@ -7,18 +7,27 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Keyboard +import androidx.compose.material3.Checkbox +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBarDefaults +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import org.fdroid.R import org.fdroid.search.SavedSearch import org.fdroid.ui.FDroidContent import org.fdroid.ui.navigation.NavigationKey +import org.fdroid.ui.utils.TopAppBarOverflowButton import org.fdroid.ui.utils.appListItems import org.fdroid.ui.utils.categoryItems import org.fdroid.ui.utils.getSearchInfo @@ -36,14 +45,36 @@ fun GlobalSearch( if (info.showKeyboard) info.onKeyboardShown() } val searchResults = info.searchResults + val showKeyboard = + info.showKeyboard || // either show keyboard because user double tapped the search icon + (info.autoShowKeyboard && // or auto-show is activated + (searchResults == null || // but only if no search results are shown + searchResults.apps.isEmpty() && searchResults.categories.isEmpty())) Scaffold( topBar = { TopSearchBar( searchFieldState = textFieldState, - shouldRequestFocus = info.showKeyboard, + shouldRequestFocus = showKeyboard, onSearch = info.actions::onSearch, onSearchCleared = info.actions::onSearchCleared, onHideSearch = onBack, + actions = { + if (textFieldState.text.isEmpty()) { + TopAppBarOverflowButton { onDismiss -> + DropdownMenuItem( + leadingIcon = { Icon(Icons.Default.Keyboard, null) }, + text = { Text(stringResource(R.string.search_auto_keyboard)) }, + trailingIcon = { + Checkbox(checked = info.autoShowKeyboard, onCheckedChange = null) + }, + onClick = { + info.actions.setAutoShowKeyboard(!info.autoShowKeyboard) + onDismiss() + }, + ) + } + } + }, ) } ) { paddingValues -> diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt index 428dc3a23..6b501fa23 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchInfo.kt @@ -7,6 +7,7 @@ interface SearchInfo { val searchResults: SearchResults? val savedSearches: List? val categories: List? + val autoShowKeyboard: Boolean val showKeyboard: Boolean val actions: SearchActions fun onKeyboardShown() @@ -18,4 +19,6 @@ interface SearchActions { fun onSearchCleared() fun onClearSearchHistory() + + fun setAutoShowKeyboard(autoShow: Boolean) } diff --git a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt index 25ba59b17..4c3c97423 100644 --- a/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/search/SearchViewModel.kt @@ -8,16 +8,21 @@ import jakarta.inject.Inject import kotlinx.coroutines.launch import org.fdroid.search.SearchHelper.isSearchable import org.fdroid.search.SearchManager +import org.fdroid.settings.SettingsManager @HiltViewModel class SearchViewModel @Inject -constructor(app: Application, private val searchManager: SearchManager) : - AndroidViewModel(app), SearchActions { +constructor( + app: Application, + private val searchManager: SearchManager, + private val settingsManager: SettingsManager, +) : AndroidViewModel(app), SearchActions { val searchResults = searchManager.searchResults val savedSearchesFlow = searchManager.savedSearches val categories = searchManager.categories + val autoShowKeyboard = settingsManager.showSearchKeyboardFlow override suspend fun onSearch(term: String) { if (term.isSearchable()) searchManager.search(term) @@ -28,4 +33,8 @@ constructor(app: Application, private val searchManager: SearchManager) : override fun onClearSearchHistory() { viewModelScope.launch { searchManager.onClearSearchHistory() } } + + override fun setAutoShowKeyboard(autoShow: Boolean) { + settingsManager.setShowSearchKeyboard(autoShow) + } } diff --git a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index 9a9db89fc..21b55121f 100644 --- a/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/app/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -650,6 +650,7 @@ fun getSearchInfo( override val savedSearches: List? = savedSearches override val categories: List? = categories + override val autoShowKeyboard: Boolean = false override val showKeyboard: Boolean = false override val actions: SearchActions = @@ -659,6 +660,8 @@ fun getSearchInfo( override fun onSearchCleared() {} override fun onClearSearchHistory() {} + + override fun setAutoShowKeyboard(autoShow: Boolean) {} } override fun onKeyboardShown() {} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a0132f570..aec885105 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -38,6 +38,7 @@ No apps found\n\nTry using fewer search terms or add more repositories No matching apps\n\nTry using fewer search terms or remove filters No matching apps\n\nTry using fewer search terms + Automatically open keyboard Sorting and compatibility Sort by name Sort by last update diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png index 0d65142d8..3c1e33fd3 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyStateTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e926d11d01b2d8786e9af5077f60d47b51d23605e016559c94b998c85edd44f0 -size 119038 +oid sha256:9d61085047b8397d4f99513c930fac8bf4b5e6fcdd6f38a5c6aa177eb8a294be +size 119701 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png index 90a354891..262aeb078 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/search/GlobalSearchTestKt/GlobalSearchEmptyTest_2ed8e27d_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b84731273d6b914154b20cd44c6a0ef56b6d5fd0815d0585e6d39da75b75760b -size 31966 +oid sha256:119b1d10a0e9926c96197938152d5ef6e2e15c71017064e48af612a794e82082 +size 32593 From 278b48ec69a7566c785d1376a2f2f1aa1a3467f9 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 12:20:38 -0300 Subject: [PATCH 31/38] Don't show no-updates card when filtering my apps --- app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt | 6 +----- app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt | 1 + app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt | 7 ++++++- app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt | 1 + 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt index 878aceccd..edd354359 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyApps.kt @@ -186,11 +186,8 @@ fun MyAppsLoadingPreview() { val model = MyAppsModel( installingApps = emptyList(), - appUpdates = null, - installedApps = null, showUpdatesHint = false, showAppIssueHint = false, - sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), ) FDroidContent { @@ -224,12 +221,11 @@ fun MyAppsEmptyPreview() { FDroidContent { val model = MyAppsModel( - installingApps = emptyList(), appUpdates = emptyList(), + installingApps = emptyList(), installedApps = emptyList(), showUpdatesHint = false, showAppIssueHint = false, - sortOrder = AppListSortOrder.NAME, networkState = NetworkState(isOnline = false, isMetered = false), ) MyApps( diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt index d804dd4e7..b9b9759f3 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsInfo.kt @@ -19,6 +19,7 @@ data class MyAppsModel( val showAppIssueHint: Boolean, val sortOrder: AppListSortOrder = AppListSortOrder.NAME, val networkState: NetworkState, + val isSearching: Boolean = false, val appUpdatesBytes: Long? = null, ) diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt index 2aa082bce..b4c259650 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsList.kt @@ -75,7 +75,12 @@ fun MyAppsList( modifier.then(if (currentPackageName == null) Modifier else Modifier.selectableGroup()), ) { // Updates header with Update all button - if (updatableApps != null && updatableApps.isEmpty() && myAppsInfo.model.showUpdatesHint) { + if ( + updatableApps != null && + updatableApps.isEmpty() && + myAppsInfo.model.showUpdatesHint && + !myAppsInfo.model.isSearching + ) { item(key = "A-hint", contentType = "hint") { OnboardingCard( title = stringResource(R.string.apps_updates_hint_title), diff --git a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt index 013c76eb6..7301a0794 100644 --- a/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt +++ b/app/src/main/kotlin/org/fdroid/ui/apps/MyAppsPresenter.kt @@ -105,6 +105,7 @@ fun MyAppsPresenter( showAppIssueHint = showAppIssueHintFlow.collectAsState().value, sortOrder = sortOrder, networkState = networkStateFlow.collectAsState().value, + isSearching = searchQuery.isNotBlank(), appUpdatesBytes = updateBytes, ) } From 21a85cde947301e942970c5fc5affcbebdd98115 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Fri, 24 Apr 2026 14:28:14 -0300 Subject: [PATCH 32/38] Use a different text color for app details progress text --- app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt | 3 +++ .../DetailsHeaderDownloadingNightTest_3534351a_0.png | 4 ++-- .../DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png | 4 ++-- .../DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png | 4 ++-- 4 files changed, 9 insertions(+), 6 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt index 6538d0860..5ad830255 100644 --- a/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt +++ b/app/src/main/kotlin/org/fdroid/ui/details/AppDetailsHeader.kt @@ -223,6 +223,7 @@ fun AppDetailsHeader( Text( text = stringResource(strRes), style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), overflow = TextOverflow.Ellipsis, maxLines = 1, ) @@ -236,6 +237,7 @@ fun AppDetailsHeader( Text( text = stringResource(R.string.status_downloading, speedStr), style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), overflow = TextOverflow.StartEllipsis, maxLines = 1, modifier = Modifier.padding(end = 8.dp), @@ -243,6 +245,7 @@ fun AppDetailsHeader( Text( text = stringResource(R.string.status_downloading_remaining, remainingStr), style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), overflow = TextOverflow.Ellipsis, maxLines = 1, ) diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png index b86ae526c..fa4d0e568 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingNightTest_3534351a_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:92066e2671a0fa2733feb5701d540b612714b7f8f23b69133b0e4fc06a5b4a64 -size 46451 +oid sha256:e7f3085f7ba43f765a1fd85cb3abcffb7d6e559da92fc9c71a353a239786600b +size 46387 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png index aa655ebe4..1e386d0df 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderDownloadingTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b7c985dfe300bc834da2b8bfc874c79ebcb5bbfc9adc42f19391f4c719a9ee4c -size 45829 +oid sha256:03bd16dda3867f2fa18f603d71b9a7d7ac49acf5b8d5e819433226e6dbd315e2 +size 45636 diff --git a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png index 940bfebc0..a4fef3c50 100644 --- a/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png +++ b/app/src/screenshotTestBasicDefaultDebug/reference/org/fdroid/ui/details/DetailsHeaderTestKt/DetailsHeaderStartingTest_0.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6491de8f2575bb81b4de9c098ebfc0b4727efd936cd3e57fb0368c35d9a5f7c3 -size 40858 +oid sha256:d56fab1b947d2fc36723e406d56016e36c1beb9c1e35a5f66751c6bc7759d27e +size 40685 From 978725310d026b52612bfe3f4d32eeda93183476 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 27 Apr 2026 15:09:29 -0300 Subject: [PATCH 33/38] [db] run fixtures inside transaction and don't release the DB before finished This is to fix a first-start race condition where the DB was already being used, but the repos weren't even inserted. --- .../13.json | 1136 +++++++++++++++++ .../java/org/fdroid/database/DbMetadata.kt | 23 + .../org/fdroid/database/FDroidDatabase.kt | 7 +- .../fdroid/database/FDroidDatabaseHolder.kt | 55 +- .../java/org/fdroid/database/Migrations.kt | 13 + 5 files changed, 1205 insertions(+), 29 deletions(-) create mode 100644 libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/13.json create mode 100644 libs/database/src/main/java/org/fdroid/database/DbMetadata.kt diff --git a/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/13.json b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/13.json new file mode 100644 index 000000000..e26e79617 --- /dev/null +++ b/libs/database/schemas/org.fdroid.database.FDroidDatabaseInt/13.json @@ -0,0 +1,1136 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "447cc6c706ec5aa97a6983c3c4a0d55b", + "entities": [ + { + "tableName": "CoreRepository", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `icon` TEXT, `address` TEXT NOT NULL, `webBaseUrl` TEXT, `timestamp` INTEGER NOT NULL, `version` INTEGER, `formatVersion` TEXT, `maxAge` INTEGER, `description` TEXT NOT NULL, `certificate` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "address", + "columnName": "address", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "webBaseUrl", + "columnName": "webBaseUrl", + "affinity": "TEXT" + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER" + }, + { + "fieldPath": "formatVersion", + "columnName": "formatVersion", + "affinity": "TEXT" + }, + { + "fieldPath": "maxAge", + "columnName": "maxAge", + "affinity": "INTEGER" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate", + "columnName": "certificate", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "repoId" + ] + } + }, + { + "tableName": "Mirror", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `url` TEXT NOT NULL, `countryCode` TEXT, `dnsA` TEXT, `dnsAAAA` TEXT, `isPrimary` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`repoId`, `url`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "countryCode", + "columnName": "countryCode", + "affinity": "TEXT" + }, + { + "fieldPath": "dnsA", + "columnName": "dnsA", + "affinity": "TEXT" + }, + { + "fieldPath": "dnsAAAA", + "columnName": "dnsAAAA", + "affinity": "TEXT" + }, + { + "fieldPath": "isPrimary", + "columnName": "isPrimary", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "url" + ] + }, + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "AntiFeature", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "id" + ] + }, + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "Category", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "id" + ] + }, + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "ReleaseChannel", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `id` TEXT NOT NULL, `icon` TEXT NOT NULL, `name` TEXT NOT NULL, `description` TEXT NOT NULL, PRIMARY KEY(`repoId`, `id`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "id" + ] + }, + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "RepositoryPreferences", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `lastUpdated` INTEGER, `lastETag` TEXT, `userMirrors` TEXT, `disabledMirrors` TEXT, `username` TEXT, `password` TEXT, `errorCount` INTEGER NOT NULL DEFAULT 0, `lastError` TEXT, PRIMARY KEY(`repoId`))", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "weight", + "columnName": "weight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastETag", + "columnName": "lastETag", + "affinity": "TEXT" + }, + { + "fieldPath": "userMirrors", + "columnName": "userMirrors", + "affinity": "TEXT" + }, + { + "fieldPath": "disabledMirrors", + "columnName": "disabledMirrors", + "affinity": "TEXT" + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT" + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT" + }, + { + "fieldPath": "errorCount", + "columnName": "errorCount", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "lastError", + "columnName": "lastError", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId" + ] + } + }, + { + "tableName": "AppMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `added` INTEGER NOT NULL, `lastUpdated` INTEGER NOT NULL, `name` TEXT, `summary` TEXT, `description` TEXT, `localizedName` TEXT, `localizedSummary` TEXT, `webSite` TEXT, `changelog` TEXT, `license` TEXT, `sourceCode` TEXT, `issueTracker` TEXT, `translation` TEXT, `preferredSigner` TEXT, `video` TEXT, `authorName` TEXT, `authorEmail` TEXT, `authorWebSite` TEXT, `authorPhone` TEXT, `donate` TEXT, `liberapayID` TEXT, `liberapay` TEXT, `openCollective` TEXT, `bitcoin` TEXT, `litecoin` TEXT, `flattrID` TEXT, `categories` TEXT, `isCompatible` INTEGER NOT NULL, PRIMARY KEY(`repoId`, `packageName`), FOREIGN KEY(`repoId`) REFERENCES `CoreRepository`(`repoId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "summary", + "columnName": "summary", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "localizedName", + "columnName": "localizedName", + "affinity": "TEXT" + }, + { + "fieldPath": "localizedSummary", + "columnName": "localizedSummary", + "affinity": "TEXT" + }, + { + "fieldPath": "webSite", + "columnName": "webSite", + "affinity": "TEXT" + }, + { + "fieldPath": "changelog", + "columnName": "changelog", + "affinity": "TEXT" + }, + { + "fieldPath": "license", + "columnName": "license", + "affinity": "TEXT" + }, + { + "fieldPath": "sourceCode", + "columnName": "sourceCode", + "affinity": "TEXT" + }, + { + "fieldPath": "issueTracker", + "columnName": "issueTracker", + "affinity": "TEXT" + }, + { + "fieldPath": "translation", + "columnName": "translation", + "affinity": "TEXT" + }, + { + "fieldPath": "preferredSigner", + "columnName": "preferredSigner", + "affinity": "TEXT" + }, + { + "fieldPath": "video", + "columnName": "video", + "affinity": "TEXT" + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT" + }, + { + "fieldPath": "authorEmail", + "columnName": "authorEmail", + "affinity": "TEXT" + }, + { + "fieldPath": "authorWebSite", + "columnName": "authorWebSite", + "affinity": "TEXT" + }, + { + "fieldPath": "authorPhone", + "columnName": "authorPhone", + "affinity": "TEXT" + }, + { + "fieldPath": "donate", + "columnName": "donate", + "affinity": "TEXT" + }, + { + "fieldPath": "liberapayID", + "columnName": "liberapayID", + "affinity": "TEXT" + }, + { + "fieldPath": "liberapay", + "columnName": "liberapay", + "affinity": "TEXT" + }, + { + "fieldPath": "openCollective", + "columnName": "openCollective", + "affinity": "TEXT" + }, + { + "fieldPath": "bitcoin", + "columnName": "bitcoin", + "affinity": "TEXT" + }, + { + "fieldPath": "litecoin", + "columnName": "litecoin", + "affinity": "TEXT" + }, + { + "fieldPath": "flattrID", + "columnName": "flattrID", + "affinity": "TEXT" + }, + { + "fieldPath": "categories", + "columnName": "categories", + "affinity": "TEXT" + }, + { + "fieldPath": "isCompatible", + "columnName": "isCompatible", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName" + ] + }, + "indices": [ + { + "name": "index_AppMetadata_authorName", + "unique": false, + "columnNames": [ + "authorName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_AppMetadata_authorName` ON `${TABLE_NAME}` (`authorName`)" + } + ], + "foreignKeys": [ + { + "table": "CoreRepository", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId" + ], + "referencedColumns": [ + "repoId" + ] + } + ] + }, + { + "tableName": "AppMetadataFts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`repoId` INTEGER NOT NULL, `name` TEXT, `summary` TEXT, `description` TEXT, `authorName` TEXT, `packageName` TEXT NOT NULL, tokenize=unicode61 `remove_diacritics=1` `separators=.` `tokenchars=-`, content=`AppMetadata`, notindexed=`repoId`)", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "summary", + "columnName": "summary", + "affinity": "TEXT" + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT" + }, + { + "fieldPath": "authorName", + "columnName": "authorName", + "affinity": "TEXT" + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [ + "remove_diacritics=1", + "separators=.", + "tokenchars=-" + ], + "contentTable": "AppMetadata", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [ + "repoId" + ], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_UPDATE BEFORE UPDATE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_BEFORE_DELETE BEFORE DELETE ON `AppMetadata` BEGIN DELETE FROM `AppMetadataFts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_UPDATE AFTER UPDATE ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_AppMetadataFts_AFTER_INSERT AFTER INSERT ON `AppMetadata` BEGIN INSERT INTO `AppMetadataFts`(`docid`, `repoId`, `name`, `summary`, `description`, `authorName`, `packageName`) VALUES (NEW.`rowid`, NEW.`repoId`, NEW.`name`, NEW.`summary`, NEW.`description`, NEW.`authorName`, NEW.`packageName`); END" + ] + }, + { + "tableName": "LocalizedFile", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER" + }, + { + "fieldPath": "ipfsCidV1", + "columnName": "ipfsCidV1", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "type", + "locale" + ] + }, + "indices": [ + { + "name": "index_LocalizedFile_packageName", + "unique": false, + "columnNames": [ + "packageName" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalizedFile_packageName` ON `${TABLE_NAME}` (`packageName`)" + } + ], + "foreignKeys": [ + { + "table": "AppMetadata", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName" + ], + "referencedColumns": [ + "repoId", + "packageName" + ] + } + ] + }, + { + "tableName": "LocalizedFileList", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `type` TEXT NOT NULL, `locale` TEXT NOT NULL, `name` TEXT NOT NULL, `sha256` TEXT, `size` INTEGER, `ipfsCidV1` TEXT, PRIMARY KEY(`repoId`, `packageName`, `type`, `locale`, `name`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "locale", + "columnName": "locale", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sha256", + "columnName": "sha256", + "affinity": "TEXT" + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER" + }, + { + "fieldPath": "ipfsCidV1", + "columnName": "ipfsCidV1", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "type", + "locale", + "name" + ] + }, + "indices": [ + { + "name": "index_LocalizedFileList_packageName_repoId", + "unique": false, + "columnNames": [ + "packageName", + "repoId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_LocalizedFileList_packageName_repoId` ON `${TABLE_NAME}` (`packageName`, `repoId`)" + } + ], + "foreignKeys": [ + { + "table": "AppMetadata", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName" + ], + "referencedColumns": [ + "repoId", + "packageName" + ] + } + ] + }, + { + "tableName": "Version", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `added` INTEGER NOT NULL, `releaseChannels` TEXT, `antiFeatures` TEXT, `whatsNew` TEXT, `appLabel` TEXT, `isCompatible` INTEGER NOT NULL, `file_name` TEXT NOT NULL, `file_sha256` TEXT NOT NULL, `file_size` INTEGER, `file_ipfsCidV1` TEXT, `src_name` TEXT, `src_sha256` TEXT, `src_size` INTEGER, `src_ipfsCidV1` TEXT, `manifest_versionName` TEXT NOT NULL, `manifest_versionCode` INTEGER NOT NULL, `manifest_maxSdkVersion` INTEGER, `manifest_nativecode` TEXT, `manifest_features` TEXT, `manifest_usesSdk_minSdkVersion` INTEGER, `manifest_usesSdk_targetSdkVersion` INTEGER, `manifest_signer_sha256` TEXT, `manifest_signer_hasMultipleSigners` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`), FOREIGN KEY(`repoId`, `packageName`) REFERENCES `AppMetadata`(`repoId`, `packageName`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionId", + "columnName": "versionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "added", + "columnName": "added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "releaseChannels", + "columnName": "releaseChannels", + "affinity": "TEXT" + }, + { + "fieldPath": "antiFeatures", + "columnName": "antiFeatures", + "affinity": "TEXT" + }, + { + "fieldPath": "whatsNew", + "columnName": "whatsNew", + "affinity": "TEXT" + }, + { + "fieldPath": "appLabel", + "columnName": "appLabel", + "affinity": "TEXT" + }, + { + "fieldPath": "isCompatible", + "columnName": "isCompatible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "file.name", + "columnName": "file_name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "file.sha256", + "columnName": "file_sha256", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "file.size", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "file.ipfsCidV1", + "columnName": "file_ipfsCidV1", + "affinity": "TEXT" + }, + { + "fieldPath": "src.name", + "columnName": "src_name", + "affinity": "TEXT" + }, + { + "fieldPath": "src.sha256", + "columnName": "src_sha256", + "affinity": "TEXT" + }, + { + "fieldPath": "src.size", + "columnName": "src_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "src.ipfsCidV1", + "columnName": "src_ipfsCidV1", + "affinity": "TEXT" + }, + { + "fieldPath": "manifest.versionName", + "columnName": "manifest_versionName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "manifest.versionCode", + "columnName": "manifest_versionCode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "manifest.maxSdkVersion", + "columnName": "manifest_maxSdkVersion", + "affinity": "INTEGER" + }, + { + "fieldPath": "manifest.nativecode", + "columnName": "manifest_nativecode", + "affinity": "TEXT" + }, + { + "fieldPath": "manifest.features", + "columnName": "manifest_features", + "affinity": "TEXT" + }, + { + "fieldPath": "manifest.usesSdk.minSdkVersion", + "columnName": "manifest_usesSdk_minSdkVersion", + "affinity": "INTEGER" + }, + { + "fieldPath": "manifest.usesSdk.targetSdkVersion", + "columnName": "manifest_usesSdk_targetSdkVersion", + "affinity": "INTEGER" + }, + { + "fieldPath": "manifest.signer.sha256", + "columnName": "manifest_signer_sha256", + "affinity": "TEXT" + }, + { + "fieldPath": "manifest.signer.hasMultipleSigners", + "columnName": "manifest_signer_hasMultipleSigners", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "versionId" + ] + }, + "foreignKeys": [ + { + "table": "AppMetadata", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName" + ], + "referencedColumns": [ + "repoId", + "packageName" + ] + } + ] + }, + { + "tableName": "VersionedString", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`repoId` INTEGER NOT NULL, `packageName` TEXT NOT NULL, `versionId` TEXT NOT NULL, `type` TEXT NOT NULL, `name` TEXT NOT NULL, `version` INTEGER, PRIMARY KEY(`repoId`, `packageName`, `versionId`, `type`, `name`), FOREIGN KEY(`repoId`, `packageName`, `versionId`) REFERENCES `Version`(`repoId`, `packageName`, `versionId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "repoId", + "columnName": "repoId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "versionId", + "columnName": "versionId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "repoId", + "packageName", + "versionId", + "type", + "name" + ] + }, + "indices": [ + { + "name": "index_VersionedString_versionId", + "unique": false, + "columnNames": [ + "versionId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_VersionedString_versionId` ON `${TABLE_NAME}` (`versionId`)" + } + ], + "foreignKeys": [ + { + "table": "Version", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "repoId", + "packageName", + "versionId" + ], + "referencedColumns": [ + "repoId", + "packageName", + "versionId" + ] + } + ] + }, + { + "tableName": "AppPrefs", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`packageName` TEXT NOT NULL, `ignoreVersionCodeUpdate` INTEGER NOT NULL, `preferredRepoId` INTEGER, `appPrefReleaseChannels` TEXT, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ignoreVersionCodeUpdate", + "columnName": "ignoreVersionCodeUpdate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "preferredRepoId", + "columnName": "preferredRepoId", + "affinity": "INTEGER" + }, + { + "fieldPath": "appPrefReleaseChannels", + "columnName": "appPrefReleaseChannels", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + } + }, + { + "tableName": "DbMetadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + } + } + ], + "views": [ + { + "viewName": "LocalizedIcon", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM LocalizedFile WHERE type='icon'" + }, + { + "viewName": "HighestVersion", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT repoId, packageName, antiFeatures FROM Version\n GROUP BY repoId, packageName HAVING MAX(manifest_versionCode)" + }, + { + "viewName": "PreferredRepo", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT packageName, repoId AS preferredRepoId FROM AppMetadata\n JOIN RepositoryPreferences AS pref USING (repoId)\n LEFT JOIN AppPrefs USING (packageName)\n WHERE pref.enabled = 1 AND (repoId = COALESCE(preferredRepoId, repoId) OR\n NOT EXISTS (SELECT 1 FROM AppMetadata WHERE repoId=AppPrefs.preferredRepoId AND packageName=AppPrefs.packageName)\n )\n GROUP BY packageName HAVING MAX(pref.weight)" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '447cc6c706ec5aa97a6983c3c4a0d55b')" + ] + } +} \ No newline at end of file diff --git a/libs/database/src/main/java/org/fdroid/database/DbMetadata.kt b/libs/database/src/main/java/org/fdroid/database/DbMetadata.kt new file mode 100644 index 000000000..148e12de0 --- /dev/null +++ b/libs/database/src/main/java/org/fdroid/database/DbMetadata.kt @@ -0,0 +1,23 @@ +package org.fdroid.database + +import androidx.room.Dao +import androidx.room.Entity +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.PrimaryKey +import androidx.room.Query + +@Entity +internal data class DbMetadata( + @PrimaryKey val key: String, + val value: String +) + +@Dao +internal interface DbMetadataDao { + @Query("SELECT value FROM DbMetadata WHERE `key` = :key") + suspend fun get(key: String): String? + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun set(vararg entries: DbMetadata) +} diff --git a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt index 343bbcdca..058f2c5fc 100644 --- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt +++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabase.kt @@ -18,7 +18,7 @@ import org.fdroid.LocaleChooser.getBestLocale // When bumping this version, please make sure to add one (or more) migration(s) below! // Consider also providing tests for that migration. // Don't forget to commit the new schema to the git repo as well. - version = 12, + version = 13, entities = [ // repo @@ -38,6 +38,8 @@ import org.fdroid.LocaleChooser.getBestLocale VersionedString::class, // app user preferences AppPrefs::class, + // internal metadata + DbMetadata::class, ], views = [LocalizedIcon::class, HighestVersion::class, PreferredRepo::class], exportSchema = true, @@ -54,6 +56,7 @@ import org.fdroid.LocaleChooser.getBestLocale AutoMigration(9, 10), AutoMigration(10, 11), AutoMigration(11, 12), + AutoMigration(12, 13, DbMetadataMigration::class), // add future migrations above! ], ) @@ -69,6 +72,8 @@ internal abstract class FDroidDatabaseInt : RoomDatabase(), FDroidDatabase, Clos abstract override fun getAppPrefsDao(): AppPrefsDaoInt + abstract fun getDbMetadataDao(): DbMetadataDao + @Deprecated("Will be removed in future version") override fun afterLocalesChanged(locales: LocaleListCompat) { val appDao = getAppDao() diff --git a/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt b/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt index c68178a09..e961417f0 100644 --- a/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt +++ b/libs/database/src/main/java/org/fdroid/database/FDroidDatabaseHolder.kt @@ -4,23 +4,18 @@ import android.content.Context import android.util.Log import androidx.annotation.GuardedBy import androidx.room.Room -import androidx.room.RoomDatabase -import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.room.withTransaction import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory -import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext /** * A way to pre-populate the database with a fixture. This can be supplied to * [FDroidDatabaseHolder.getDb] and will then be called when a new database is created. */ public fun interface FDroidFixture { - /** - * Called when a new database gets created. Multiple DB operations should use - * [FDroidDatabase.runInTransaction]. - */ + /** Called when a new database gets created. This gets run inside a transaction. */ public fun prePopulateDb(db: FDroidDatabase) } @@ -61,8 +56,6 @@ public object FDroidDatabaseHolder { // We allow destructive migration (if no real migration was provided), // so we have the option to nuke the DB in production (if that will ever be needed). fallbackToDestructiveMigration(false) - // Add our [FixtureCallback] if a fixture was provided - if (fixture != null) addCallback(FixtureCallback(fixture)) if (logSlowQueries) { openHelperFactory( @@ -73,27 +66,33 @@ public object FDroidDatabaseHolder { ) } } - val instance = builder.build() - this.instance = instance - // return instance - instance + val db = builder.build() + runBlocking { runFixtureIfNeeded(db, fixture) } + instance = db + db } } - private class FixtureCallback(private val fixture: FDroidFixture) : RoomDatabase.Callback() { - override fun onCreate(db: SupportSQLiteDatabase) { - super.onCreate(db) - @OptIn(DelicateCoroutinesApi::class) - GlobalScope.launch(dispatcher) { - val database: FDroidDatabase - synchronized(lock) { database = instance ?: error("DB not yet initialized") } - fixture.prePopulateDb(database) - Log.d(TAG, "Loaded fixtures") + /** + * Inserts the given [fixture] into the DB if it has not been run before. We are using a flag in + * the [DbMetadata] table set in the same transaction to ensure fixtures get inserted only once + * when the DB is created. + * + * This is an ugly hack allowing us to insert fixtures into the DB while it is being constructed + * on the UiThread without having to allow general UiThread access to the DB. + */ + private suspend fun runFixtureIfNeeded(db: FDroidDatabaseInt, fixture: FDroidFixture?) { + withContext(dispatcher) { + db.withTransaction { + val isSetup = db.getDbMetadataDao().get("setup") + if (isSetup != "true") { + fixture?.prePopulateDb(db) + db.getDbMetadataDao().set(DbMetadata("setup", "true")) + Log.d(TAG, "Finished initial setup") + } else { + Log.d(TAG, "DB already set up") + } } } - - override fun onDestructiveMigration(db: SupportSQLiteDatabase) { - onCreate(db) - } } } diff --git a/libs/database/src/main/java/org/fdroid/database/Migrations.kt b/libs/database/src/main/java/org/fdroid/database/Migrations.kt index 7fbaea3d4..972bc48d0 100644 --- a/libs/database/src/main/java/org/fdroid/database/Migrations.kt +++ b/libs/database/src/main/java/org/fdroid/database/Migrations.kt @@ -233,3 +233,16 @@ internal val MIGRATION_8_9 = db.execSQL("INSERT INTO AppMetadataFts(AppMetadataFts) VALUES('rebuild')") } } + +/** Raise the setup flag in the [DbMetadata] table, so we know that initial setup is done. */ +internal class DbMetadataMigration : AutoMigrationSpec { + override fun onPostMigrate(db: SupportSQLiteDatabase) { + db.beginTransaction() + try { + db.execSQL("INSERT INTO DbMetadata (key, value) VALUES ('setup', 'true')") + db.setTransactionSuccessful() + } finally { + db.endTransaction() + } + } +} From d6aa4a22978c7ec0dbc88ca6e07cbc788fe0ea4a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 27 Apr 2026 15:17:47 -0300 Subject: [PATCH 34/38] Allow a privileged system app to tell us to update all repos This is useful for a setup wizard app that wants to ensure the repos are loaded during setup, so a restore from backup can already link to our apps, and we will find them. --- app/src/main/AndroidManifest.xml | 17 ++++++++++++ .../org/fdroid/repo/RepoUpdateWorker.kt | 1 + .../ui/repositories/UpdateRepoActivity.kt | 27 +++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100644 app/src/main/kotlin/org/fdroid/ui/repositories/UpdateRepoActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 867162373..d6f6e0fc1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,12 @@ + + + + + + + + + + Date: Mon, 27 Apr 2026 16:24:53 -0300 Subject: [PATCH 35/38] Use UID for logcat if available to get logs from previous processes --- .../org/fdroid/ui/settings/SettingsViewModel.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt b/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt index e404f80bb..8b782a542 100644 --- a/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt +++ b/app/src/main/kotlin/org/fdroid/ui/settings/SettingsViewModel.kt @@ -2,7 +2,9 @@ package org.fdroid.ui.settings import android.app.Application import android.net.Uri +import android.os.Build.VERSION.SDK_INT import android.os.Process +import android.os.Process.myUid import android.widget.Toast import android.widget.Toast.LENGTH_SHORT import androidx.annotation.StringRes @@ -68,8 +70,14 @@ constructor( sendToast(R.string.export_log_error) return@launch } - // support for --pid was introduced in SDK 24 - val command = "logcat -d --pid=" + Process.myPid() + " *:V" + val command = if (SDK_INT < 30) { + // support for --pid was introduced in SDK 24 + "logcat -d --pid=${Process.myPid()} *:V" + } else { + // support for --uid was introduced in SDK 30 and is better, + // because it gives logs before process death + "logcat -d --uid=${myUid()} *:V" + } try { application.contentResolver.openOutputStream(uri, "wt")?.use { outputStream -> getRuntime().exec(command).inputStream.use { inputStream -> From 3a524603aeb916a4d144f6a11c3c72850b5bb288 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 28 Apr 2026 10:38:44 -0300 Subject: [PATCH 36/38] Fix PublicSourceDirProviderTest on older SDKs --- .../java/org/fdroid/fdroid/nearby/CopyUtils.kt | 12 ++++++++++++ .../fdroid/nearby/PublicSourceDirProviderTest.java | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt new file mode 100644 index 000000000..a185ed728 --- /dev/null +++ b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/CopyUtils.kt @@ -0,0 +1,12 @@ +package org.fdroid.fdroid.nearby + +import java.io.File +import java.io.InputStream + +object CopyUtils { + + @JvmStatic + fun copyInputStreamToFile(inputStream: InputStream, file: File) { + inputStream.use { input -> file.outputStream().use { output -> input.copyTo(output) } } + } +} diff --git a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java index 59d89f387..93c29bfe6 100644 --- a/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java +++ b/legacy/src/androidTestFull/java/org/fdroid/fdroid/nearby/PublicSourceDirProviderTest.java @@ -1,5 +1,6 @@ package org.fdroid.fdroid.nearby; +import static org.fdroid.fdroid.nearby.CopyUtils.copyInputStreamToFile; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; @@ -14,7 +15,6 @@ import android.provider.MediaStore; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; -import org.apache.commons.io.FileUtils; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -57,7 +57,8 @@ public class PublicSourceDirProviderTest { File f = null; is = context.getContentResolver().openInputStream(uri); f = File.createTempFile("received", ".apk"); - FileUtils.copyInputStreamToFile(is, f); + assertNotNull(is); + copyInputStreamToFile(is, f); assertTrue("dest file " + f + " should exist", f.exists()); assertEquals(f + " should equal " + apk, apk.length(), f.length()); f.delete(); From 27e79114fa4adc7ce019dcbda742eaa4f143a87d Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 27 Apr 2026 17:48:28 -0300 Subject: [PATCH 37/38] Release 2.0-alpha9 --- app/build.gradle.kts | 4 ++-- .../android/ar/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/be/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/cs/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/de/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/en-GB/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/en-US/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/eo/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/es/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/et/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/fr/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/ga/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/hu-HU/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/id/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/ja/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/nb/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/nl/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/pl/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/pt-BR/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/pt-PT/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/pt/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/ru/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/sv-SE/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/sv/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/sw/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/tr/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/uk/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/zh-CN/changelogs/{2000008.txt => 2000009.txt} | 0 .../android/zh-TW/changelogs/{2000008.txt => 2000009.txt} | 0 29 files changed, 2 insertions(+), 2 deletions(-) rename src/basic/fastlane/metadata/android/ar/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/be/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/cs/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/de/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/en-GB/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/en-US/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/eo/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/es/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/et/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/fr/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/ga/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/hu-HU/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/id/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/ja/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/nb/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/nl/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/pl/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/pt-BR/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/pt-PT/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/pt/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/ru/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/sv-SE/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/sv/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/sw/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/tr/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/uk/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/zh-CN/changelogs/{2000008.txt => 2000009.txt} (100%) rename src/basic/fastlane/metadata/android/zh-TW/changelogs/{2000008.txt => 2000009.txt} (100%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 697dbc602..9ef3aafd3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,8 +18,8 @@ android { applicationId = "org.fdroid" minSdk = 24 targetSdk = 36 - versionCode = 2000008 - versionName = "2.0-alpha8" + versionCode = 2000009 + versionName = "2.0-alpha9" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/src/basic/fastlane/metadata/android/ar/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ar/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ar/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/ar/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/be/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/be/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/be/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/be/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/cs/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/cs/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/cs/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/cs/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/de/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/de/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/de/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/de/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/en-GB/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/en-GB/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/en-GB/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/en-GB/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/en-US/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/en-US/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/en-US/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/en-US/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/eo/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/eo/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/eo/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/eo/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/es/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/es/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/es/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/es/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/et/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/et/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/et/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/et/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/fr/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/fr/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/fr/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/fr/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/ga/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ga/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ga/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/ga/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/hu-HU/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/hu-HU/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/hu-HU/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/hu-HU/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/id/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/id/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/id/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/id/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/ja/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ja/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ja/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/ja/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/nb/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/nb/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/nb/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/nl/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/nl/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/nl/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/pl/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pl/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pl/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/pl/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/pt-BR/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pt-BR/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pt-BR/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/pt-BR/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/pt-PT/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pt-PT/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pt-PT/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/pt-PT/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/pt/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/pt/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/pt/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/pt/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/ru/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ru/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/ru/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/sv-SE/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/sv-SE/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sv-SE/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/sv-SE/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/sv/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sv/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/sw/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/sw/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sw/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/sw/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/tr/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/tr/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/tr/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/tr/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/uk/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/uk/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/uk/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/uk/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/zh-CN/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/zh-CN/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/zh-CN/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/zh-CN/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/zh-TW/changelogs/2000008.txt b/src/basic/fastlane/metadata/android/zh-TW/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/zh-TW/changelogs/2000008.txt rename to src/basic/fastlane/metadata/android/zh-TW/changelogs/2000009.txt From d743d6e7fce0c3c9cb59828c5eb5d1d2fc8b082a Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Tue, 28 Apr 2026 09:31:10 -0300 Subject: [PATCH 38/38] Fix weblate warning: The component contains several translation files mapped to a single language in Weblate. Please fix this by removing one of the translation files. Please consider the following: Avoid having translation files for both the plain language code and its equivalent territory designation (for example de and de_DE). --- .../android/{fa-IR => fa}/full_description.txt | 0 .../android/{fa-IR => fa}/short_description.txt | 0 .../metadata/android/{fa-IR => fa}/title.txt | 0 .../android/{hu-HU => hu}/changelogs/2000009.txt | 0 .../android/{hu-HU => hu}/full_description.txt | 0 .../android/{hu-HU => hu}/short_description.txt | 0 .../metadata/android/{hu-HU => hu}/title.txt | 0 .../android/{is-IS => is}/full_description.txt | 0 .../android/{is-IS => is}/short_description.txt | 0 .../metadata/android/{is-IS => is}/title.txt | 0 .../android/sv-SE/changelogs/2000009.txt | 8 -------- .../metadata/android/sv-SE/short_description.txt | 1 - .../fastlane/metadata/android/sv-SE/title.txt | 1 - .../metadata/android/sv/changelogs/2000009.txt | 16 ++++++++-------- .../android/{sv-SE => sv}/full_description.txt | 0 .../metadata/android/sv/short_description.txt | 2 +- src/basic/fastlane/metadata/android/sv/title.txt | 2 +- .../android/{ta-IN => ta}/full_description.txt | 0 .../android/{ta-IN => ta}/short_description.txt | 0 .../metadata/android/{ta-IN => ta}/title.txt | 0 20 files changed, 10 insertions(+), 20 deletions(-) rename src/basic/fastlane/metadata/android/{fa-IR => fa}/full_description.txt (100%) rename src/basic/fastlane/metadata/android/{fa-IR => fa}/short_description.txt (100%) rename src/basic/fastlane/metadata/android/{fa-IR => fa}/title.txt (100%) rename src/basic/fastlane/metadata/android/{hu-HU => hu}/changelogs/2000009.txt (100%) rename src/basic/fastlane/metadata/android/{hu-HU => hu}/full_description.txt (100%) rename src/basic/fastlane/metadata/android/{hu-HU => hu}/short_description.txt (100%) rename src/basic/fastlane/metadata/android/{hu-HU => hu}/title.txt (100%) rename src/basic/fastlane/metadata/android/{is-IS => is}/full_description.txt (100%) rename src/basic/fastlane/metadata/android/{is-IS => is}/short_description.txt (100%) rename src/basic/fastlane/metadata/android/{is-IS => is}/title.txt (100%) delete mode 100644 src/basic/fastlane/metadata/android/sv-SE/changelogs/2000009.txt delete mode 100644 src/basic/fastlane/metadata/android/sv-SE/short_description.txt delete mode 100644 src/basic/fastlane/metadata/android/sv-SE/title.txt rename src/basic/fastlane/metadata/android/{sv-SE => sv}/full_description.txt (100%) rename src/basic/fastlane/metadata/android/{ta-IN => ta}/full_description.txt (100%) rename src/basic/fastlane/metadata/android/{ta-IN => ta}/short_description.txt (100%) rename src/basic/fastlane/metadata/android/{ta-IN => ta}/title.txt (100%) diff --git a/src/basic/fastlane/metadata/android/fa-IR/full_description.txt b/src/basic/fastlane/metadata/android/fa/full_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/fa-IR/full_description.txt rename to src/basic/fastlane/metadata/android/fa/full_description.txt diff --git a/src/basic/fastlane/metadata/android/fa-IR/short_description.txt b/src/basic/fastlane/metadata/android/fa/short_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/fa-IR/short_description.txt rename to src/basic/fastlane/metadata/android/fa/short_description.txt diff --git a/src/basic/fastlane/metadata/android/fa-IR/title.txt b/src/basic/fastlane/metadata/android/fa/title.txt similarity index 100% rename from src/basic/fastlane/metadata/android/fa-IR/title.txt rename to src/basic/fastlane/metadata/android/fa/title.txt diff --git a/src/basic/fastlane/metadata/android/hu-HU/changelogs/2000009.txt b/src/basic/fastlane/metadata/android/hu/changelogs/2000009.txt similarity index 100% rename from src/basic/fastlane/metadata/android/hu-HU/changelogs/2000009.txt rename to src/basic/fastlane/metadata/android/hu/changelogs/2000009.txt diff --git a/src/basic/fastlane/metadata/android/hu-HU/full_description.txt b/src/basic/fastlane/metadata/android/hu/full_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/hu-HU/full_description.txt rename to src/basic/fastlane/metadata/android/hu/full_description.txt diff --git a/src/basic/fastlane/metadata/android/hu-HU/short_description.txt b/src/basic/fastlane/metadata/android/hu/short_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/hu-HU/short_description.txt rename to src/basic/fastlane/metadata/android/hu/short_description.txt diff --git a/src/basic/fastlane/metadata/android/hu-HU/title.txt b/src/basic/fastlane/metadata/android/hu/title.txt similarity index 100% rename from src/basic/fastlane/metadata/android/hu-HU/title.txt rename to src/basic/fastlane/metadata/android/hu/title.txt diff --git a/src/basic/fastlane/metadata/android/is-IS/full_description.txt b/src/basic/fastlane/metadata/android/is/full_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/is-IS/full_description.txt rename to src/basic/fastlane/metadata/android/is/full_description.txt diff --git a/src/basic/fastlane/metadata/android/is-IS/short_description.txt b/src/basic/fastlane/metadata/android/is/short_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/is-IS/short_description.txt rename to src/basic/fastlane/metadata/android/is/short_description.txt diff --git a/src/basic/fastlane/metadata/android/is-IS/title.txt b/src/basic/fastlane/metadata/android/is/title.txt similarity index 100% rename from src/basic/fastlane/metadata/android/is-IS/title.txt rename to src/basic/fastlane/metadata/android/is/title.txt diff --git a/src/basic/fastlane/metadata/android/sv-SE/changelogs/2000009.txt b/src/basic/fastlane/metadata/android/sv-SE/changelogs/2000009.txt deleted file mode 100644 index 3da80645c..000000000 --- a/src/basic/fastlane/metadata/android/sv-SE/changelogs/2000009.txt +++ /dev/null @@ -1,8 +0,0 @@ -* Användargränssnittet är omskrivet från grunden med Kotlin Compose -* Förbättrad sökning, även sökning i beskrivningar och översättningar -* Enklare att upptäcka nya appar, även markering av de mest nedladdade -* Godkännande av installation *före* nedladdning -* Flera uppdateringar/nedladdningar samtidigt -* Meddelande till användaren om problem med appar (t.ex. ändrad signeringsnyckel) -* Valfritt Material You-färgtema -* Förbättrad filtrering av listor diff --git a/src/basic/fastlane/metadata/android/sv-SE/short_description.txt b/src/basic/fastlane/metadata/android/sv-SE/short_description.txt deleted file mode 100644 index 879b0dca3..000000000 --- a/src/basic/fastlane/metadata/android/sv-SE/short_description.txt +++ /dev/null @@ -1 +0,0 @@ -Den minimala klient-appen för app-butiken som respekterar frihet och privatliv diff --git a/src/basic/fastlane/metadata/android/sv-SE/title.txt b/src/basic/fastlane/metadata/android/sv-SE/title.txt deleted file mode 100644 index 996a1c6b2..000000000 --- a/src/basic/fastlane/metadata/android/sv-SE/title.txt +++ /dev/null @@ -1 +0,0 @@ -F-Droid Basic diff --git a/src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt b/src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt index 898797168..3da80645c 100644 --- a/src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt +++ b/src/basic/fastlane/metadata/android/sv/changelogs/2000009.txt @@ -1,8 +1,8 @@ -* Användargränssnittet har skrivits om från grunden med Kotlin Compose -* förbättrad sök, som nu även söker i beskrivningar och översättningar -* enklare att upptäcka nya appar, med markering av de mest nedladdade -* godkännande av installation *innan* nedladdning -* flera uppdateringar/nedladdningar samtidigt -* meddelanden till användaren om problem med appar (t.ex. ändrad signeringsnyckel) -* valfritt färgtema i Material You -* förbättrad filtrering av listor +* Användargränssnittet är omskrivet från grunden med Kotlin Compose +* Förbättrad sökning, även sökning i beskrivningar och översättningar +* Enklare att upptäcka nya appar, även markering av de mest nedladdade +* Godkännande av installation *före* nedladdning +* Flera uppdateringar/nedladdningar samtidigt +* Meddelande till användaren om problem med appar (t.ex. ändrad signeringsnyckel) +* Valfritt Material You-färgtema +* Förbättrad filtrering av listor diff --git a/src/basic/fastlane/metadata/android/sv-SE/full_description.txt b/src/basic/fastlane/metadata/android/sv/full_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/sv-SE/full_description.txt rename to src/basic/fastlane/metadata/android/sv/full_description.txt diff --git a/src/basic/fastlane/metadata/android/sv/short_description.txt b/src/basic/fastlane/metadata/android/sv/short_description.txt index 40e8147f6..879b0dca3 100644 --- a/src/basic/fastlane/metadata/android/sv/short_description.txt +++ b/src/basic/fastlane/metadata/android/sv/short_description.txt @@ -1 +1 @@ -Den minimalistiska klient-appen för app-butiken som respekterar frihet och privatliv +Den minimala klient-appen för app-butiken som respekterar frihet och privatliv diff --git a/src/basic/fastlane/metadata/android/sv/title.txt b/src/basic/fastlane/metadata/android/sv/title.txt index a97be6208..996a1c6b2 100644 --- a/src/basic/fastlane/metadata/android/sv/title.txt +++ b/src/basic/fastlane/metadata/android/sv/title.txt @@ -1 +1 @@ -Grundläggande F-Droid +F-Droid Basic diff --git a/src/basic/fastlane/metadata/android/ta-IN/full_description.txt b/src/basic/fastlane/metadata/android/ta/full_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ta-IN/full_description.txt rename to src/basic/fastlane/metadata/android/ta/full_description.txt diff --git a/src/basic/fastlane/metadata/android/ta-IN/short_description.txt b/src/basic/fastlane/metadata/android/ta/short_description.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ta-IN/short_description.txt rename to src/basic/fastlane/metadata/android/ta/short_description.txt diff --git a/src/basic/fastlane/metadata/android/ta-IN/title.txt b/src/basic/fastlane/metadata/android/ta/title.txt similarity index 100% rename from src/basic/fastlane/metadata/android/ta-IN/title.txt rename to src/basic/fastlane/metadata/android/ta/title.txt