mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-10-06 02:53:12 -04:00
Show onboarding for anti-features in app details
This commit is contained in:
9 files changed
+71
-5
No files matched your search
@@ -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<Boolean>) {
|
||||
|
||||
@@ -30,11 +30,11 @@ import org.fdroid.ui.utils.testApp
|
||||
|
||||
@Composable
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
fun AntiFeatures(antiFeatures: List<AntiFeature>) {
|
||||
fun AntiFeatures(antiFeatures: List<AntiFeature>, 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),
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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<AntiFeature>? = 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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -47,6 +47,7 @@ fun DetailsPresenter(
|
||||
viewModel: AppDetailsViewModel,
|
||||
packageInfoFlow: StateFlow<AppInfo?>,
|
||||
currentRepoIdFlow: StateFlow<Long?>,
|
||||
showAntiFeaturesOnboardingFlow: StateFlow<Boolean>,
|
||||
appsWithIssuesFlow: StateFlow<List<AppWithIssueItem>?>,
|
||||
networkStateFlow: StateFlow<NetworkState>,
|
||||
): 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,
|
||||
|
||||
@@ -257,6 +257,7 @@ fun getAppDetailsActions() =
|
||||
onRepoChanged = {},
|
||||
onPreferredRepoChanged = {},
|
||||
allowBetaVersions = {},
|
||||
onAntiFeaturesOnboardingSeen = {},
|
||||
ignoreAllUpdates = {},
|
||||
ignoreThisUpdate = {},
|
||||
shareApk = Intent(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -226,6 +226,10 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string name="app_details_more_apps_interesting">This might also be interesting to you:</string>
|
||||
<string name="app_details_more_apps_by_author">More apps by %1$s</string>
|
||||
|
||||
<string name="app_details_anti_features_title">What are Anti-Features?</string>
|
||||
<string name="app_details_anti_features_text">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.</string>
|
||||
|
||||
<string name="about_title_full">About F-Droid</string>
|
||||
<string name="about_title_basic">About F-Droid Basic</string>
|
||||
<string name="about_text">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.</string>
|
||||
|
||||
Reference in new issue
Block a user