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