Use experimental navigation3 library

which is still buggy, but so was the old implementation and this seems to be the future
This commit is contained in:
Torsten Grote
2025-07-16 17:29:35 -03:00
parent fa53a8fb11
commit acbb4320c2
17 changed files with 544 additions and 728 deletions

View File

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

View File

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

View File

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

View File

@@ -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<List<Repository>> = 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) {

View File

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

View File

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

View File

@@ -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<UpdatableApp>,
installed: List<InstalledApp>,
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 = {},
)
}

View File

@@ -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>(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<NavKey>()//(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<NavigationKey.Discover>(
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<NavigationKey.MyApps>(
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<NavigationKey.AppDetails>(
metadata = ListDetailSceneStrategy.detailPane("appdetails")
) {
val currentItem = viewModel.appDetails.collectAsStateWithLifecycle().value
it.packageName // TODO use?
AppDetails(
appItem = currentItem,
onBackNav = { backStack.removeLastOrNull() },
modifier = Modifier,
)
}
entry<NavigationKey.AppList>(
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<NavigationKey.Repos>(
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<NavigationKey.RepoDetails>(
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() }
}
},
)
}
}

View File

@@ -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<UpdatableApp>,
installedApps: List<InstalledApp>,
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,
)
}
}

View File

@@ -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<UpdatableApp>,
installedApps: List<InstalledApp>,
currentItem: AppDetailsItem?,
onSelectAppItem: (MinimalApp) -> Unit,
sortBy: Sort,
onSortChanged: (Sort) -> Unit,
modifier: Modifier = Modifier,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<MinimalApp>()
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>(Sort.NAME) }
MyAppsScaffold(
updatableApps = listOf(app1, app2),
installedApps = listOf(installedApp1, installedApp2, installedApp3),
currentItem = null,
onSelectAppItem = {},
sortBy = sortBy,
onSortChanged = { sortBy = it },
)
}
}

View File

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

View File

@@ -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<MinimalApp>()
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,
)
}
}

View File

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

View File

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

View File

@@ -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<Repository>?,
currentRepository: Repository?,
onRepositorySelected: (Repository) -> Unit,
onAddRepo: () -> Unit,
onBackClicked: () -> Unit,
) {
val navigator = rememberListDetailPaneScaffoldNavigator<Repository>()
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
}

View File

@@ -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<Repository>?,
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
}

View File

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