mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-02-13 16:42:09 -05:00
Merge branch '2.0' into 'master'
Fixes for 2.0-alpha2 Closes #3188, #3186, #3185, #3184, #3182, and #3178 See merge request fdroid/fdroidclient!1618
This commit is contained in:
@@ -17,8 +17,8 @@ android {
|
||||
applicationId = "org.fdroid"
|
||||
minSdk = 24
|
||||
targetSdk = 36
|
||||
versionCode = 2000001
|
||||
versionName = "2.0-alpha1"
|
||||
versionCode = 2000002
|
||||
versionName = "2.0-alpha2"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -10,19 +10,23 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import org.fdroid.settings.SettingsConstants.AutoUpdateValues
|
||||
import org.fdroid.settings.SettingsManager
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NetworkMonitor @Inject constructor(
|
||||
@param:ApplicationContext private val context: Context,
|
||||
private val settingsManager: SettingsManager,
|
||||
) : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
private val connectivityManager =
|
||||
context.getSystemService(ConnectivityManager::class.java) as ConnectivityManager
|
||||
private val neverMetered get() = settingsManager.autoUpdateApps == AutoUpdateValues.Always
|
||||
private val _networkState = MutableStateFlow(
|
||||
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)?.let {
|
||||
NetworkState(it)
|
||||
NetworkState(it, neverMetered)
|
||||
} ?: NetworkState(isOnline = false, isMetered = false)
|
||||
)
|
||||
val networkState = _networkState.asStateFlow()
|
||||
@@ -37,7 +41,7 @@ class NetworkMonitor @Inject constructor(
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(network: Network, networkCapabilities: NetworkCapabilities) {
|
||||
_networkState.update { NetworkState(networkCapabilities) }
|
||||
_networkState.update { NetworkState(networkCapabilities, neverMetered) }
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
@@ -49,8 +53,9 @@ data class NetworkState(
|
||||
val isOnline: Boolean,
|
||||
val isMetered: Boolean,
|
||||
) {
|
||||
constructor(networkCapabilities: NetworkCapabilities) : this(
|
||||
constructor(networkCapabilities: NetworkCapabilities, neverMetered: Boolean) : this(
|
||||
isOnline = networkCapabilities.hasCapability(NET_CAPABILITY_INTERNET),
|
||||
isMetered = !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED),
|
||||
isMetered = if (neverMetered) false
|
||||
else !networkCapabilities.hasCapability(NET_CAPABILITY_NOT_METERED),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ class OnboardingManager @Inject constructor(
|
||||
const val KEY_FILTER = "appFilter"
|
||||
const val KEY_REPO_LIST = "repoList"
|
||||
const val KEY_REPO_DETAILS = "repoDetails"
|
||||
const val KEY_APP_ISSUE_HINT = "appIssueHint"
|
||||
}
|
||||
|
||||
private val prefs = context.getSharedPreferences("onboarding", MODE_PRIVATE)
|
||||
@@ -34,6 +35,9 @@ class OnboardingManager @Inject constructor(
|
||||
private val _showRepoDetailsOnboarding = Onboarding(KEY_REPO_DETAILS, prefs)
|
||||
val showRepoDetailsOnboarding = _showRepoDetailsOnboarding.flow
|
||||
|
||||
private val _showAppIssueHint = Onboarding(KEY_APP_ISSUE_HINT, prefs)
|
||||
val showAppIssueHint = _showAppIssueHint.flow
|
||||
|
||||
fun onFilterOnboardingSeen() {
|
||||
_showFilterOnboarding.onSeen(prefs)
|
||||
}
|
||||
@@ -45,6 +49,10 @@ class OnboardingManager @Inject constructor(
|
||||
fun onRepoDetailsOnboardingSeen() {
|
||||
_showRepoDetailsOnboarding.onSeen(prefs)
|
||||
}
|
||||
|
||||
fun onAppIssueHintSeen() {
|
||||
_showAppIssueHint.onSeen(prefs)
|
||||
}
|
||||
}
|
||||
|
||||
private data class Onboarding(
|
||||
|
||||
@@ -2,9 +2,9 @@ package org.fdroid.settings
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.core.content.edit
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.ktor.client.engine.ProxyBuilder
|
||||
import io.ktor.client.engine.ProxyConfig
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -35,6 +35,8 @@ import org.fdroid.settings.SettingsConstants.PREF_KEY_SHOW_INCOMPATIBLE
|
||||
import org.fdroid.settings.SettingsConstants.PREF_KEY_THEME
|
||||
import org.fdroid.settings.SettingsConstants.getAppListSortOrder
|
||||
import org.fdroid.settings.SettingsConstants.toSettings
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@@ -112,12 +114,15 @@ class SettingsManager @Inject constructor(
|
||||
}
|
||||
|
||||
val proxyConfig: ProxyConfig?
|
||||
@UiThread
|
||||
get() {
|
||||
val proxyStr = prefs.getString(PREF_KEY_PROXY, PREF_DEFAULT_PROXY)
|
||||
return if (proxyStr.isNullOrBlank()) null
|
||||
else {
|
||||
val (host, port) = proxyStr.split(':')
|
||||
ProxyBuilder.socks(host, port.toInt())
|
||||
// don't resolve hostname here, or we get NetworkOnMainThreadException
|
||||
val address = InetSocketAddress.createUnresolved(host, port.toInt())
|
||||
Proxy(Proxy.Type.SOCKS, address)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,9 @@ import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.R
|
||||
import org.fdroid.database.AppIssue
|
||||
import org.fdroid.database.NoCompatibleSigner
|
||||
import org.fdroid.database.UpdateInOtherRepo
|
||||
import org.fdroid.ui.FDroidContent
|
||||
import org.fdroid.ui.utils.AsyncShimmerImage
|
||||
import org.fdroid.ui.utils.BadgeIcon
|
||||
@@ -29,15 +32,19 @@ fun InstalledAppRow(
|
||||
app: MyInstalledAppItem,
|
||||
isSelected: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
hasIssue: Boolean = false,
|
||||
appIssue: AppIssue? = null,
|
||||
) {
|
||||
Column(modifier = modifier) {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
BadgedBox(badge = {
|
||||
if (hasIssue) BadgeIcon(
|
||||
if (appIssue != null) BadgeIcon(
|
||||
icon = Icons.Filled.Error,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
color = if (appIssue is UpdateInOtherRepo) {
|
||||
MaterialTheme.colorScheme.inverseSurface
|
||||
} else {
|
||||
MaterialTheme.colorScheme.error
|
||||
},
|
||||
contentDescription =
|
||||
stringResource(R.string.my_apps_header_apps_with_issue),
|
||||
)
|
||||
@@ -83,7 +90,8 @@ fun InstalledAppRowPreview() {
|
||||
Column {
|
||||
InstalledAppRow(app, false)
|
||||
InstalledAppRow(app, true)
|
||||
InstalledAppRow(app, false, hasIssue = true)
|
||||
InstalledAppRow(app, false, appIssue = UpdateInOtherRepo(2L))
|
||||
InstalledAppRow(app, false, appIssue = NoCompatibleSigner())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ fun MyApps(
|
||||
val app = myAppsModel.appToConfirm
|
||||
if (app != null && lifecycleOwner.lifecycle.currentState.isAtLeast(STARTED)) {
|
||||
val state = app.installState as InstallConfirmationState
|
||||
myAppsInfo.confirmAppInstall(app.packageName, state)
|
||||
myAppsInfo.actions.confirmAppInstall(app.packageName, state)
|
||||
}
|
||||
}
|
||||
val installingApps = myAppsModel.installingApps
|
||||
@@ -74,7 +74,7 @@ fun MyApps(
|
||||
val installedApps = myAppsModel.installedApps
|
||||
val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState())
|
||||
var searchActive by rememberSaveable { mutableStateOf(false) }
|
||||
val onSearchCleared = { myAppsInfo.search("") }
|
||||
val onSearchCleared = { myAppsInfo.actions.search("") }
|
||||
// when search bar is shown, back button closes it again
|
||||
BackHandler(enabled = searchActive) {
|
||||
searchActive = false
|
||||
@@ -84,7 +84,10 @@ fun MyApps(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (searchActive) {
|
||||
TopSearchBar(onSearch = myAppsInfo::search, onSearchCleared = onSearchCleared) {
|
||||
TopSearchBar(
|
||||
onSearch = myAppsInfo.actions::search,
|
||||
onSearchCleared = onSearchCleared,
|
||||
) {
|
||||
onBackPressedDispatcher?.onBackPressed()
|
||||
}
|
||||
} else TopAppBar(
|
||||
@@ -121,7 +124,7 @@ fun MyApps(
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
myAppsInfo.changeSortOrder(AppListSortOrder.NAME)
|
||||
myAppsInfo.actions.changeSortOrder(AppListSortOrder.NAME)
|
||||
sortByMenuExpanded = false
|
||||
},
|
||||
)
|
||||
@@ -137,7 +140,7 @@ fun MyApps(
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
myAppsInfo.changeSortOrder(LAST_UPDATED)
|
||||
myAppsInfo.actions.changeSortOrder(LAST_UPDATED)
|
||||
sortByMenuExpanded = false
|
||||
},
|
||||
)
|
||||
@@ -186,6 +189,7 @@ fun MyAppsLoadingPreview() {
|
||||
installingApps = emptyList(),
|
||||
appUpdates = null,
|
||||
installedApps = null,
|
||||
showAppIssueHint = false,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
)
|
||||
@@ -210,3 +214,24 @@ fun MyAppsPreview() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@RestrictTo(RestrictTo.Scope.TESTS)
|
||||
fun MyAppsEmptyPreview() {
|
||||
FDroidContent {
|
||||
val model = MyAppsModel(
|
||||
installingApps = emptyList(),
|
||||
appUpdates = emptyList(),
|
||||
installedApps = emptyList(),
|
||||
showAppIssueHint = false,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
)
|
||||
MyApps(
|
||||
myAppsInfo = getMyAppsInfo(model),
|
||||
currentPackageName = null,
|
||||
onAppItemClick = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.runtime.EntryProviderScope
|
||||
import androidx.navigation3.runtime.NavKey
|
||||
import org.fdroid.database.AppListSortOrder
|
||||
import org.fdroid.install.InstallConfirmationState
|
||||
import org.fdroid.ui.navigation.NavigationKey
|
||||
import org.fdroid.ui.navigation.Navigator
|
||||
|
||||
@@ -22,19 +20,7 @@ fun EntryProviderScope<NavKey>.myAppsEntry(
|
||||
val myAppsViewModel = hiltViewModel<MyAppsViewModel>()
|
||||
val myAppsInfo = object : MyAppsInfo {
|
||||
override val model = myAppsViewModel.myAppsModel.collectAsStateWithLifecycle().value
|
||||
|
||||
override fun updateAll() = myAppsViewModel.updateAll()
|
||||
override fun changeSortOrder(sort: AppListSortOrder) =
|
||||
myAppsViewModel.changeSortOrder(sort)
|
||||
|
||||
override fun search(query: String) = myAppsViewModel.search(query)
|
||||
override fun confirmAppInstall(
|
||||
packageName: String,
|
||||
state: InstallConfirmationState,
|
||||
) = myAppsViewModel.confirmAppInstall(packageName, state)
|
||||
|
||||
override fun ignoreAppIssue(item: AppWithIssueItem) =
|
||||
myAppsViewModel.ignoreAppIssue(item)
|
||||
override val actions: MyAppsActions = myAppsViewModel
|
||||
}
|
||||
MyApps(
|
||||
myAppsInfo = myAppsInfo,
|
||||
|
||||
@@ -6,11 +6,7 @@ import org.fdroid.install.InstallConfirmationState
|
||||
|
||||
interface MyAppsInfo {
|
||||
val model: MyAppsModel
|
||||
fun updateAll()
|
||||
fun changeSortOrder(sort: AppListSortOrder)
|
||||
fun search(query: String)
|
||||
fun confirmAppInstall(packageName: String, state: InstallConfirmationState)
|
||||
fun ignoreAppIssue(item: AppWithIssueItem)
|
||||
val actions: MyAppsActions
|
||||
}
|
||||
|
||||
data class MyAppsModel(
|
||||
@@ -19,7 +15,17 @@ data class MyAppsModel(
|
||||
val installingApps: List<InstallingAppItem>,
|
||||
val appsWithIssue: List<AppWithIssueItem>? = null,
|
||||
val installedApps: List<InstalledAppItem>? = null,
|
||||
val showAppIssueHint: Boolean,
|
||||
val sortOrder: AppListSortOrder = AppListSortOrder.NAME,
|
||||
val networkState: NetworkState,
|
||||
val appUpdatesBytes: Long? = null,
|
||||
)
|
||||
|
||||
interface MyAppsActions {
|
||||
fun updateAll()
|
||||
fun changeSortOrder(sort: AppListSortOrder)
|
||||
fun search(query: String)
|
||||
fun confirmAppInstall(packageName: String, state: InstallConfirmationState)
|
||||
fun ignoreAppIssue(item: AppWithIssueItem)
|
||||
fun onAppIssueHintSeen()
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ package org.fdroid.ui.apps
|
||||
import androidx.annotation.RestrictTo
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
@@ -12,10 +15,14 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.selection.selectableGroup
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -45,6 +52,16 @@ fun MyAppsList(
|
||||
val installingApps = myAppsInfo.model.installingApps
|
||||
val appsWithIssue = myAppsInfo.model.appsWithIssue
|
||||
val installedApps = myAppsInfo.model.installedApps
|
||||
// scroll to top if new updatable apps were added
|
||||
var previousNumUpdates by remember { mutableIntStateOf(0) }
|
||||
LaunchedEffect(updatableApps) {
|
||||
if (updatableApps != null && updatableApps.isNotEmpty()) {
|
||||
if (updatableApps.size > previousNumUpdates) {
|
||||
lazyListState.animateScrollToItem(0)
|
||||
}
|
||||
previousNumUpdates = updatableApps.size
|
||||
}
|
||||
}
|
||||
// allow us to hide "update all" button to avoid user pressing it twice
|
||||
var showUpdateAllButton by remember(updatableApps) {
|
||||
mutableStateOf(true)
|
||||
@@ -79,7 +96,7 @@ fun MyAppsList(
|
||||
if (showUpdateAllButton) Button(
|
||||
onClick = {
|
||||
val installLambda = {
|
||||
myAppsInfo.updateAll()
|
||||
myAppsInfo.actions.updateAll()
|
||||
showUpdateAllButton = false
|
||||
}
|
||||
if (myAppsInfo.model.networkState.isMetered) {
|
||||
@@ -160,6 +177,30 @@ fun MyAppsList(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
)
|
||||
}
|
||||
if (myAppsInfo.model.showAppIssueHint) item(key = "C-hint", contentType = "hint") {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 16.dp, start = 16.dp, end = 16.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.app_issue_hint),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
TextButton(onClick = { myAppsInfo.actions.onAppIssueHintSeen() }) {
|
||||
Text(stringResource(R.string.got_it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// list of apps with issues
|
||||
items(
|
||||
items = appsWithIssue,
|
||||
@@ -190,13 +231,13 @@ fun MyAppsList(
|
||||
val modifier = Modifier
|
||||
.animateItem()
|
||||
.then(interactionModifier)
|
||||
InstalledAppRow(app, isSelected, modifier, hasIssue = true)
|
||||
InstalledAppRow(app, isSelected, modifier, app.issue)
|
||||
// Dialogs
|
||||
val appToIgnore = showIssueIgnoreDialog
|
||||
if (appToIgnore != null) IgnoreIssueDialog(
|
||||
appName = appToIgnore.name,
|
||||
onIgnore = {
|
||||
myAppsInfo.ignoreAppIssue(appToIgnore)
|
||||
myAppsInfo.actions.ignoreAppIssue(appToIgnore)
|
||||
showIssueIgnoreDialog = null
|
||||
},
|
||||
onDismiss = { showIssueIgnoreDialog = null },
|
||||
|
||||
@@ -22,6 +22,7 @@ fun MyAppsPresenter(
|
||||
appInstallStatesFlow: StateFlow<Map<String, InstallState>>,
|
||||
appsWithIssuesFlow: StateFlow<List<AppWithIssueItem>?>,
|
||||
installedAppsFlow: Flow<List<InstalledAppItem>>,
|
||||
showAppIssueHintFlow: StateFlow<Boolean>,
|
||||
searchQueryFlow: StateFlow<String>,
|
||||
sortOrderFlow: StateFlow<AppListSortOrder>,
|
||||
networkStateFlow: StateFlow<NetworkState>,
|
||||
@@ -97,6 +98,7 @@ fun MyAppsPresenter(
|
||||
appUpdates = updates?.sort(sortOrder),
|
||||
appsWithIssue = withIssues?.sort(sortOrder),
|
||||
installedApps = installed?.sort(sortOrder),
|
||||
showAppIssueHint = showAppIssueHintFlow.collectAsState().value,
|
||||
sortOrder = sortOrder,
|
||||
networkState = networkStateFlow.collectAsState().value,
|
||||
appUpdatesBytes = updateBytes,
|
||||
|
||||
@@ -28,6 +28,7 @@ import org.fdroid.install.AppInstallManager
|
||||
import org.fdroid.install.InstallConfirmationState
|
||||
import org.fdroid.install.InstallState
|
||||
import org.fdroid.install.InstalledAppsCache
|
||||
import org.fdroid.settings.OnboardingManager
|
||||
import org.fdroid.settings.SettingsManager
|
||||
import org.fdroid.updates.UpdatesManager
|
||||
import org.fdroid.utils.IoDispatcher
|
||||
@@ -40,12 +41,13 @@ class MyAppsViewModel @Inject constructor(
|
||||
savedStateHandle: SavedStateHandle,
|
||||
private val db: FDroidDatabase,
|
||||
private val settingsManager: SettingsManager,
|
||||
private val installedAppsCache: InstalledAppsCache,
|
||||
installedAppsCache: InstalledAppsCache,
|
||||
private val onboardingManager: OnboardingManager,
|
||||
private val appInstallManager: AppInstallManager,
|
||||
private val networkMonitor: NetworkMonitor,
|
||||
private val updatesManager: UpdatesManager,
|
||||
private val repoManager: RepoManager,
|
||||
) : AndroidViewModel(app) {
|
||||
) : AndroidViewModel(app), MyAppsActions {
|
||||
|
||||
private val log = KotlinLogging.logger { }
|
||||
private val localeList = LocaleListCompat.getDefault()
|
||||
@@ -83,6 +85,7 @@ class MyAppsViewModel @Inject constructor(
|
||||
appInstallStatesFlow = appInstallManager.appInstallStates,
|
||||
appsWithIssuesFlow = updatesManager.appsWithIssues,
|
||||
installedAppsFlow = installedAppItems,
|
||||
showAppIssueHintFlow = onboardingManager.showAppIssueHint,
|
||||
searchQueryFlow = searchQuery,
|
||||
sortOrderFlow = sortOrder,
|
||||
networkStateFlow = networkMonitor.networkState,
|
||||
@@ -90,21 +93,21 @@ class MyAppsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun updateAll() {
|
||||
override fun updateAll() {
|
||||
scope.launch {
|
||||
updatesManager.updateAll(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
override fun search(query: String) {
|
||||
searchQuery.value = query
|
||||
}
|
||||
|
||||
fun changeSortOrder(sort: AppListSortOrder) {
|
||||
override fun changeSortOrder(sort: AppListSortOrder) {
|
||||
sortOrder.value = sort
|
||||
}
|
||||
|
||||
fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {
|
||||
override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {
|
||||
log.info { "Asking user to confirm install of $packageName..." }
|
||||
scope.launch(Dispatchers.Main) {
|
||||
when (state) {
|
||||
@@ -118,8 +121,10 @@ class MyAppsViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun ignoreAppIssue(item: AppWithIssueItem) {
|
||||
override fun ignoreAppIssue(item: AppWithIssueItem) {
|
||||
settingsManager.ignoreAppIssue(item.packageName, item.installedVersionCode)
|
||||
updatesManager.loadUpdates()
|
||||
}
|
||||
|
||||
override fun onAppIssueHintSeen() = onboardingManager.onAppIssueHintSeen()
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.LiveRegionMode
|
||||
@@ -30,6 +31,7 @@ import androidx.compose.ui.semantics.hideFromAccessibility
|
||||
import androidx.compose.ui.semantics.liveRegion
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.fdroid.R
|
||||
import org.fdroid.ui.FDroidContent
|
||||
@@ -75,7 +77,12 @@ fun UpdatableAppRow(
|
||||
val size = app.update.size?.let {
|
||||
Formatter.formatFileSize(LocalContext.current, it)
|
||||
}
|
||||
Text("${app.installedVersionName} → ${app.update.versionName} • $size")
|
||||
val text = if (LocalLayoutDirection.current == LayoutDirection.Ltr) {
|
||||
"${app.installedVersionName} → ${app.update.versionName} • $size"
|
||||
} else {
|
||||
"$size • ${app.update.versionName} ← ${app.installedVersionName}"
|
||||
}
|
||||
Text(text)
|
||||
},
|
||||
trailingContent = {
|
||||
if (app.whatsNew != null) IconButton(onClick = { isExpanded = !isExpanded }) {
|
||||
@@ -135,3 +142,21 @@ fun UpdatableAppRowPreview() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(locale = "fa")
|
||||
@Composable
|
||||
private fun UpdatableAppRowRtl() {
|
||||
val app1 = AppUpdateItem(
|
||||
repoId = 1,
|
||||
packageName = "A",
|
||||
name = "App Update 123",
|
||||
installedVersionName = "1.0.1",
|
||||
update = getPreviewVersion("1.1.0", 123456789),
|
||||
whatsNew = "This is new, all is new, nothing old.",
|
||||
)
|
||||
FDroidContent {
|
||||
Column {
|
||||
UpdatableAppRow(app1, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +97,9 @@ private fun Screenshots(phoneScreenshots: List<Any>) {
|
||||
pageCount = { phoneScreenshots.size },
|
||||
)
|
||||
Surface {
|
||||
HorizontalPager(state = pagerState) { page ->
|
||||
// The overscrollEffect was bouncing screenshots with each swipe.
|
||||
// Maybe this was a bug and overscroll effect can be enabled again once fixed.
|
||||
HorizontalPager(state = pagerState, overscrollEffect = null) { page ->
|
||||
AsyncShimmerImage(
|
||||
model = phoneScreenshots[page],
|
||||
contentDescription = "",
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.fdroid.ui.discover
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
@@ -61,9 +62,10 @@ fun Discover(
|
||||
BadgedBox(badge = {
|
||||
val hasRepoIssues =
|
||||
(discoverModel as? LoadedDiscoverModel)?.hasRepoIssues == true
|
||||
if (dest.id == NavigationKey.Repos && hasRepoIssues) Badge {
|
||||
Text("")
|
||||
}
|
||||
if (dest.id == NavigationKey.Repos && hasRepoIssues) Badge(
|
||||
content = null,
|
||||
modifier = Modifier.size(8.dp)
|
||||
)
|
||||
}) {
|
||||
IconButton(onClick = { onNav(dest.id) }) {
|
||||
Icon(
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
package org.fdroid.ui.lists
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -15,6 +16,7 @@ import java.util.Locale
|
||||
|
||||
@Composable
|
||||
fun AppListPresenter(
|
||||
type: AppListType,
|
||||
appsFlow: StateFlow<List<AppListItem>?>,
|
||||
sortByFlow: StateFlow<AppListSortOrder>,
|
||||
filterIncompatibleFlow: StateFlow<Boolean>,
|
||||
@@ -40,7 +42,12 @@ fun AppListPresenter(
|
||||
}
|
||||
val filteredCategories = remember(categories, apps) {
|
||||
categories?.filter {
|
||||
it.id in availableCategoryIds
|
||||
if (type is AppListType.Category) {
|
||||
// don't show category for list we are currently seeing, because all apps are in it
|
||||
it.id in availableCategoryIds && it.id != type.categoryId
|
||||
} else {
|
||||
it.id in availableCategoryIds
|
||||
}
|
||||
}
|
||||
}
|
||||
val availableRepositories = remember(apps) {
|
||||
@@ -61,6 +68,8 @@ fun AppListPresenter(
|
||||
val matchesCompatibility = !filterIncompatible || it.isCompatible
|
||||
matchesCategories && matchesRepos && matchesQuery && matchesCompatibility
|
||||
}
|
||||
|
||||
@SuppressLint("NonObservableLocale") // the alternative isn't available here
|
||||
val locale = Locale.getDefault()
|
||||
return AppListModel(
|
||||
apps = if (sortBy == AppListSortOrder.NAME) {
|
||||
|
||||
@@ -86,6 +86,7 @@ class AppListViewModel @AssistedInject constructor(
|
||||
val appListModel: StateFlow<AppListModel> by lazy(LazyThreadSafetyMode.NONE) {
|
||||
moleculeScope.launchMolecule(mode = ContextClock) {
|
||||
AppListPresenter(
|
||||
type = type,
|
||||
appsFlow = apps,
|
||||
sortByFlow = sortBy,
|
||||
filterIncompatibleFlow = filterIncompatible,
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.fdroid.ui.apps.AppUpdateItem
|
||||
import org.fdroid.ui.apps.AppWithIssueItem
|
||||
import org.fdroid.ui.apps.InstalledAppItem
|
||||
import org.fdroid.ui.apps.InstallingAppItem
|
||||
import org.fdroid.ui.apps.MyAppsActions
|
||||
import org.fdroid.ui.apps.MyAppsInfo
|
||||
import org.fdroid.ui.apps.MyAppsModel
|
||||
import org.fdroid.ui.categories.CategoryItem
|
||||
@@ -268,11 +269,15 @@ fun getAppListInfo(model: AppListModel) = object : AppListInfo {
|
||||
|
||||
fun getMyAppsInfo(model: MyAppsModel): MyAppsInfo = object : MyAppsInfo {
|
||||
override val model = model
|
||||
override fun updateAll() {}
|
||||
override fun changeSortOrder(sort: AppListSortOrder) {}
|
||||
override fun search(query: String) {}
|
||||
override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {}
|
||||
override fun ignoreAppIssue(item: AppWithIssueItem) {}
|
||||
override val actions: MyAppsActions
|
||||
get() = object : MyAppsActions {
|
||||
override fun updateAll() {}
|
||||
override fun changeSortOrder(sort: AppListSortOrder) {}
|
||||
override fun search(query: String) {}
|
||||
override fun confirmAppInstall(packageName: String, state: InstallConfirmationState) {}
|
||||
override fun ignoreAppIssue(item: AppWithIssueItem) {}
|
||||
override fun onAppIssueHintSeen() {}
|
||||
}
|
||||
}
|
||||
|
||||
@RestrictTo(RestrictTo.Scope.TESTS)
|
||||
@@ -348,6 +353,7 @@ internal val myAppsModel = MyAppsModel(
|
||||
lastUpdated = System.currentTimeMillis() - DAYS.toMillis(3)
|
||||
)
|
||||
),
|
||||
showAppIssueHint = true,
|
||||
sortOrder = AppListSortOrder.NAME,
|
||||
networkState = NetworkState(isOnline = false, isMetered = false),
|
||||
appUpdatesBytes = null,
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
<vector android:height="24dp"
|
||||
android:viewportHeight="48.0"
|
||||
android:tint="?android:attr/colorControlNormal"
|
||||
android:viewportWidth="48.0"
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
android:height="24dp"
|
||||
android:tint="#FFFFFFFF"
|
||||
android:viewportWidth="48.0"
|
||||
android:viewportHeight="48.0">
|
||||
<path
|
||||
android:fillAlpha="1"
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="m2.61,1.25c-0.45,0.01 -0.76,0.19 -0.98,0.45 0,0 0,0 0.01,0.01 -0,0 -0.01,0.01 -0.01,0.01 -0,-0 -0,-0 -0,-0.01 -0,0 -0.01,0.02 -0.02,0.03 -0.01,0.01 -0.01,0.02 -0.02,0.03 -0.15,0.2 -0.49,0.8 0.01,1.5L5.21,7.93C5.08,8.26 5,8.62 5,9v3,1 2.46L5,16c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,15.46 43,9.54 43,9C43,8.62 42.92,8.26 42.79,7.93l3.61,-4.66c0.5,-0.7 0.16,-1.3 0.01,-1.5 -0.01,-0.01 -0.01,-0.02 -0.02,-0.03 -0,-0.01 -0.01,-0.02 -0.02,-0.03 -0,0 -0,0 -0,0.01 -0,-0 -0.01,-0.01 -0.01,-0.01 0,-0 0,-0 0.01,-0.01 -0.22,-0.26 -0.53,-0.44 -0.98,-0.45 -0.38,0.01 -0.74,0.18 -0.97,0.49L40.99,6.18C40.86,6.13 40.74,6.09 40.61,6.06 40.41,6.02 40.21,6 40,6L8,6C7.65,6 7.32,6.07 7.01,6.18L3.58,1.74C3.35,1.43 2.99,1.26 2.61,1.25ZM14.38,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38C12.51,16.5 11,14.99 11,13.13 11,11.26 12.51,9.75 14.38,9.75ZM33.88,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38 -1.86,0 -3.38,-1.51 -3.38,-3.38 0,-1.86 1.51,-3.38 3.38,-3.38zM8,20.97c-1.66,0 -3,1.34 -3,3L5,30 5,36 5,42.23 5,43c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,42.23 43,24.73 43,23.97c0,-1.66 -1.34,-3 -3,-3z"
|
||||
android:strokeAlpha="1"
|
||||
android:strokeColor="#00000000"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="miter"
|
||||
android:strokeWidth="2.5" />
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="m2.61,1.25c-0.45,0.01 -0.76,0.19 -0.98,0.45 0,0 0,0 0.01,0.01 -0,0 -0.01,0.01 -0.01,0.01 -0,-0 -0,-0 -0,-0.01 -0,0 -0.01,0.02 -0.02,0.03 -0.01,0.01 -0.01,0.02 -0.02,0.03 -0.15,0.2 -0.49,0.8 0.01,1.5L5.21,7.93C5.08,8.26 5,8.62 5,9v3,1 2.46L5,16c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,15.46 43,9.54 43,9C43,8.62 42.92,8.26 42.79,7.93l3.61,-4.66c0.5,-0.7 0.16,-1.3 0.01,-1.5 -0.01,-0.01 -0.01,-0.02 -0.02,-0.03 -0,-0.01 -0.01,-0.02 -0.02,-0.03 -0,0 -0,0 -0,0.01 -0,-0 -0.01,-0.01 -0.01,-0.01 0,-0 0,-0 0.01,-0.01 -0.22,-0.26 -0.53,-0.44 -0.98,-0.45 -0.38,0.01 -0.74,0.18 -0.97,0.49L40.99,6.18C40.86,6.13 40.74,6.09 40.61,6.06 40.41,6.02 40.21,6 40,6L8,6C7.65,6 7.32,6.07 7.01,6.18L3.58,1.74C3.35,1.43 2.99,1.26 2.61,1.25ZM14.38,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38C12.51,16.5 11,14.99 11,13.13 11,11.26 12.51,9.75 14.38,9.75ZM33.88,9.75c1.86,0 3.38,1.51 3.38,3.38 0,1.86 -1.51,3.38 -3.38,3.38 -1.86,0 -3.38,-1.51 -3.38,-3.38 0,-1.86 1.51,-3.38 3.38,-3.38zM8,20.97c-1.66,0 -3,1.34 -3,3L5,30 5,36 5,42.23 5,43c0,1.66 1.34,3 3,3h32c1.66,0 3,-1.34 3,-3L43,42.23 43,24.73 43,23.97c0,-1.66 -1.34,-3 -3,-3z" />
|
||||
</vector>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?android:attr/colorControlNormal"
|
||||
android:tint="#FFFFFFFF"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
|
||||
</vector>
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
<string name="app_issue_not_available_title">No longer available</string>
|
||||
<string name="app_issue_not_available_text">This app is not receiving updates, because it is no longer in any enabled repository.\n\nIt may have been in a repository you removed or disabled. Or it was simply removed.</string>
|
||||
<string name="app_issue_not_available_button">Show app info</string>
|
||||
<string name="app_issue_hint">The apps below need your attention. You can ignore an app issue by long pressing it.</string>
|
||||
<string name="added_x_ago">Added %1$s</string>
|
||||
<string name="size_colon">Size: %1$s</string>
|
||||
<string name="version_code_colon">Version code: %1$s</string>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[versions]
|
||||
compileSdk = "36"
|
||||
kotlin = "2.3.0"
|
||||
kotlin = "2.3.10"
|
||||
androidGradlePlugin = "8.13.2"
|
||||
androidKspPlugin = "2.3.4"
|
||||
hilt = "2.57.2"
|
||||
@@ -10,11 +10,11 @@ dokka = "2.1.0"
|
||||
mavenPublish = "0.35.0"
|
||||
jlleitschuhKtlint = "14.0.1"
|
||||
|
||||
kotlinxSerializationCore = "1.9.0"
|
||||
kotlinxSerializationJson = "1.9.0"
|
||||
kotlinxSerializationCore = "1.10.0"
|
||||
kotlinxSerializationJson = "1.10.0"
|
||||
kotlinxCoroutinesTest = "1.10.2"
|
||||
|
||||
ktor = "3.3.3"
|
||||
ktor = "3.4.0"
|
||||
okhttp = "4.12.0"
|
||||
room = "2.8.4"
|
||||
glide = "5.0.5"
|
||||
@@ -28,7 +28,7 @@ androidxCoreKtx = "1.17.0"
|
||||
androidxAppcompat = "1.7.1"
|
||||
androidxPreferenceKtx = "1.2.1"
|
||||
androidxLifecycleLivedataKtx = "2.10.0"
|
||||
androidxWork = "2.11.0"
|
||||
androidxWork = "2.11.1"
|
||||
androidxDocumentfile = "1.1.0"
|
||||
androidxLocalbroadcastmanager = "1.1.0"
|
||||
androidxHiltCompiler = "1.3.0"
|
||||
@@ -37,8 +37,8 @@ androidxConstraintlayout = "2.2.1"
|
||||
androidxSwipeRefreshLayout = "1.2.0"
|
||||
androidxVectordrawable = "1.2.0"
|
||||
androidxGridlayout = "1.1.0"
|
||||
androidxComposeBom = "2025.12.01"
|
||||
androidxActivityCompose = "1.12.2"
|
||||
androidxComposeBom = "2026.01.01"
|
||||
androidxActivityCompose = "1.12.3"
|
||||
accompanistDrawablepainter = "0.37.3"
|
||||
|
||||
# navigation3
|
||||
@@ -67,12 +67,12 @@ rxandroid = "3.0.2"
|
||||
|
||||
slf4jApi = "2.0.17"
|
||||
microutilsKotlinLogging = "3.0.5"
|
||||
logbackClassic = "1.5.22"
|
||||
logbackClassic = "1.5.27"
|
||||
logbackAndroid = "3.0.0"
|
||||
|
||||
junit = "4.13.2"
|
||||
mockk = "1.14.6"
|
||||
robolectric = "4.16"
|
||||
robolectric = "4.16.1"
|
||||
androidxTestCore = "1.7.0"
|
||||
androidxTestRunner = "1.7.0"
|
||||
androidxTestExtJunit = "1.3.0"
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -9,6 +9,7 @@ import org.fdroid.CompatibilityChecker
|
||||
import org.fdroid.CompatibilityCheckerImpl
|
||||
import org.fdroid.UpdateChecker
|
||||
import org.fdroid.index.IndexUtils.getPackageSigner
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
public class DbAppChecker(
|
||||
db: FDroidDatabase,
|
||||
@@ -115,12 +116,15 @@ public class DbAppChecker(
|
||||
)
|
||||
} else if (hasCompatibleSigner) {
|
||||
// the signer is compatible, so the update must come from a non-preferred repo
|
||||
AvailableAppWithIssue(
|
||||
val now = System.currentTimeMillis()
|
||||
// Only flag the compatible update in another repo, if older than a week.
|
||||
// This is to prevent short delays in providing updates causing unneeded UX churn.
|
||||
if (now - update.added > TimeUnit.DAYS.toMillis(7)) AvailableAppWithIssue(
|
||||
app = app,
|
||||
installVersionName = packageInfo.versionName ?: "???",
|
||||
installVersionCode = getLongVersionCode(packageInfo),
|
||||
issue = UpdateInOtherRepo(update.repoId),
|
||||
)
|
||||
) else null
|
||||
} else {
|
||||
// no update with compatible signer available
|
||||
getNoCompatibleSignerApp(
|
||||
|
||||
Reference in New Issue
Block a user