Implement repository onboarding

This commit is contained in:
Torsten Grote
2025-10-13 10:05:56 -03:00
parent c4cbf5abbc
commit 6cced040cb
12 changed files with 79 additions and 20 deletions

View File

@@ -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
}
}

View File

@@ -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))
}

View File

@@ -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(

View File

@@ -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) { }
}

View File

@@ -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(

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.StateFlow
fun RepositoriesPresenter(
repositoriesFlow: Flow<List<RepositoryItem>>,
repoSortingMapFlow: StateFlow<Map<Long, Int>>,
showOnboardingFlow: StateFlow<Boolean>,
): 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,
)
}

View File

@@ -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<Map<Long, Int>>
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()
}

View File

@@ -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<RepositoryItem>?,
val showOnboarding: Boolean,
)

View File

@@ -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

View File

@@ -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

View File

@@ -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) {}

View File

@@ -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 {