From acbb4320c295ea7c2735a549d17ca04f18d75f16 Mon Sep 17 00:00:00 2001 From: Torsten Grote Date: Wed, 16 Jul 2025 17:29:35 -0300 Subject: [PATCH] Use experimental navigation3 library which is still buggy, but so was the old implementation and this seems to be the future --- basic/build.gradle.kts | 12 +- .../java/org/fdroid/basic/MainViewModel.kt | 6 +- .../org/fdroid/basic/manager/MyAppsManager.kt | 2 +- .../fdroid/basic/repo/RepositoryManager.kt | 8 +- .../java/org/fdroid/basic/ui/Navigation.kt | 47 ++++ .../org/fdroid/basic/ui/main/BottomBar.kt | 65 +++++ .../fdroid/basic/ui/main/BottomBarScreen.kt | 170 ------------ .../java/org/fdroid/basic/ui/main/Main.kt | 247 +++++++++++++----- .../ui/main/apps/{MyAppsList.kt => MyApps.kt} | 55 +++- .../basic/ui/main/apps/MyAppsScaffold.kt | 114 -------- .../fdroid/basic/ui/main/discover/Discover.kt | 23 +- .../ui/main/discover/DiscoverScaffold.kt | 150 ----------- .../fdroid/basic/ui/main/lists/AppsFilter.kt | 2 - .../{discover => lists}/FilterPresenter.kt | 8 +- .../main/repositories/RepositoriesScaffold.kt | 204 --------------- .../ui/main/repositories/RepositoryList.kt | 141 ++++++++++ gradle/libs.versions.toml | 18 +- 17 files changed, 544 insertions(+), 728 deletions(-) create mode 100644 basic/src/main/java/org/fdroid/basic/ui/Navigation.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/BottomBar.kt delete mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/BottomBarScreen.kt rename basic/src/main/java/org/fdroid/basic/ui/main/apps/{MyAppsList.kt => MyApps.kt} (81%) delete mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/apps/MyAppsScaffold.kt delete mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/discover/DiscoverScaffold.kt rename basic/src/main/java/org/fdroid/basic/ui/main/{discover => lists}/FilterPresenter.kt (91%) delete mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoriesScaffold.kt create mode 100644 basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoryList.kt diff --git a/basic/build.gradle.kts b/basic/build.gradle.kts index 0db5926db..a23d5748a 100644 --- a/basic/build.gradle.kts +++ b/basic/build.gradle.kts @@ -67,13 +67,15 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.navigation.compose) + implementation(libs.androidx.compose.material3.adaptive.navigation3) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.material3) - implementation(libs.androidx.compose.material3.adaptive) - implementation(libs.androidx.compose.material3.adaptive.layout) - implementation(libs.androidx.compose.material3.adaptive.navigation) - implementation(libs.androidx.compose.material3.adaptive.navigation.suite.android) + + implementation(libs.androidx.navigation3.ui) + implementation(libs.androidx.navigation3.runtime) + implementation(libs.androidx.lifecycle.viewmodel.navigation3) + implementation(libs.kotlinx.serialization.core) + implementation(libs.molecule.runtime) implementation(libs.coil.compose) implementation(libs.coil.network.okhttp) diff --git a/basic/src/main/java/org/fdroid/basic/MainViewModel.kt b/basic/src/main/java/org/fdroid/basic/MainViewModel.kt index 4b21c50fe..a087f8bf1 100644 --- a/basic/src/main/java/org/fdroid/basic/MainViewModel.kt +++ b/basic/src/main/java/org/fdroid/basic/MainViewModel.kt @@ -25,9 +25,9 @@ import org.fdroid.basic.ui.main.apps.MinimalApp import org.fdroid.basic.ui.main.discover.AppNavigationItem import org.fdroid.basic.ui.main.discover.DiscoverModel import org.fdroid.basic.ui.main.discover.DiscoverPresenter -import org.fdroid.basic.ui.main.discover.FilterModel -import org.fdroid.basic.ui.main.discover.FilterPresenter -import org.fdroid.basic.ui.main.discover.Sort +import org.fdroid.basic.ui.main.lists.FilterModel +import org.fdroid.basic.ui.main.lists.FilterPresenter +import org.fdroid.basic.ui.main.lists.Sort import org.fdroid.basic.ui.main.lists.AppList import org.fdroid.database.FDroidDatabase import java.text.Collator diff --git a/basic/src/main/java/org/fdroid/basic/manager/MyAppsManager.kt b/basic/src/main/java/org/fdroid/basic/manager/MyAppsManager.kt index 93a7264b9..bbdc7737b 100644 --- a/basic/src/main/java/org/fdroid/basic/manager/MyAppsManager.kt +++ b/basic/src/main/java/org/fdroid/basic/manager/MyAppsManager.kt @@ -11,7 +11,7 @@ import org.fdroid.basic.ui.Icons import org.fdroid.basic.ui.Names import org.fdroid.basic.ui.main.apps.InstalledApp import org.fdroid.basic.ui.main.apps.UpdatableApp -import org.fdroid.basic.ui.main.discover.Sort +import org.fdroid.basic.ui.main.lists.Sort import java.util.Locale import javax.inject.Inject import javax.inject.Singleton diff --git a/basic/src/main/java/org/fdroid/basic/repo/RepositoryManager.kt b/basic/src/main/java/org/fdroid/basic/repo/RepositoryManager.kt index 2eeb56808..d4281b787 100644 --- a/basic/src/main/java/org/fdroid/basic/repo/RepositoryManager.kt +++ b/basic/src/main/java/org/fdroid/basic/repo/RepositoryManager.kt @@ -3,8 +3,7 @@ package org.fdroid.basic.repo import android.content.Context import androidx.core.os.LocaleListCompat import dagger.hilt.android.qualifiers.ApplicationContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -13,6 +12,7 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.fdroid.basic.ui.main.repositories.Repository +import org.fdroid.basic.utils.IoDispatcher import org.fdroid.index.RepoManager import org.fdroid.repo.AddRepoError import org.fdroid.repo.Added @@ -24,12 +24,14 @@ import javax.inject.Singleton @Singleton // TODO maybe more like a ViewModel name clash with RepoManager class RepositoryManager @Inject constructor( @ApplicationContext private val context: Context, + @IoDispatcher private val coroutineScope: CoroutineScope, private val repoManager: RepoManager, ) { val repos: Flow> = repoManager.repositoriesState.map { repos -> repos.map { Repository( + repoId = it.repoId, address = it.address, timestamp = it.timestamp, lastUpdated = it.lastUpdated, @@ -52,7 +54,7 @@ class RepositoryManager @Inject constructor( fun addRepo() { // just temp code to get repo into DB repoManager.fetchRepositoryPreview("https://f-droid.org/repo") - GlobalScope.launch(Dispatchers.IO) { + coroutineScope.launch { var hasAdded = false repoManager.addRepoState.collect { if (it is Fetching) { diff --git a/basic/src/main/java/org/fdroid/basic/ui/Navigation.kt b/basic/src/main/java/org/fdroid/basic/ui/Navigation.kt new file mode 100644 index 000000000..98cb6f5e8 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/Navigation.kt @@ -0,0 +1,47 @@ +package org.fdroid.basic.ui + +import androidx.annotation.StringRes +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Apps +import androidx.compose.material.icons.filled.Explore +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation3.runtime.NavKey +import kotlinx.serialization.Serializable +import org.fdroid.basic.R + +sealed interface NavigationKey : NavKey { + + @Serializable + data object Discover : NavigationKey + + @Serializable + data object MyApps : NavigationKey + + @Serializable + data class AppDetails(val packageName: String) : NavigationKey + + @Serializable + data object AppList : NavigationKey + + @Serializable + data object Repos : NavigationKey + + @Serializable + data class RepoDetails(val repoId: Long) : NavigationKey + + @Serializable + data object Settings : NavigationKey + + @Serializable + data object About : NavigationKey + +} + +enum class BottomNavDestinations( + val key: NavigationKey, + @StringRes val label: Int, + val icon: ImageVector, +) { + DISCOVER(NavigationKey.Discover, R.string.discover, Icons.Filled.Explore), + MY_APPS(NavigationKey.MyApps, R.string.apps_my, Icons.Filled.Apps), +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/BottomBar.kt b/basic/src/main/java/org/fdroid/basic/ui/main/BottomBar.kt new file mode 100644 index 000000000..9d2d1a6d7 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/BottomBar.kt @@ -0,0 +1,65 @@ +package org.fdroid.basic.ui.main + +import androidx.compose.material3.Badge +import androidx.compose.material3.BadgedBox +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.navigation3.runtime.NavKey +import org.fdroid.basic.ui.BottomNavDestinations +import org.fdroid.basic.ui.NavigationKey + +@Composable +fun BottomBar(numUpdates: Int, currentNavKey: NavKey, onNav: (NavigationKey) -> Unit) { + NavigationBar { + BottomNavDestinations.entries.forEach { dest -> + NavigationBarItem( + icon = { NavIcon(dest, numUpdates) }, + label = { Text(stringResource(dest.label)) }, + selected = dest.key == currentNavKey, + onClick = { + if (dest.key != currentNavKey) onNav(dest.key) + }, + ) + } + } +} + +@Composable +fun NavigationRail(numUpdates: Int, currentNavKey: NavKey, onNav: (NavigationKey) -> Unit) { + NavigationRail { + BottomNavDestinations.entries.forEach { dest -> + NavigationRailItem( + icon = { NavIcon(dest, numUpdates) }, + label = { Text(stringResource(dest.label)) }, + selected = dest.key == currentNavKey, + onClick = { + if (dest.key != currentNavKey) onNav(dest.key) + }, + ) + } + } +} + +@Composable +private fun NavIcon(dest: BottomNavDestinations, numUpdates: Int) { + BadgedBox( + badge = { + if (dest == BottomNavDestinations.MY_APPS && numUpdates > 0) { + Badge { + Text(text = numUpdates.toString()) + } + } + } + ) { + Icon( + dest.icon, + contentDescription = stringResource(dest.label) + ) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/BottomBarScreen.kt b/basic/src/main/java/org/fdroid/basic/ui/main/BottomBarScreen.kt deleted file mode 100644 index 6f8d05098..000000000 --- a/basic/src/main/java/org/fdroid/basic/ui/main/BottomBarScreen.kt +++ /dev/null @@ -1,170 +0,0 @@ -package org.fdroid.basic.ui.main - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Apps -import androidx.compose.material.icons.filled.Explore -import androidx.compose.material3.Badge -import androidx.compose.material3.BadgedBox -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteDefaults -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold -import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewScreenSizes -import androidx.window.core.layout.WindowWidthSizeClass -import org.fdroid.basic.R -import org.fdroid.basic.details.AppDetailsItem -import org.fdroid.basic.ui.main.apps.InstalledApp -import org.fdroid.basic.ui.main.apps.MinimalApp -import org.fdroid.basic.ui.main.apps.MyAppsScaffold -import org.fdroid.basic.ui.main.apps.UpdatableApp -import org.fdroid.basic.ui.main.discover.AppNavigationItem -import org.fdroid.basic.ui.main.discover.DiscoverModel -import org.fdroid.basic.ui.main.discover.DiscoverScaffold -import org.fdroid.basic.ui.main.discover.FilterModel -import org.fdroid.basic.ui.main.discover.LoadingDiscoverModel -import org.fdroid.basic.ui.main.discover.Sort -import org.fdroid.basic.ui.main.lists.AppList -import org.fdroid.basic.ui.main.lists.FilterInfo - -enum class BottomNavDestinations( - @StringRes val label: Int, - val icon: ImageVector, -) { - APPS(R.string.discover, Icons.Filled.Explore), - UPDATES(R.string.apps_my, Icons.Filled.Apps), -} - -@Composable -fun BottomBarScreen( - onMainNav: (String) -> Unit, - numUpdates: Int, - updates: List, - installed: List, - appList: AppList, - discoverModel: DiscoverModel, - filterInfo: FilterInfo, - currentItem: AppDetailsItem?, - onSelectAppItem: (MinimalApp) -> Unit, - sortBy: Sort, - onSortChanged: (Sort) -> Unit, - onAppListChanged: (AppList) -> Unit, -) { - var currentDestination by rememberSaveable { mutableStateOf(BottomNavDestinations.APPS) } - val adaptiveInfo = currentWindowAdaptiveInfo() - val customNavSuiteType = with(adaptiveInfo) { - when (windowSizeClass.windowWidthSizeClass) { - WindowWidthSizeClass.COMPACT -> NavigationSuiteType.NavigationBar - else -> NavigationSuiteType.NavigationRail - } - } - NavigationSuiteScaffold( - modifier = Modifier.fillMaxSize(), - layoutType = customNavSuiteType, - navigationSuiteColors = NavigationSuiteDefaults.colors( - navigationBarContainerColor = Color.Transparent, - ), - navigationSuiteItems = { - BottomNavDestinations.entries.forEach { dest -> - item( - icon = { - BadgedBox( - badge = { - if (dest == BottomNavDestinations.UPDATES && numUpdates > 0) { - Badge { - Text(text = numUpdates.toString()) - } - } - } - ) { - Icon( - dest.icon, - contentDescription = stringResource(dest.label) - ) - } - }, - label = { Text(stringResource(dest.label)) }, - selected = dest == currentDestination, - onClick = { currentDestination = dest } - ) - } - } - ) { - when (currentDestination) { - BottomNavDestinations.APPS -> DiscoverScaffold( - discoverModel = discoverModel, - appList = appList, - filterInfo = filterInfo, - onMainNav = onMainNav, - currentItem = currentItem, - onSelectAppItem = onSelectAppItem, - onAppListChanged = onAppListChanged, - modifier = Modifier, - ) - BottomNavDestinations.UPDATES -> MyAppsScaffold( - updatableApps = updates, - installedApps = installed, - currentItem = currentItem, - onSelectAppItem = onSelectAppItem, - sortBy = sortBy, - onSortChanged = onSortChanged, - ) - } - } -} - -@Preview -@PreviewScreenSizes -@Composable -fun BottomBarPreview() { - val apps = listOf( - AppNavigationItem("", "foo", null, "bar", false), - AppNavigationItem("", "foo", null, "bar", false), - AppNavigationItem("", "foo", null, "bar", false), - ) - var filterExpanded by rememberSaveable { mutableStateOf(true) } - val filterInfo = object : FilterInfo { - override val model = FilterModel( - isLoading = false, - areFiltersShown = filterExpanded, - apps = apps, - sortBy = Sort.NAME, - allCategories = emptyList(), - addedCategories = emptyList(), - ) - - override fun toggleFilterVisibility() {} - override fun sortBy(sort: Sort) {} - override fun addCategory(category: String) {} - override fun removeCategory(category: String) { - filterExpanded = !filterExpanded - } - } - BottomBarScreen( - onMainNav = { }, - numUpdates = 2, - updates = emptyList(), - installed = emptyList(), - appList = AppList.New, - discoverModel = LoadingDiscoverModel(true), - filterInfo = filterInfo, - currentItem = null, - onSelectAppItem = {}, - sortBy = Sort.NAME, - onSortChanged = {}, - onAppListChanged = {}, - ) -} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt b/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt index 3725978f2..4698e9020 100644 --- a/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt +++ b/basic/src/main/java/org/fdroid/basic/ui/main/Main.kt @@ -1,33 +1,59 @@ package org.fdroid.basic.ui.main import androidx.annotation.StringRes +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement.spacedBy +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo +import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective +import androidx.compose.material3.adaptive.navigation3.ListDetailSceneStrategy +import androidx.compose.material3.adaptive.navigation3.rememberListDetailSceneStrategy import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController +import androidx.navigation3.runtime.NavKey +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator +import androidx.navigation3.scene.rememberSceneSetupNavEntryDecorator +import androidx.navigation3.ui.NavDisplay +import androidx.window.core.layout.WindowSizeClass.Companion.WIDTH_DP_MEDIUM_LOWER_BOUND import org.fdroid.basic.MainViewModel import org.fdroid.basic.R +import org.fdroid.basic.ui.NavigationKey import org.fdroid.basic.ui.icons.PackageVariant -import org.fdroid.basic.ui.main.discover.Sort +import org.fdroid.basic.ui.main.apps.MyApps +import org.fdroid.basic.ui.main.details.AppDetails +import org.fdroid.basic.ui.main.discover.Discover +import org.fdroid.basic.ui.main.lists.AppList import org.fdroid.basic.ui.main.lists.FilterInfo -import org.fdroid.basic.ui.main.repositories.RepositoriesScaffold +import org.fdroid.basic.ui.main.lists.Sort +import org.fdroid.basic.ui.main.repositories.RepositoryList import org.fdroid.fdroid.ui.theme.FDroidContent sealed class NavDestinations( - val id: String, + val id: NavigationKey, @StringRes val label: Int, val icon: ImageVector, ) { - object Main : NavDestinations("main", R.string.app_name, Icons.Default.Info) - object Repos : NavDestinations("repos", R.string.app_details_repositories, PackageVariant) - object Settings : NavDestinations("settings", R.string.menu_settings, Icons.Filled.Settings) - object About : NavDestinations("about", R.string.about, Icons.Filled.Info) + object Repos : + NavDestinations(NavigationKey.Repos, R.string.app_details_repositories, PackageVariant) + + object Settings : + NavDestinations(NavigationKey.Settings, R.string.menu_settings, Icons.Filled.Settings) + + object About : NavDestinations(NavigationKey.About, R.string.about, Icons.Filled.Info) } val topBarMenuItems = listOf( @@ -39,64 +65,159 @@ val moreMenuItems = listOf( NavDestinations.About, ) +@OptIn(ExperimentalMaterial3AdaptiveApi::class) @Composable fun Main(viewModel: MainViewModel = hiltViewModel()) { + val backStack = rememberNavBackStack(NavigationKey.Discover) + // Override the defaults so that there isn't a horizontal space between the panes. + val windowAdaptiveInfo = currentWindowAdaptiveInfo() + val directive = remember(windowAdaptiveInfo) { + calculatePaneScaffoldDirective(windowAdaptiveInfo) + .copy(horizontalPartitionSpacerSize = 0.dp) + } + val isBigScreen = remember(windowAdaptiveInfo) { + windowAdaptiveInfo.windowSizeClass.isWidthAtLeastBreakpoint(WIDTH_DP_MEDIUM_LOWER_BOUND) + } + val listDetailStrategy = rememberListDetailSceneStrategy()//(directive = directive) FDroidContent { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = NavDestinations.Main.id) { - composable(route = NavDestinations.Main.id) { - val numUpdates = viewModel.numUpdates.collectAsStateWithLifecycle(0).value - val updates = viewModel.updates.collectAsStateWithLifecycle().value - val installed = viewModel.installed.collectAsStateWithLifecycle().value - val filterInfo = object : FilterInfo { - override val model = viewModel.filterModel.collectAsStateWithLifecycle().value - override fun toggleFilterVisibility() { - viewModel.toggleListFilterVisibility() - } - - override fun sortBy(sort: Sort) = viewModel.sortBy(sort) - override fun addCategory(category: String) = viewModel.addCategory(category) - override fun removeCategory(category: String) = - viewModel.removeCategory(category) - } - val currentItem = viewModel.appDetails.collectAsStateWithLifecycle().value - BottomBarScreen( - onMainNav = { navController.navigate(it) }, - numUpdates = numUpdates, - updates = updates, - installed = installed, - onAppListChanged = viewModel::setAppList, - appList = viewModel.currentList.collectAsStateWithLifecycle().value, - discoverModel = viewModel.discoverModel.collectAsStateWithLifecycle().value, - filterInfo = filterInfo, - currentItem = currentItem, - onSelectAppItem = viewModel::setAppDetails, - sortBy = viewModel.myAppsManager.sortBy.collectAsStateWithLifecycle().value, - onSortChanged = viewModel.myAppsManager::sortBy, - ) - } - composable(route = NavDestinations.Repos.id) { - val repositoryManager = viewModel.repositoryManager - val repos = repositoryManager.repos.collectAsStateWithLifecycle(null).value - val visibleRepository = - repositoryManager.visibleRepository.collectAsStateWithLifecycle().value - RepositoriesScaffold( - repositories = repos, - currentRepository = visibleRepository, - onRepositorySelected = repositoryManager::setVisibleRepository, - onAddRepo = repositoryManager::addRepo, + NavDisplay( + backStack = backStack, + onBack = { keysToRemove -> + repeat(keysToRemove) { backStack.removeLastOrNull() } + }, + sceneStrategy = listDetailStrategy, + entryDecorators = listOf( + rememberSceneSetupNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), +// rememberViewModelStoreNavEntryDecorator(), + ), + entryProvider = entryProvider { + entry( + metadata = ListDetailSceneStrategy.listPane("appdetails") { + Text("No app selected") + }, ) { - navController.popBackStack() + val numUpdates = viewModel.numUpdates.collectAsStateWithLifecycle(0).value + Discover( + discoverModel = viewModel.discoverModel.collectAsStateWithLifecycle().value, + onTitleTap = { + viewModel.setAppList(it) + backStack.add(NavigationKey.AppList) + }, + onAppTap = { + viewModel.setAppDetails(it) + backStack.add(NavigationKey.AppDetails(it.packageName)) + }, + onNav = { backStack.add(it) }, + numUpdates = numUpdates, + isBigScreen = isBigScreen, + modifier = Modifier, + ) } - } - composable(route = NavDestinations.Settings.id) { - Settings { navController.popBackStack() } - } - composable(route = NavDestinations.About.id) { - About { navController.popBackStack() } - } - // Add more destinations similarly. - } + entry( + metadata = ListDetailSceneStrategy.listPane("appdetails") { + Text("No app selected") + }, + ) { + val updates = viewModel.updates.collectAsStateWithLifecycle().value + val installed = viewModel.installed.collectAsStateWithLifecycle().value + val currentItem = viewModel.appDetails.collectAsStateWithLifecycle().value + MyApps( + updatableApps = updates, + installedApps = installed, + currentItem = currentItem, + onItemClick = { + viewModel.setAppDetails(it) + backStack.add(NavigationKey.AppDetails(it.packageName)) + }, + onNav = { backStack.add(it) }, + sortBy = viewModel.myAppsManager.sortBy.collectAsStateWithLifecycle().value, + isBigScreen = isBigScreen, + onSortChanged = viewModel.myAppsManager::sortBy, + ) + } + entry( + metadata = ListDetailSceneStrategy.detailPane("appdetails") + ) { + val currentItem = viewModel.appDetails.collectAsStateWithLifecycle().value + it.packageName // TODO use? + AppDetails( + appItem = currentItem, + onBackNav = { backStack.removeLastOrNull() }, + modifier = Modifier, + ) + } + entry( + metadata = ListDetailSceneStrategy.listPane("appdetails") { + Text("No app selected") + }, + ) { + val filterInfo = object : FilterInfo { + override val model = + viewModel.filterModel.collectAsStateWithLifecycle().value + override fun toggleFilterVisibility() { + viewModel.toggleListFilterVisibility() + } + + override fun sortBy(sort: Sort) = viewModel.sortBy(sort) + override fun addCategory(category: String) = viewModel.addCategory(category) + override fun removeCategory(category: String) = + viewModel.removeCategory(category) + } + val currentItem = viewModel.appDetails.collectAsStateWithLifecycle().value + AppList( + appList = viewModel.currentList.collectAsStateWithLifecycle().value, + filterInfo = filterInfo, + currentItem = currentItem, + onBackClicked = { backStack.removeLastOrNull() }, + modifier = Modifier, + ) { + viewModel.setAppDetails(it) + backStack.add(NavigationKey.AppDetails(it.packageName)) + } + } + entry( + metadata = ListDetailSceneStrategy.listPane("repos") { + Text(text = "No repository selected") + }, + ) { + val repositoryManager = viewModel.repositoryManager + val repos = repositoryManager.repos.collectAsStateWithLifecycle(null).value + val visibleRepository = + repositoryManager.visibleRepository.collectAsStateWithLifecycle().value + RepositoryList( + repositories = repos, + currentRepository = visibleRepository, + onRepositorySelected = { + repositoryManager.setVisibleRepository(it) + backStack.add(NavigationKey.RepoDetails(it.repoId)) + } , + onAddRepo = repositoryManager::addRepo, + ) { + backStack.removeLastOrNull() + } + } + entry( + metadata = ListDetailSceneStrategy.detailPane("repos") + ) { + Column( + verticalArrangement = spacedBy(16.dp), + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .safeDrawingPadding(), + ) { + Text("Repo ${it.repoId}") + Text("This will basically be the repo details screen from latest client") + } + } + entry(NavigationKey.Settings) { + Settings { backStack.removeLastOrNull() } + } + entry(NavigationKey.About) { + About { backStack.removeLastOrNull() } + } + }, + ) } } diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/MyAppsList.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/MyApps.kt similarity index 81% rename from basic/src/main/java/org/fdroid/basic/ui/main/apps/MyAppsList.kt rename to basic/src/main/java/org/fdroid/basic/ui/main/apps/MyApps.kt index 4acbe6889..f1ad3a1e5 100644 --- a/basic/src/main/java/org/fdroid/basic/ui/main/apps/MyAppsList.kt +++ b/basic/src/main/java/org/fdroid/basic/ui/main/apps/MyApps.kt @@ -33,20 +33,29 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.NavKey import org.fdroid.basic.R import org.fdroid.basic.details.AppDetailsItem -import org.fdroid.basic.ui.main.discover.Sort +import org.fdroid.basic.ui.Names +import org.fdroid.basic.ui.NavigationKey +import org.fdroid.basic.ui.main.BottomBar +import org.fdroid.basic.ui.main.lists.Sort +import org.fdroid.fdroid.ui.theme.FDroidContent @Composable @OptIn(ExperimentalMaterial3Api::class) -fun MyAppsList( +fun MyApps( updatableApps: List, installedApps: List, currentItem: AppDetailsItem?, onItemClick: (MinimalApp) -> Unit, + onNav: (NavKey) -> Unit, sortBy: Sort, onSortChanged: (Sort) -> Unit, + isBigScreen: Boolean, modifier: Modifier = Modifier, ) { val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) @@ -120,6 +129,9 @@ fun MyAppsList( scrollBehavior = scrollBehavior, ) }, + bottomBar = { + if (!isBigScreen) BottomBar(updatableApps.size, NavigationKey.MyApps, onNav) + }, modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> LazyColumn( @@ -181,3 +193,42 @@ fun MyAppsList( } } } + +@Preview +@PreviewScreenSizes +@Composable +fun MyAppsScaffoldPreview() { + FDroidContent { + val app1 = UpdatableApp( + packageName = "A", + name = Names.randomName, + currentVersionName = "1.0.1", + updateVersionName = "1.1.0", + size = 123456789, + ) + val app2 = UpdatableApp( + packageName = "B", + name = Names.randomName, + currentVersionName = "3.0.1", + updateVersionName = "3.1.0", + size = 9876543, + ) + val installedApp1 = + InstalledApp("1", Names.randomName, org.fdroid.basic.ui.Icons.randomIcon, "3.0.1") + val installedApp2 = + InstalledApp("2", Names.randomName, org.fdroid.basic.ui.Icons.randomIcon, "1.0") + val installedApp3 = + InstalledApp("3", Names.randomName, org.fdroid.basic.ui.Icons.randomIcon, "0.1") + var sortBy by remember { mutableStateOf(Sort.NAME) } + MyApps( + updatableApps = listOf(app1, app2), + installedApps = listOf(installedApp1, installedApp2, installedApp3), + currentItem = null, + onItemClick = {}, + onNav = {}, + sortBy = sortBy, + onSortChanged = { sortBy = it }, + isBigScreen = false, + ) + } +} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/apps/MyAppsScaffold.kt b/basic/src/main/java/org/fdroid/basic/ui/main/apps/MyAppsScaffold.kt deleted file mode 100644 index 88abcaabb..000000000 --- a/basic/src/main/java/org/fdroid/basic/ui/main/apps/MyAppsScaffold.kt +++ /dev/null @@ -1,114 +0,0 @@ -package org.fdroid.basic.ui.main.apps - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole.Detail -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole.List -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.runtime.Composable -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.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewScreenSizes -import androidx.compose.ui.unit.dp -import kotlinx.coroutines.launch -import org.fdroid.basic.details.AppDetailsItem -import org.fdroid.basic.ui.Icons -import org.fdroid.basic.ui.Names -import org.fdroid.basic.ui.main.details.AppDetails -import org.fdroid.basic.ui.main.discover.Sort -import org.fdroid.fdroid.ui.theme.FDroidContent - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class) -@Composable -fun MyAppsScaffold( - updatableApps: List, - installedApps: List, - currentItem: AppDetailsItem?, - onSelectAppItem: (MinimalApp) -> Unit, - sortBy: Sort, - onSortChanged: (Sort) -> Unit, - modifier: Modifier = Modifier, -) { - val navigator = rememberListDetailPaneScaffoldNavigator() - val scope = rememberCoroutineScope() - BackHandler(enabled = navigator.canNavigateBack()) { - scope.launch { - navigator.navigateBack() - } - } - val isDetailVisible = navigator.scaffoldValue[Detail] == PaneAdaptedValue.Expanded - val isListVisible = navigator.scaffoldValue[List] == PaneAdaptedValue.Expanded - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - modifier = modifier, - listPane = { - AnimatedPane { - MyAppsList( - updatableApps = updatableApps, - installedApps = installedApps, - currentItem = if (isDetailVisible) currentItem else null, - onItemClick = { - onSelectAppItem(it) - scope.launch { navigator.navigateTo(Detail, it) } - }, - sortBy = sortBy, - onSortChanged = onSortChanged, - ) - } - }, - detailPane = { - AnimatedPane { - currentItem?.let { - val back: () -> Unit = { scope.launch { navigator.navigateBack() } } - AppDetails(it, if (isListVisible) null else back) - } ?: Text("No app selected", modifier = Modifier.padding(16.dp)) - } - }, - ) -} - -@Preview -@PreviewScreenSizes -@Composable -fun MyAppsScaffoldPreview() { - FDroidContent { - val app1 = UpdatableApp( - packageName = "", - name = Names.randomName, - currentVersionName = "1.0.1", - updateVersionName = "1.1.0", - size = 123456789, - ) - val app2 = UpdatableApp( - packageName = "", - name = Names.randomName, - currentVersionName = "3.0.1", - updateVersionName = "3.1.0", - size = 9876543, - ) - val installedApp1 = InstalledApp("", Names.randomName, Icons.randomIcon, "3.0.1") - val installedApp2 = InstalledApp("", Names.randomName, Icons.randomIcon, "1.0") - val installedApp3 = InstalledApp("", Names.randomName, Icons.randomIcon, "0.1") - var sortBy by remember { mutableStateOf(Sort.NAME) } - MyAppsScaffold( - updatableApps = listOf(app1, app2), - installedApps = listOf(installedApp1, installedApp2, installedApp3), - currentItem = null, - onSelectAppItem = {}, - sortBy = sortBy, - onSortChanged = { sortBy = it }, - ) - } -} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/discover/Discover.kt b/basic/src/main/java/org/fdroid/basic/ui/main/discover/Discover.kt index 661a0c982..1e951d360 100644 --- a/basic/src/main/java/org/fdroid/basic/ui/main/discover/Discover.kt +++ b/basic/src/main/java/org/fdroid/basic/ui/main/discover/Discover.kt @@ -37,7 +37,10 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.navigation3.runtime.NavKey +import org.fdroid.basic.ui.NavigationKey import org.fdroid.basic.ui.categories.CategoryList +import org.fdroid.basic.ui.main.BottomBar import org.fdroid.basic.ui.main.MainOverFlowMenu import org.fdroid.basic.ui.main.lists.AppList import org.fdroid.basic.ui.main.topBarMenuItems @@ -47,10 +50,12 @@ import org.fdroid.fdroid.ui.theme.FDroidContent @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) fun Discover( discoverModel: DiscoverModel, + numUpdates: Int, + isBigScreen: Boolean, modifier: Modifier = Modifier, onTitleTap: (AppList) -> Unit, onAppTap: (AppNavigationItem) -> Unit, - onMainNav: (String) -> Unit, + onNav: (NavKey) -> Unit, ) { val searchBarState = rememberSearchBarState() val scrollBehavior = enterAlwaysScrollBehavior(rememberTopAppBarState()) @@ -62,7 +67,7 @@ fun Discover( }, actions = { topBarMenuItems.forEach { dest -> - IconButton(onClick = { onMainNav(dest.id) }) { + IconButton(onClick = { onNav(dest.id) }) { Icon( imageVector = dest.icon, contentDescription = stringResource(dest.label), @@ -78,7 +83,7 @@ fun Discover( } MainOverFlowMenu(menuExpanded, { menuExpanded = false - onMainNav(it.id) + onNav(it.id) }) { menuExpanded = false } @@ -86,6 +91,9 @@ fun Discover( scrollBehavior = scrollBehavior, ) }, + bottomBar = { + if (!isBigScreen) BottomBar(numUpdates, NavigationKey.Discover, onNav) + }, modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection), ) { paddingValues -> Column( @@ -154,6 +162,13 @@ fun Discover( @Composable fun LoadingDiscoverPreview() { FDroidContent { - Discover(LoadingDiscoverModel(true), Modifier, {}, {}, {}) + Discover( + discoverModel = LoadingDiscoverModel(true), + numUpdates = 23, + isBigScreen = false, + onTitleTap = {}, + onAppTap = {}, + onNav = {}, + ) } } diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/discover/DiscoverScaffold.kt b/basic/src/main/java/org/fdroid/basic/ui/main/discover/DiscoverScaffold.kt deleted file mode 100644 index f93f22134..000000000 --- a/basic/src/main/java/org/fdroid/basic/ui/main/discover/DiscoverScaffold.kt +++ /dev/null @@ -1,150 +0,0 @@ -package org.fdroid.basic.ui.main.discover - -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Text -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole.Detail -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewScreenSizes -import androidx.compose.ui.unit.dp -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController -import kotlinx.coroutines.launch -import org.fdroid.basic.details.AppDetailsItem -import org.fdroid.basic.ui.main.apps.MinimalApp -import org.fdroid.basic.ui.main.details.AppDetails -import org.fdroid.basic.ui.main.lists.AppList -import org.fdroid.basic.ui.main.lists.FilterInfo -import org.fdroid.fdroid.ui.theme.FDroidContent - -enum class Sort { - NAME, - LATEST, -} - -@Composable -@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class) -fun DiscoverScaffold( - appList: AppList, - discoverModel: DiscoverModel, - filterInfo: FilterInfo, - onMainNav: (String) -> Unit, - onSelectAppItem: (MinimalApp) -> Unit, - onAppListChanged: (AppList) -> Unit, - currentItem: AppDetailsItem?, - modifier: Modifier = Modifier, -) { - val navigator = rememberListDetailPaneScaffoldNavigator() - val scope = rememberCoroutineScope() - BackHandler(enabled = navigator.canNavigateBack()) { - scope.launch { - navigator.navigateBack() - } - } - val isDetailVisible = navigator.scaffoldValue[Detail] == PaneAdaptedValue.Expanded - val isListVisible = - navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - modifier = modifier, - listPane = { - AnimatedPane { - val discoverNavController = rememberNavController() - NavHost(navController = discoverNavController, startDestination = "discover") { - composable("discover") { - Discover( - discoverModel = discoverModel, - onTitleTap = { - onAppListChanged(it) - discoverNavController.navigate("list") - }, - onAppTap = { - onSelectAppItem(it) - scope.launch { navigator.navigateTo(Detail, it) } - }, - onMainNav = { onMainNav(it) }, - modifier = Modifier, - ) - } - composable("list") { - AppList( - appList = appList, - filterInfo = filterInfo, - currentItem = if (isDetailVisible) currentItem else null, - onBackClicked = { discoverNavController.popBackStack() }, - modifier = Modifier, - ) { - onSelectAppItem(it) - scope.launch { navigator.navigateTo(Detail, it) } - } - } - } - } - }, - detailPane = { - AnimatedPane { - currentItem?.let { - val back: () -> Unit = { scope.launch { navigator.navigateBack() } } - AppDetails(it, if (isListVisible) null else back) - } ?: Text("No app selected", modifier = Modifier.padding(16.dp)) - } - }, - ) -} - -@Preview -@PreviewScreenSizes -@Composable -fun DiscoverScaffoldPreview() { - FDroidContent { - val apps = listOf( - AppNavigationItem("1", "foo", null, "bar", false), - AppNavigationItem("2", "foo", null, "bar", false), - AppNavigationItem("3", "foo", null, "bar", false), - ) - var filterExpanded by rememberSaveable { mutableStateOf(true) } - val filterInfo = object : FilterInfo { - override val model = FilterModel( - isLoading = false, - areFiltersShown = filterExpanded, - apps = apps, - sortBy = Sort.NAME, - allCategories = emptyList(), - addedCategories = emptyList(), - ) - - override fun toggleFilterVisibility() { - filterExpanded = !filterExpanded - } - - override fun sortBy(sort: Sort) {} - override fun addCategory(category: String) {} - override fun removeCategory(category: String) {} - } - DiscoverScaffold( - appList = AppList.New, - discoverModel = LoadingDiscoverModel(true), - filterInfo = filterInfo, - onMainNav = {}, - onSelectAppItem = {}, - onAppListChanged = {}, - currentItem = null, - ) - } -} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/lists/AppsFilter.kt b/basic/src/main/java/org/fdroid/basic/ui/main/lists/AppsFilter.kt index 35016f24e..3b018ff0b 100644 --- a/basic/src/main/java/org/fdroid/basic/ui/main/lists/AppsFilter.kt +++ b/basic/src/main/java/org/fdroid/basic/ui/main/lists/AppsFilter.kt @@ -26,8 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import org.fdroid.basic.ui.main.discover.FilterModel -import org.fdroid.basic.ui.main.discover.Sort interface FilterInfo { val model: FilterModel diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/discover/FilterPresenter.kt b/basic/src/main/java/org/fdroid/basic/ui/main/lists/FilterPresenter.kt similarity index 91% rename from basic/src/main/java/org/fdroid/basic/ui/main/discover/FilterPresenter.kt rename to basic/src/main/java/org/fdroid/basic/ui/main/lists/FilterPresenter.kt index b7b9f02f1..5aad35c1d 100644 --- a/basic/src/main/java/org/fdroid/basic/ui/main/discover/FilterPresenter.kt +++ b/basic/src/main/java/org/fdroid/basic/ui/main/lists/FilterPresenter.kt @@ -1,12 +1,18 @@ -package org.fdroid.basic.ui.main.discover +package org.fdroid.basic.ui.main.lists import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import org.fdroid.basic.ui.categories.Category +import org.fdroid.basic.ui.main.discover.AppNavigationItem import java.util.Locale +enum class Sort { + NAME, + LATEST, +} + @Composable fun FilterPresenter( areFiltersShownFlow: StateFlow, diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoriesScaffold.kt b/basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoriesScaffold.kt deleted file mode 100644 index 984de2cda..000000000 --- a/basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoriesScaffold.kt +++ /dev/null @@ -1,204 +0,0 @@ -package org.fdroid.basic.ui.main.repositories - -import android.content.res.Configuration.UI_MODE_NIGHT_YES -import android.content.res.Configuration.UI_MODE_TYPE_NORMAL -import android.os.Parcelable -import androidx.activity.compose.BackHandler -import androidx.compose.foundation.background -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Arrangement.spacedBy -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeDrawingPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LoadingIndicator -import androidx.compose.material3.LocalMinimumInteractiveComponentSize -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.VerticalDragHandle -import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi -import androidx.compose.material3.adaptive.layout.AnimatedPane -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole.Detail -import androidx.compose.material3.adaptive.layout.PaneAdaptedValue -import androidx.compose.material3.adaptive.layout.defaultDragHandleSemantics -import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -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.tooling.preview.PreviewScreenSizes -import androidx.compose.ui.unit.dp -import androidx.core.os.LocaleListCompat -import kotlinx.coroutines.launch -import kotlinx.parcelize.Parcelize -import org.fdroid.basic.R -import org.fdroid.fdroid.ui.theme.FDroidContent - -@Composable -@OptIn( - ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class, - ExperimentalMaterial3ExpressiveApi::class -) -fun RepositoriesScaffold( - repositories: List?, - currentRepository: Repository?, - onRepositorySelected: (Repository) -> Unit, - onAddRepo: () -> Unit, - onBackClicked: () -> Unit, -) { - val navigator = rememberListDetailPaneScaffoldNavigator() - val scope = rememberCoroutineScope() - BackHandler(enabled = navigator.canNavigateBack()) { - scope.launch { - navigator.navigateBack() - } - } - val isDetailVisible = navigator.scaffoldValue[Detail] == PaneAdaptedValue.Expanded - ListDetailPaneScaffold( - directive = navigator.scaffoldDirective, - value = navigator.scaffoldValue, - listPane = { - AnimatedPane { - Scaffold( - topBar = { - TopAppBar( - navigationIcon = { - IconButton(onClick = onBackClicked) { - Icon(Icons.AutoMirrored.Default.ArrowBack, "back") - } - }, - title = { - Text(stringResource(R.string.app_details_repositories)) - }, - ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = onAddRepo, - modifier = Modifier.padding(16.dp) - ) { - Icon(Icons.Default.Add, contentDescription = "Add repo") - } - } - ) { paddingValues -> - if (repositories == null) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize(), - ) { - LoadingIndicator(modifier = Modifier.padding(paddingValues)) - } - } else { - RepositoriesList( - repositories = repositories, - currentRepository = if (isDetailVisible) currentRepository else null, - onRepositorySelected = { - onRepositorySelected(it) - scope.launch { navigator.navigateTo(Detail, it) } - }, - modifier = Modifier - .padding(paddingValues), - ) - } - } - } - }, - detailPane = { - AnimatedPane { - Column( - verticalArrangement = spacedBy(16.dp), - modifier = Modifier - .background(MaterialTheme.colorScheme.background) - .safeDrawingPadding(), - ) { - currentRepository?.let { - Text(it.getName(LocaleListCompat.getDefault()) ?: "Unknown repo") - Text("This will basically be the repo details screen from latest client") - } ?: Text(text = "No repository selected") - } - } - }, - paneExpansionDragHandle = { state -> - val interactionSource = remember { MutableInteractionSource() } - VerticalDragHandle( - modifier = - Modifier.paneExpansionDraggable( - state, - LocalMinimumInteractiveComponentSize.current, - interactionSource, - state.defaultDragHandleSemantics(), - ), - interactionSource = interactionSource - ) - }, - modifier = Modifier.background(MaterialTheme.colorScheme.background), - ) -} - -@Preview( - showBackground = true, - uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL, -) -@Composable -fun RepositoriesScaffoldLoadingPreview() { - FDroidContent { - RepositoriesScaffold(null, null, {}, {}) { } - } -} - -@Preview( - showBackground = true, - uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL, -) -@PreviewScreenSizes -@Composable -fun RepositoriesScaffoldPreview() { - FDroidContent { - val repos = listOf( - Repository( - address = "http://example.org", - timestamp = System.currentTimeMillis(), - lastUpdated = null, - weight = 1, - enabled = true, - name = "My first repository", - ), - Repository( - address = "http://example.com", - timestamp = System.currentTimeMillis(), - lastUpdated = null, - weight = 2, - enabled = true, - name = "My second repository", - ), - ) - RepositoriesScaffold(repos, repos[0], {}, {}) { } - } -} - -@Parcelize -data class Repository( - val address: String, - val timestamp: Long, - val lastUpdated: Long?, - val weight: Int, - val enabled: Boolean, - private val name: String, -) : Parcelable { - fun getName(localeList: LocaleListCompat): String? = name -} diff --git a/basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoryList.kt b/basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoryList.kt new file mode 100644 index 000000000..a0d37bbc6 --- /dev/null +++ b/basic/src/main/java/org/fdroid/basic/ui/main/repositories/RepositoryList.kt @@ -0,0 +1,141 @@ +package org.fdroid.basic.ui.main.repositories + +import android.content.res.Configuration.UI_MODE_NIGHT_YES +import android.content.res.Configuration.UI_MODE_TYPE_NORMAL +import android.os.Parcelable +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 +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LoadingIndicator +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi +import androidx.compose.runtime.Composable +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 androidx.core.os.LocaleListCompat +import kotlinx.parcelize.Parcelize +import org.fdroid.basic.R +import org.fdroid.fdroid.ui.theme.FDroidContent + +@Composable +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalMaterial3AdaptiveApi::class, + ExperimentalMaterial3ExpressiveApi::class +) +fun RepositoryList( + repositories: List?, + currentRepository: Repository?, + onRepositorySelected: (Repository) -> Unit, + onAddRepo: () -> Unit, + onBackClicked: () -> Unit, +) { + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { + IconButton(onClick = onBackClicked) { + Icon(Icons.AutoMirrored.Default.ArrowBack, "back") + } + }, + title = { + Text(stringResource(R.string.app_details_repositories)) + }, + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = onAddRepo, + modifier = Modifier.padding(16.dp) + ) { + Icon(Icons.Default.Add, contentDescription = "Add repo") + } + } + ) { paddingValues -> + if (repositories == null) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { + LoadingIndicator(modifier = Modifier.padding(paddingValues)) + } + } else { + RepositoriesList( + repositories = repositories, + currentRepository = currentRepository, + onRepositorySelected = { + onRepositorySelected(it) + }, + modifier = Modifier + .padding(paddingValues), + ) + } + } +} + +@Preview( + showBackground = true, + uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL, +) +@Composable +fun RepositoriesScaffoldLoadingPreview() { + FDroidContent { + RepositoryList(null, null, {}, {}) { } + } +} + +@Preview( + showBackground = true, + uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL, +) +@Composable +fun RepositoriesScaffoldPreview() { + FDroidContent { + val repos = listOf( + Repository( + repoId = 1, + address = "http://example.org", + timestamp = System.currentTimeMillis(), + lastUpdated = null, + weight = 1, + enabled = true, + name = "My first repository", + ), + Repository( + repoId = 2, + address = "http://example.com", + timestamp = System.currentTimeMillis(), + lastUpdated = null, + weight = 2, + enabled = true, + name = "My second repository", + ), + ) + RepositoryList(repos, repos[0], {}, {}) { } + } +} + +@Parcelize +data class Repository( + val repoId: Long, + val address: String, + val timestamp: Long, + val lastUpdated: Long?, + val weight: Int, + val enabled: Boolean, + private val name: String, +) : Parcelable { + fun getName(localeList: LocaleListCompat): String? = name +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 51dd36a22..e5510c5cd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ dokka = "2.1.0" mavenPublish = "0.34.0" jlleitschuhKtlint = "13.1.0" +kotlinxSerializationCore = "1.8.1" kotlinxSerializationJson = "1.9.0" kotlinxCoroutinesTest = "1.10.2" @@ -36,8 +37,13 @@ androidxVectordrawable = "1.2.0" androidxGridlayout = "1.1.0" androidxComposeBom = "2025.09.01" androidxActivityCompose = "1.11.0" -androidxNavCompose = "2.9.1" accompanistDrawablepainter = "0.37.3" + +# navigation3 +nav3Core = "1.0.0-alpha10" +lifecycleViewmodelNav3 = "2.10.0-alpha04" +material3AdaptiveNav3 = "1.0.0-alpha03" + material = "1.13.0" #noinspection GradleDependency newer version need minSdk 24 or library desugering @@ -84,6 +90,7 @@ json = "20220320" [libraries] kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } +kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerializationCore" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" } @@ -110,13 +117,12 @@ androidx-compose-bom = { group = "androidx.compose", name = "compose-bom-alpha", androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" } androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose" } -androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavCompose" } +androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" } +androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" } +androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" } androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-compose-material3 = { module = "androidx.compose.material3:material3" } -androidx-compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive" } -androidx-compose-material3-adaptive-navigation = { module = "androidx.compose.material3.adaptive:adaptive-navigation" } -androidx-compose-material3-adaptive-layout = { module = "androidx.compose.material3.adaptive:adaptive-layout" } -androidx-compose-material3-adaptive-navigation-suite-android = { module = "androidx.compose.material3:material3-adaptive-navigation-suite-android" } +androidx-compose-material3-adaptive-navigation3 = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation3", version.ref = "material3AdaptiveNav3" } androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" } androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidxHiltCompiler" }