mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-06-22 23:01:17 -04:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
47
basic/src/main/java/org/fdroid/basic/ui/Navigation.kt
Normal file
47
basic/src/main/java/org/fdroid/basic/ui/Navigation.kt
Normal 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),
|
||||
}
|
||||
65
basic/src/main/java/org/fdroid/basic/ui/main/BottomBar.kt
Normal file
65
basic/src/main/java/org/fdroid/basic/ui/main/BottomBar.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
@@ -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() }
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>,
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user