From 6cced040cbefa534090a0a500062ac5c7077ec54 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Mon, 13 Oct 2025 10:05:56 -0300 Subject: [PATCH] Implement repository onboarding --- .../org/fdroid/settings/SettingsManager.kt | 7 ++++ next/src/main/kotlin/org/fdroid/ui/Main.kt | 2 + .../kotlin/org/fdroid/ui/lists/AppList.kt | 26 ++++++------- .../fdroid/ui/repositories/Repositories.kt | 38 ++++++++++++++++++- .../ui/repositories/RepositoriesList.kt | 6 +-- .../ui/repositories/RepositoriesPresenter.kt | 2 + .../ui/repositories/RepositoriesViewModel.kt | 6 +++ .../fdroid/ui/repositories/RepositoryInfo.kt | 2 + .../fdroid/{ => ui}/utils/DragDropState.kt | 2 +- .../fdroid/{ => ui}/utils/OnboardingCard.kt | 2 +- .../org/fdroid/ui/utils/PreviewUtils.kt | 1 + .../kotlin/org/fdroid/ui/utils/UiUtils.kt | 5 +++ 12 files changed, 79 insertions(+), 20 deletions(-) rename next/src/main/kotlin/org/fdroid/{ => ui}/utils/DragDropState.kt (99%) rename next/src/main/kotlin/org/fdroid/{ => ui}/utils/OnboardingCard.kt (98%) diff --git a/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt b/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt index 8be0f4f6b..ba6791bf4 100644 --- a/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt +++ b/next/src/main/kotlin/org/fdroid/settings/SettingsManager.kt @@ -16,6 +16,8 @@ class SettingsManager @Inject constructor() { val appListSortOrder = _appListSortOrder.asStateFlow() private val _showFilterOnboarding = MutableStateFlow(true) val showFilterOnboarding = _showFilterOnboarding.asStateFlow() + private val _showRepositoriesOnboarding = MutableStateFlow(true) + val showRepositoriesOnboarding = _showRepositoriesOnboarding.asStateFlow() fun saveAppListFilter(sortOrder: AppListSortOrder, filterIncompatible: Boolean) { _appListSortOrder.value = sortOrder @@ -27,4 +29,9 @@ class SettingsManager @Inject constructor() { // TODO persist } + fun onRepositoriesOnboardingSeen() { + _showRepositoriesOnboarding.value = false + // TODO persist + } + } diff --git a/next/src/main/kotlin/org/fdroid/ui/Main.kt b/next/src/main/kotlin/org/fdroid/ui/Main.kt index 56fbe3044..7e4f7b8a7 100644 --- a/next/src/main/kotlin/org/fdroid/ui/Main.kt +++ b/next/src/main/kotlin/org/fdroid/ui/Main.kt @@ -201,6 +201,8 @@ fun Main(onListeningForIntent: () -> Unit = {}) { (backStack.last() as? NavigationKey.RepoDetails)?.repoId } else null + override fun onOnboardingSeen() = viewModel.onOnboardingSeen() + override fun onRepositorySelected(repositoryItem: RepositoryItem) { backStack.add(NavigationKey.RepoDetails(repositoryItem.repoId)) } diff --git a/next/src/main/kotlin/org/fdroid/ui/lists/AppList.kt b/next/src/main/kotlin/org/fdroid/ui/lists/AppList.kt index 08a20b9b7..7fbdcc0aa 100644 --- a/next/src/main/kotlin/org/fdroid/ui/lists/AppList.kt +++ b/next/src/main/kotlin/org/fdroid/ui/lists/AppList.kt @@ -24,7 +24,6 @@ import androidx.compose.material.icons.filled.Search import androidx.compose.material3.Badge import androidx.compose.material3.BadgedBox import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -52,19 +51,16 @@ 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.FlowPreview import org.fdroid.database.AppListSortOrder import org.fdroid.fdroid.ui.theme.FDroidContent import org.fdroid.next.R import org.fdroid.ui.utils.BigLoadingIndicator +import org.fdroid.ui.utils.OnboardingCard import org.fdroid.ui.utils.getAppListInfo -import org.fdroid.utils.OnboardingCard +import org.fdroid.ui.utils.getHintOverlayColor @Composable -@OptIn( - ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class, - FlowPreview::class -) +@OptIn(ExperimentalMaterial3Api::class) fun AppList( appListInfo: AppListInfo, currentPackageName: String?, @@ -76,21 +72,25 @@ fun AppList( val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) val hintController = rememberHintController( - overlay = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f), + overlay = getHintOverlayColor(), ) val hint = rememberHint { OnboardingCard( 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), - ) { - appListInfo.actions.onOnboardingSeen() - hintController.dismiss() - } + onGotIt = { + appListInfo.actions.onOnboardingSeen() + hintController.dismiss() + }, + ) } val hintAnchor = rememberHintAnchorState(hint) LaunchedEffect(appListInfo.showOnboarding) { - if (appListInfo.showOnboarding) hintController.show(hintAnchor) + if (appListInfo.showOnboarding) { + hintController.show(hintAnchor) + appListInfo.actions.onOnboardingSeen() + } } Scaffold( diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt index 40701d295..2d244b6c8 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/Repositories.kt @@ -2,6 +2,8 @@ package org.fdroid.ui.repositories import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_TYPE_NORMAL +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -14,13 +16,20 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.viktormykhailiv.compose.hints.rememberHint +import com.viktormykhailiv.compose.hints.rememberHintAnchorState +import com.viktormykhailiv.compose.hints.rememberHintController import org.fdroid.fdroid.ui.theme.FDroidContent import org.fdroid.next.R import org.fdroid.ui.utils.BigLoadingIndicator +import org.fdroid.ui.utils.OnboardingCard +import org.fdroid.ui.utils.getHintOverlayColor import org.fdroid.ui.utils.getRepositoriesInfo @Composable @@ -29,6 +38,30 @@ fun Repositories( info: RepositoryInfo, onBackClicked: () -> Unit, ) { + val hintController = rememberHintController( + overlay = getHintOverlayColor(), + ) + val hint = rememberHint { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + OnboardingCard( + 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 (info.model.showOnboarding) { + hintController.show(hintAnchor) + info.onOnboardingSeen() + } + } Scaffold( topBar = { TopAppBar( @@ -43,6 +76,7 @@ fun Repositories( title = { Text(stringResource(R.string.app_details_repositories)) }, + // TODO show when repos were last updated ) }, floatingActionButton = { @@ -73,7 +107,7 @@ fun Repositories( @Composable fun RepositoriesScaffoldLoadingPreview() { FDroidContent { - val model = RepositoryModel(null) + val model = RepositoryModel(null, false) val info = getRepositoriesInfo(model) Repositories(info) {} } @@ -106,7 +140,7 @@ fun RepositoriesScaffoldPreview() { name = "My second repository", ), ) - val model = RepositoryModel(repos) + val model = RepositoryModel(repos, false) val info = getRepositoriesInfo(model, repos[0].repoId) Repositories(info) { } } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt index d203ac08c..b22d3d3aa 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesList.kt @@ -18,9 +18,9 @@ import androidx.compose.ui.draw.dropShadow import androidx.compose.ui.graphics.shadow.Shadow import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp -import org.fdroid.utils.DraggableItem -import org.fdroid.utils.dragContainer -import org.fdroid.utils.rememberDragDropState +import org.fdroid.ui.utils.DraggableItem +import org.fdroid.ui.utils.dragContainer +import org.fdroid.ui.utils.rememberDragDropState @Composable fun RepositoriesList( diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesPresenter.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesPresenter.kt index 1836af7f2..da5a26bc2 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesPresenter.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesPresenter.kt @@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.StateFlow fun RepositoriesPresenter( repositoriesFlow: Flow>, repoSortingMapFlow: StateFlow>, + showOnboardingFlow: StateFlow, ): RepositoryModel { val repositories = repositoriesFlow.collectAsState(null).value val repoSortingMap = repoSortingMapFlow.collectAsState().value @@ -17,5 +18,6 @@ fun RepositoriesPresenter( // newly added repos will not be in repoSortingMap, so they need fallback repoSortingMap[repo.repoId] ?: repositories.find { it.repoId == repo.repoId }?.weight }, + showOnboarding = showOnboardingFlow.collectAsState().value, ) } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt index 659defc22..23675edc3 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoriesViewModel.kt @@ -15,12 +15,14 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import mu.KotlinLogging import org.fdroid.index.RepoManager +import org.fdroid.settings.SettingsManager import javax.inject.Inject @HiltViewModel class RepositoriesViewModel @Inject constructor( app: Application, private val repoManager: RepoManager, + private val settingsManager: SettingsManager, ) : AndroidViewModel(app) { private val log = KotlinLogging.logger { } @@ -34,6 +36,7 @@ class RepositoriesViewModel @Inject constructor( } } private val repoSortingMap: MutableStateFlow> + private val showOnboarding = settingsManager.showRepositoriesOnboarding init { // just add repos to sortingMap, because they are already pre-sorted by weight @@ -49,6 +52,7 @@ class RepositoriesViewModel @Inject constructor( RepositoriesPresenter( repositoriesFlow = repos, repoSortingMapFlow = repoSortingMap, + showOnboardingFlow = showOnboarding, ) } @@ -75,4 +79,6 @@ class RepositoriesViewModel @Inject constructor( fun addRepo() { } + + fun onOnboardingSeen() = settingsManager.onRepositoriesOnboardingSeen() } diff --git a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt index 2e28ce763..087587f5a 100644 --- a/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt +++ b/next/src/main/kotlin/org/fdroid/ui/repositories/RepositoryInfo.kt @@ -3,6 +3,7 @@ package org.fdroid.ui.repositories interface RepositoryInfo { val model: RepositoryModel val currentRepositoryId: Long? + fun onOnboardingSeen() fun onRepositorySelected(repositoryItem: RepositoryItem) fun onAddRepo() fun onRepositoryMoved(fromIndex: Int, toIndex: Int) @@ -11,4 +12,5 @@ interface RepositoryInfo { data class RepositoryModel( val repositories: List?, + val showOnboarding: Boolean, ) diff --git a/next/src/main/kotlin/org/fdroid/utils/DragDropState.kt b/next/src/main/kotlin/org/fdroid/ui/utils/DragDropState.kt similarity index 99% rename from next/src/main/kotlin/org/fdroid/utils/DragDropState.kt rename to next/src/main/kotlin/org/fdroid/ui/utils/DragDropState.kt index 10249d639..7e7a843d1 100644 --- a/next/src/main/kotlin/org/fdroid/utils/DragDropState.kt +++ b/next/src/main/kotlin/org/fdroid/ui/utils/DragDropState.kt @@ -17,7 +17,7 @@ * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/foundation/foundation/integration-tests/foundation-demos/src/main/java/androidx/compose/foundation/demos/LazyColumnDragAndDropDemo.kt;drc=e6d33dd5d0a60001a5784d84123b05308d35f410 */ -package org.fdroid.utils +package org.fdroid.ui.utils import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.Spring diff --git a/next/src/main/kotlin/org/fdroid/utils/OnboardingCard.kt b/next/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt similarity index 98% rename from next/src/main/kotlin/org/fdroid/utils/OnboardingCard.kt rename to next/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt index 5812c1460..3094da1b5 100644 --- a/next/src/main/kotlin/org/fdroid/utils/OnboardingCard.kt +++ b/next/src/main/kotlin/org/fdroid/ui/utils/OnboardingCard.kt @@ -1,4 +1,4 @@ -package org.fdroid.utils +package org.fdroid.ui.utils import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.widthIn diff --git a/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt b/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt index baa881630..c8b35afc0 100644 --- a/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt +++ b/next/src/main/kotlin/org/fdroid/ui/utils/PreviewUtils.kt @@ -229,6 +229,7 @@ fun getRepositoriesInfo( ): RepositoryInfo = object : RepositoryInfo { override val model: RepositoryModel = model override val currentRepositoryId: Long? = currentRepositoryId + override fun onOnboardingSeen() {} override fun onRepositorySelected(repositoryItem: RepositoryItem) {} override fun onAddRepo() {} override fun onRepositoryMoved(fromIndex: Int, toIndex: Int) {} diff --git a/next/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt b/next/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt index aa79993ef..4b4d43a64 100644 --- a/next/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt +++ b/next/src/main/kotlin/org/fdroid/ui/utils/UiUtils.kt @@ -7,11 +7,16 @@ import android.content.Intent import android.os.PowerManager import android.text.format.DateUtils import android.util.Log +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable import androidx.compose.ui.platform.UriHandler import androidx.core.content.ContextCompat import java.text.Normalizer import java.text.Normalizer.Form.NFKD +@Composable +fun getHintOverlayColor() = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.8f) + fun Context.startActivitySafe(i: Intent?) { if (i == null) return try {