diff --git a/buildSrc/src/main/java/deps.kt b/buildSrc/src/main/java/deps.kt index 9cffc891f4..d27e670b10 100644 --- a/buildSrc/src/main/java/deps.kt +++ b/buildSrc/src/main/java/deps.kt @@ -13,7 +13,7 @@ object deps { const val kotlin = "1.8.20" const val okHttp = "4.9.1" const val retrofit = "2.9.0" - const val work = "2.7.1" + const val work = "2.9.0" const val navigation = "2.5.2" const val lifecycle = "2.6.1" const val leanback = "1.1.0-rc01" @@ -91,7 +91,6 @@ object deps { const val composeBom = "androidx.compose:compose-bom:${versions.composeBom}" const val material3 = "androidx.compose.material3:material3" const val extendedIcons = "androidx.compose.material:material-icons-extended" - const val liveData = "androidx.compose.runtime:runtime-livedata" const val tooling = "androidx.compose.ui:ui-tooling" const val toolingPreview = "androidx.compose.ui:ui-tooling-preview" diff --git a/lemuroid-app/build.gradle.kts b/lemuroid-app/build.gradle.kts index be8250b655..3fc57bcc8a 100644 --- a/lemuroid-app/build.gradle.kts +++ b/lemuroid-app/build.gradle.kts @@ -183,7 +183,6 @@ dependencies { debugImplementation(deps.libs.androidx.compose.tooling) implementation(deps.libs.androidx.compose.toolingPreview) implementation(deps.libs.androidx.compose.extendedIcons) - implementation(deps.libs.androidx.compose.liveData) implementation(deps.libs.androidx.compose.accompanist.systemUiController) implementation(deps.libs.androidx.compose.accompanist.navigationMaterial) implementation(deps.libs.androidx.compose.accompanist.drawablePainter) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesScreen.kt index e0209baef1..5fe71beb47 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/favorites/FavoritesScreen.kt @@ -3,6 +3,7 @@ package com.swordfish.lemuroid.app.mobile.feature.favorites import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable @@ -11,13 +12,12 @@ import androidx.compose.ui.unit.dp import androidx.paging.compose.collectAsLazyPagingItems import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidEmptyView import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidGameCard -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.lib.library.db.entity.Game @OptIn(ExperimentalFoundationApi::class) @Composable fun FavoritesScreen( - paddingValues: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: FavoritesViewModel, onGameClick: (Game) -> Unit, onGameLongClick: (Game) -> Unit, @@ -30,8 +30,9 @@ fun FavoritesScreen( } LazyVerticalGrid( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), columns = GridCells.Adaptive(144.dp), - contentPadding = (paddingValues + PaddingValues(16.dp)).asPaddingValues(), horizontalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesScreen.kt index 33123bf929..a4439fc230 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/games/GamesScreen.kt @@ -1,19 +1,19 @@ package com.swordfish.lemuroid.app.mobile.feature.games import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.paging.compose.collectAsLazyPagingItems import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidEmptyView import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidGameListRow -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.lib.library.db.entity.Game @OptIn(ExperimentalFoundationApi::class) @Composable fun GamesScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: GamesViewModel, onGameClick: (Game) -> Unit, onGameLongClick: (Game) -> Unit, @@ -26,9 +26,7 @@ fun GamesScreen( return } - LazyColumn( - contentPadding = padding.asPaddingValues(), - ) { + LazyColumn(modifier = modifier.fillMaxSize()) { items(games.itemCount, key = { games[it]?.id ?: it }) { index -> val game = games[index] ?: return@items diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeScreen.kt index 49b32bd52d..8f6618b6dc 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeScreen.kt @@ -30,13 +30,12 @@ import androidx.lifecycle.Lifecycle import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidGameCard import com.swordfish.lemuroid.app.utils.android.ComposableLifecycle -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.common.displayDetailsSettingsScreen import com.swordfish.lemuroid.lib.library.db.entity.Game @Composable fun HomeScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: HomeViewModel, onGameClick: (Game) -> Unit, onGameLongClick: (Game) -> Unit, @@ -64,7 +63,7 @@ fun HomeScreen( val state = viewModel.getViewStates().collectAsState(HomeViewModel.UIState()) HomeScreen( - padding, + modifier, state.value, onGameClick, onGameLongClick, @@ -81,20 +80,18 @@ fun HomeScreen( @Composable private fun HomeScreen( - paddings: MergedPaddingValues, + modifier: Modifier = Modifier, state: HomeViewModel.UIState, onGameClicked: (Game) -> Unit, onGameLongClick: (Game) -> Unit, onEnableNotificationsClicked: () -> Unit, onSetDirectoryClicked: () -> Unit, ) { - val finalPadding = paddings + PaddingValues(vertical = 16.dp) - Column( modifier = - Modifier + modifier .verticalScroll(rememberScrollState()) - .padding(finalPadding.asPaddingValues()), + .padding(top = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { AnimatedVisibility(state.showNoPermissionNotification) { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt index 6cbe36b9c8..361af5c179 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/home/HomeViewModel.kt @@ -7,10 +7,8 @@ import android.os.Build import androidx.core.content.ContextCompat import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor -import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor import com.swordfish.lemuroid.app.shared.settings.StorageFrameworkPickerLauncher import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import com.swordfish.lemuroid.lib.library.db.entity.Game @@ -27,7 +25,6 @@ import kotlinx.coroutines.launch class HomeViewModel( appContext: Context, retrogradeDb: RetrogradeDatabase, - private val settingsInteractor: SettingsInteractor, ) : ViewModel() { companion object { const val CAROUSEL_MAX_ITEMS = 10 @@ -37,10 +34,9 @@ class HomeViewModel( class Factory( val appContext: Context, val retrogradeDb: RetrogradeDatabase, - val settingsInteractor: SettingsInteractor, ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return HomeViewModel(appContext, retrogradeDb, settingsInteractor) as T + return HomeViewModel(appContext, retrogradeDb) as T } } @@ -120,7 +116,7 @@ class HomeViewModel( } private fun indexingInProgress(appContext: Context) = - PendingOperationsMonitor(appContext).anyLibraryOperationInProgress().asFlow() + PendingOperationsMonitor(appContext).anyLibraryOperationInProgress() private fun discoveryGames(retrogradeDb: RetrogradeDatabase) = retrogradeDb.gameDao().selectFirstNotPlayed(CAROUSEL_MAX_ITEMS) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt index 3b6c8c477f..6093873e76 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainActivity.kt @@ -5,13 +5,17 @@ import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent import androidx.activity.viewModels +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost @@ -43,7 +47,6 @@ import com.swordfish.lemuroid.app.mobile.feature.shortcuts.ShortcutsGenerator import com.swordfish.lemuroid.app.mobile.feature.systems.MetaSystemsScreen import com.swordfish.lemuroid.app.mobile.feature.systems.MetaSystemsViewModel import com.swordfish.lemuroid.app.mobile.shared.compose.ui.AppTheme -import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidScaffold import com.swordfish.lemuroid.app.shared.GameInteractor import com.swordfish.lemuroid.app.shared.game.BaseGameActivity import com.swordfish.lemuroid.app.shared.game.GameLauncher @@ -51,7 +54,6 @@ import com.swordfish.lemuroid.app.shared.input.InputDeviceManager import com.swordfish.lemuroid.app.shared.main.BusyActivity import com.swordfish.lemuroid.app.shared.main.GameLaunchTaskHandler import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.common.coroutines.safeLaunch import com.swordfish.lemuroid.ext.feature.review.ReviewManager import com.swordfish.lemuroid.lib.android.RetrogradeComponentActivity @@ -116,6 +118,7 @@ class MainActivity : RetrogradeComponentActivity(), BusyActivity { } } + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun MainScreen(navController: NavHostController) { AppTheme { @@ -124,6 +127,7 @@ class MainActivity : RetrogradeComponentActivity(), BusyActivity { val currentRoute = currentDestination?.route ?.let { MainRoute.findByRoute(it) } + ?: MainRoute.HOME val infoDialogDisplayed = remember { @@ -131,7 +135,7 @@ class MainActivity : RetrogradeComponentActivity(), BusyActivity { } LaunchedEffect(currentRoute) { - mainViewModel.update() + mainViewModel.changeRoute(currentRoute) } val selectedGameState = @@ -157,81 +161,71 @@ class MainActivity : RetrogradeComponentActivity(), BusyActivity { val mainUIState = mainViewModel.state - .observeAsState(MainViewModel.UiState()) + .collectAsState(MainViewModel.UiState()) .value - // TODO COMPOSE Get rid of this double scaffold. Scaffold( - bottomBar = { MainNavigationBar(currentRoute, navController) }, - ) { outerPadding -> - - @Composable - fun Page( - mainRoute: MainRoute, - content: @Composable (MergedPaddingValues) -> Unit, - ) { - LemuroidScaffold( - mainRoute, - navController, - mainUIState, - outerPadding, - content, - onHelpPressed, + topBar = { + MainTopBar( + currentRoute = currentRoute, + navController = navController, + onHelpPressed = onHelpPressed, + mainUIState = mainUIState, + onUpdateQueryString = { mainViewModel.changeQueryString(it) }, ) - } - + }, + bottomBar = { MainNavigationBar(currentRoute, navController) }, + ) { padding -> NavHost( + modifier = Modifier.fillMaxSize(), navController = navController, startDestination = MainRoute.HOME.route, ) { composable(MainRoute.HOME) { - Page(MainRoute.HOME) { padding -> - HomeScreen( - padding, + HomeScreen( + modifier = Modifier.padding(padding), + viewModel = viewModel( factory = HomeViewModel.Factory( applicationContext, retrogradeDb, - settingsInteractor, ), ), - onGameClick = onGameClick, - onGameLongClick = onGameLongClick, - ) - } + onGameClick = onGameClick, + onGameLongClick = onGameLongClick, + ) } composable(MainRoute.FAVORITES) { - Page(MainRoute.FAVORITES) { padding -> - FavoritesScreen( - padding, + FavoritesScreen( + modifier = Modifier.padding(padding), + viewModel = viewModel( factory = FavoritesViewModel.Factory(retrogradeDb), ), - onGameClick = onGameClick, - onGameLongClick = onGameLongClick, - ) - } + onGameClick = onGameClick, + onGameLongClick = onGameLongClick, + ) } composable(MainRoute.SEARCH) { SearchScreen( - outerPadding, - navController, - viewModel( - factory = SearchViewModel.Factory(retrogradeDb), - ), - mainUIState = mainUIState, + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = SearchViewModel.Factory(retrogradeDb), + ), + searchQuery = mainUIState.searchQuery, onGameClick = onGameClick, onGameLongClick = onGameLongClick, onGameFavoriteToggle = onGameFavoriteToggle, - onHelpPressed = onHelpPressed, + onResetSearchQuery = { mainViewModel.changeQueryString("") }, ) } composable(MainRoute.SYSTEMS) { - Page(MainRoute.SYSTEMS) { padding -> - MetaSystemsScreen( - padding, - navController, + MetaSystemsScreen( + modifier = Modifier.padding(padding), + navController = navController, + viewModel = viewModel( factory = MetaSystemsViewModel.Factory( @@ -239,121 +233,106 @@ class MainActivity : RetrogradeComponentActivity(), BusyActivity { applicationContext, ), ), - ) - } + ) } composable(MainRoute.SYSTEM_GAMES) { entry -> - Page(MainRoute.SYSTEM_GAMES) { padding -> - val metaSystemId = entry.arguments?.getString("metaSystemId") - GamesScreen( - padding, - viewModel = - viewModel( - factory = - GamesViewModel.Factory( - retrogradeDb, - MetaSystemID.valueOf(metaSystemId!!), - ), - ), - onGameClick = onGameClick, - onGameLongClick = onGameLongClick, - onGameFavoriteToggle = onGameFavoriteToggle, - ) - } + val metaSystemId = entry.arguments?.getString("metaSystemId") + GamesScreen( + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = + GamesViewModel.Factory( + retrogradeDb, + MetaSystemID.valueOf(metaSystemId!!), + ), + ), + onGameClick = onGameClick, + onGameLongClick = onGameLongClick, + onGameFavoriteToggle = onGameFavoriteToggle, + ) } composable(MainRoute.SETTINGS) { - Page(MainRoute.SETTINGS) { padding -> - SettingsScreen( - padding, - viewModel = - viewModel( - factory = - SettingsViewModel.Factory( - applicationContext, - settingsInteractor, - saveSyncManager, - FlowSharedPreferences( - SharedPreferencesHelper.getLegacySharedPreferences( - applicationContext, - ), + SettingsScreen( + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = + SettingsViewModel.Factory( + applicationContext, + settingsInteractor, + saveSyncManager, + FlowSharedPreferences( + SharedPreferencesHelper.getLegacySharedPreferences( + applicationContext, ), ), - ), - navController = navController, - ) - } + ), + ), + navController = navController, + ) } composable(MainRoute.SETTINGS_ADVANCED) { - Page(MainRoute.SETTINGS_ADVANCED) { padding -> - AdvancedSettingsScreen( - padding, - viewModel = - viewModel( - factory = - AdvancedSettingsViewModel.Factory( - applicationContext, - settingsInteractor, - ), - ), - navController, - ) - } + AdvancedSettingsScreen( + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = + AdvancedSettingsViewModel.Factory( + applicationContext, + settingsInteractor, + ), + ), + navController = navController, + ) } composable(MainRoute.SETTINGS_BIOS) { - Page(MainRoute.SETTINGS_BIOS) { padding -> - BiosScreen( - padding, - viewModel = - viewModel( - factory = BiosSettingsViewModel.Factory(biosManager), - ), - ) - } + BiosScreen( + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = BiosSettingsViewModel.Factory(biosManager), + ), + ) } composable(MainRoute.SETTINGS_CORES_SELECTION) { - Page(MainRoute.SETTINGS_CORES_SELECTION) { padding -> - CoresSelectionScreen( - padding, - viewModel = - viewModel( - factory = - CoresSelectionViewModel.Factory( - applicationContext, - coresSelection, - ), - ), - ) - } + CoresSelectionScreen( + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = + CoresSelectionViewModel.Factory( + applicationContext, + coresSelection, + ), + ), + ) } composable(MainRoute.SETTINGS_INPUT_DEVICES) { - Page(MainRoute.SETTINGS_INPUT_DEVICES) { padding -> - InputDevicesSettingsScreen( - padding, - viewModel = - viewModel( - factory = - InputDevicesSettingsViewModel.Factory( - applicationContext, - inputDeviceManager, - ), - ), - ) - } + InputDevicesSettingsScreen( + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = + InputDevicesSettingsViewModel.Factory( + applicationContext, + inputDeviceManager, + ), + ), + ) } composable(MainRoute.SETTINGS_SAVE_SYNC) { - Page(MainRoute.SETTINGS_SAVE_SYNC) { padding -> - SaveSyncSettingsScreen( - padding, - viewModel = - viewModel( - factory = - SaveSyncSettingsViewModel.Factory( - application, - saveSyncManager, - ), - ), - ) - } + SaveSyncSettingsScreen( + modifier = Modifier.padding(padding), + viewModel = + viewModel( + factory = + SaveSyncSettingsViewModel.Factory( + application, + saveSyncManager, + ), + ), + ) } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainNavigationBar.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainNavigationBar.kt index 4d8e9d171c..bc2e61a1af 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainNavigationBar.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainNavigationBar.kt @@ -1,13 +1,15 @@ package com.swordfish.lemuroid.app.mobile.feature.main import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBar import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.navigation.NavGraph.Companion.findStartDestination import androidx.navigation.NavHostController @@ -19,40 +21,48 @@ fun MainNavigationBar( ) { AnimatedVisibility( visible = currentRoute?.showBottomNavigation != false, - enter = slideInVertically { it }, - exit = slideOutVertically { it }, + enter = expandVertically(), + exit = shrinkVertically(), ) { - NavigationBar { - MainNavigationRoutes.values().forEach { destination -> - val isSelected = currentRoute?.root == destination.route - val iconDrawable = if (isSelected) destination.selectedIcon else destination.unselectedIcon + LemuroidNavigationBar(currentRoute, navController) + } +} + +@Composable +private fun LemuroidNavigationBar( + currentRoute: MainRoute?, + navController: NavHostController, +) { + NavigationBar(modifier = Modifier.fillMaxWidth()) { + MainNavigationRoutes.values().forEach { destination -> + val isSelected = currentRoute?.root == destination.route + val iconDrawable = if (isSelected) destination.selectedIcon else destination.unselectedIcon - NavigationBarItem( - icon = { - Icon( - imageVector = iconDrawable, - contentDescription = stringResource(destination.titleId), - ) - }, - label = { Text(stringResource(destination.titleId)) }, - selected = isSelected, - onClick = { - navController.navigate(destination.route.route) { - // Pop up to the start destination of the graph to - // avoid building up a large stack of destinations - // on the back stack as users select items - popUpTo(navController.graph.findStartDestination().id) { - saveState = false - } - // Avoid multiple copies of the same destination when - // reselecting the same item - launchSingleTop = true - // Restore state when reselecting a previously selected item - restoreState = false + NavigationBarItem( + icon = { + Icon( + imageVector = iconDrawable, + contentDescription = stringResource(destination.titleId), + ) + }, + label = { Text(stringResource(destination.titleId)) }, + selected = isSelected, + onClick = { + navController.navigate(destination.route.route) { + // Pop up to the start destination of the graph to + // avoid building up a large stack of destinations + // on the back stack as users select items + popUpTo(navController.graph.findStartDestination().id) { + saveState = false } - }, - ) - } + // Avoid multiple copies of the same destination when + // reselecting the same item + launchSingleTop = true + // Restore state when reselecting a previously selected item + restoreState = false + } + }, + ) } } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainTopBar.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainTopBar.kt new file mode 100644 index 0000000000..924eb19a4c --- /dev/null +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainTopBar.kt @@ -0,0 +1,224 @@ +package com.swordfish.lemuroid.app.mobile.feature.main + +import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.outlined.CloudSync +import androidx.compose.material.icons.outlined.Info +import androidx.compose.material.icons.outlined.Settings +import androidx.compose.material3.BottomAppBarDefaults +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.navigation.NavController +import androidx.navigation.NavHostController +import com.swordfish.lemuroid.R +import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork + +@Composable +fun MainTopBar( + currentRoute: MainRoute, + navController: NavHostController, + onHelpPressed: () -> Unit, + onUpdateQueryString: (String) -> Unit, + mainUIState: MainViewModel.UiState, +) { + Column { + LemuroidTopAppBar( + route = currentRoute, + navController = navController, + mainUIState = mainUIState, + onHelpPressed = onHelpPressed, + onUpdateQueryString = onUpdateQueryString, + ) + + AnimatedVisibility(mainUIState.operationInProgress) { + LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LemuroidTopAppBar( + route: MainRoute, + navController: NavController, + mainUIState: MainViewModel.UiState, + onHelpPressed: () -> Unit, + onUpdateQueryString: (String) -> Unit, +) { + val context = LocalContext.current + val topBarColor = + MaterialTheme.colorScheme.surfaceColorAtElevation( + BottomAppBarDefaults.ContainerElevation, + ) + + TopAppBar( + title = { + if (route == MainRoute.SEARCH) { + LemuroidSearchView( + mainUIState = mainUIState, + onUpdateQueryString = onUpdateQueryString, + ) + } else { + Text(text = stringResource(route.titleId)) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + scrolledContainerColor = topBarColor, + containerColor = topBarColor, + ), + navigationIcon = { + AnimatedVisibility( + visible = route.parent != null, + enter = fadeIn(), + exit = fadeOut(), + ) { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + stringResource(id = R.string.back), + ) + } + } + }, + actions = { + LemuroidTopBarActions( + route = route, + navController = navController, + context = context, + saveSyncEnabled = mainUIState.saveSyncEnabled, + onHelpPressed = onHelpPressed, + operationsInProgress = mainUIState.operationInProgress, + ) + }, + ) +} + +@Composable +fun LemuroidTopBarActions( + route: MainRoute, + navController: NavController, + context: Context, + saveSyncEnabled: Boolean, + operationsInProgress: Boolean, + onHelpPressed: () -> Unit, +) { + Row { + IconButton( + onClick = { onHelpPressed() }, + ) { + Icon( + Icons.Outlined.Info, + stringResource(R.string.mobile_settings_help), + ) + } + if (saveSyncEnabled) { + IconButton( + onClick = { SaveSyncWork.enqueueManualWork(context.applicationContext) }, + enabled = !operationsInProgress, + ) { + Icon( + Icons.Outlined.CloudSync, + stringResource(R.string.save_sync), + ) + } + } + if (route.showBottomNavigation) { + IconButton( + onClick = { navController.navigate(MainRoute.SETTINGS.route) }, + ) { + Icon( + Icons.Outlined.Settings, + stringResource(R.string.settings), + ) + } + } + } +} + +@Composable +private fun LemuroidSearchView( + mainUIState: MainViewModel.UiState, + onUpdateQueryString: (String) -> Unit, +) { + val focusRequester = remember { FocusRequester() } + val focusManager = LocalFocusManager.current + + LaunchedEffect(Unit) { + focusRequester.requestFocus() + } + + Box( + modifier = + Modifier + .fillMaxWidth() + .height(56.dp), + ) { + Surface( + modifier = + Modifier + .fillMaxSize() + .padding(top = 8.dp, bottom = 8.dp, end = 8.dp), + shape = RoundedCornerShape(100), + tonalElevation = 16.dp, + ) { } + + TextField( + value = mainUIState.searchQuery, + modifier = + Modifier + .fillMaxSize() + .focusRequester(focusRequester), + textStyle = MaterialTheme.typography.bodyMedium, + leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, + onValueChange = { onUpdateQueryString(it) }, + singleLine = true, + keyboardActions = + KeyboardActions( + onDone = { focusManager.clearFocus(true) }, + ), + colors = + TextFieldDefaults.colors( + focusedContainerColor = Color.Transparent, + unfocusedContainerColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + ), + ) + } +} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt index 1a049d84f5..5746e03bd3 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/main/MainViewModel.kt @@ -1,12 +1,16 @@ package com.swordfish.lemuroid.app.mobile.feature.main import android.content.Context -import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor -import com.swordfish.lemuroid.app.utils.livedata.CombinedLiveData import com.swordfish.lemuroid.lib.savesync.SaveSyncManager +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn class MainViewModel(appContext: Context, private val saveSyncManager: SaveSyncManager) : ViewModel() { class Factory( @@ -21,29 +25,49 @@ class MainViewModel(appContext: Context, private val saveSyncManager: SaveSyncMa data class UiState( val operationInProgress: Boolean = false, val saveSyncEnabled: Boolean = false, + val displaySearch: Boolean = false, + val searchQuery: String = "", ) - private val saveSyncEnabledLiveData = MutableLiveData(false) - private val operationInProgressLiveData = - PendingOperationsMonitor(appContext) - .anyOperationInProgress() + private val currentRouteFlow = MutableStateFlow(MainRoute.HOME) + private val saveSyncEnabledFlow = MutableStateFlow(false) + private val operationInProgressFlow = PendingOperationsMonitor(appContext).anyOperationInProgress() + private val searchQueryFlow = MutableStateFlow("") - val state = - CombinedLiveData( - saveSyncEnabledLiveData, - operationInProgressLiveData, - this::buildState, - ) + val state = buildStateFlow() - fun update() { + private fun buildStateFlow(): StateFlow { + val combinedFlows = + combine( + currentRouteFlow, + saveSyncEnabledFlow, + operationInProgressFlow, + searchQueryFlow, + ) { currentRoute, saveSyncEnabled, operationInProgress, searchQuery -> + UiState( + operationInProgress = operationInProgress, + saveSyncEnabled = saveSyncEnabled, + displaySearch = currentRoute == MainRoute.SEARCH, + searchQuery = searchQuery, + ) + } + + return combinedFlows + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = UiState(), + ) + } + + fun changeRoute(currentRoute: MainRoute) { val current = saveSyncManager.isSupported() && saveSyncManager.isConfigured() - saveSyncEnabledLiveData.postValue(current) + saveSyncEnabledFlow.value = current + + currentRouteFlow.value = currentRoute } - private fun buildState( - saveSyncEnabled: Boolean, - operationInProgress: Boolean, - ): UiState { - return UiState(operationInProgress, saveSyncEnabled) + fun changeQueryString(newSearchQuery: String) { + searchQueryFlow.value = newSearchQuery } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchScreen.kt index e3ed15519d..494494b6c1 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/search/SearchScreen.kt @@ -6,169 +6,69 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Search import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Surface -import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import com.swordfish.lemuroid.R -import com.swordfish.lemuroid.app.mobile.feature.main.MainRoute -import com.swordfish.lemuroid.app.mobile.feature.main.MainViewModel import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidEmptyView import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidGameListRow -import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidTopAppBarContainer -import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidTopBarActions -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues -import com.swordfish.lemuroid.app.utils.android.compose.plus import com.swordfish.lemuroid.lib.library.db.entity.Game -@OptIn(ExperimentalMaterial3Api::class) @Composable fun SearchScreen( - outerPadding: PaddingValues, - navController: NavController, + modifier: Modifier = Modifier, viewModel: SearchViewModel, - mainUIState: MainViewModel.UiState, + searchQuery: String, onGameClick: (Game) -> Unit, onGameLongClick: (Game) -> Unit, onGameFavoriteToggle: (Game, Boolean) -> Unit, - onHelpPressed: () -> Unit, + onResetSearchQuery: () -> Unit, ) { - val context = LocalContext.current - val query = viewModel.queryString.collectAsState() val searchState = viewModel.searchState.collectAsState(SearchViewModel.UIState.Idle) val searchGames = viewModel.searchResults.collectAsLazyPagingItems() - val focusRequester = FocusRequester() - val focusManager = LocalFocusManager.current - LaunchedEffect(Unit) { - focusRequester.requestFocus() + onResetSearchQuery() + } + + LaunchedEffect(key1 = searchQuery) { + viewModel.queryString.value = searchQuery } - Scaffold( - topBar = { - LemuroidTopAppBarContainer(mainUIState.operationInProgress) { - Row( - modifier = - Modifier - .fillMaxWidth() - .height(64.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Box( - modifier = Modifier.weight(1f), - ) { - // Standard TextField has huge margins that can't be customized. - // We set the background to invisible and draw a surface behind - Surface( - modifier = - Modifier - .fillMaxSize() - .padding(8.dp), - shape = RoundedCornerShape(100), - tonalElevation = 16.dp, - ) { } - TextField( - value = query.value, - modifier = - Modifier - .fillMaxSize() - .padding(start = 8.dp, end = 8.dp) - .focusRequester(focusRequester), - leadingIcon = { Icon(Icons.Default.Search, contentDescription = null) }, - onValueChange = { viewModel.queryString.value = it }, - singleLine = true, - keyboardActions = - KeyboardActions( - onDone = { focusManager.clearFocus(true) }, - ), - colors = - TextFieldDefaults.colors( - focusedContainerColor = Color.Transparent, - unfocusedContainerColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - ), - ) - } + AnimatedContent( + targetState = searchState.value, + label = "SearchContent", + transitionSpec = { fadeIn() togetherWith fadeOut() }, + ) { state -> + when { + state == SearchViewModel.UIState.Idle -> { + SearchEmptyView(modifier, stringResource(R.string.game_page_search_suggestion)) + } - val colors = TopAppBarDefaults.topAppBarColors() - CompositionLocalProvider( - LocalContentColor provides colors.actionIconContentColor, - content = { - Box(modifier = Modifier.padding(4.dp)) { - LemuroidTopBarActions( - MainRoute.SEARCH, - navController, - onHelpPressed = onHelpPressed, - context = context, - saveSyncEnabled = mainUIState.saveSyncEnabled, - operationsInProgress = mainUIState.operationInProgress, - ) - } - }, - ) - } + state == SearchViewModel.UIState.Loading -> { + SearchLoadingView(modifier) } - }, - ) { innerPadding -> - val padding = outerPadding + innerPadding - AnimatedContent( - targetState = searchState.value, - label = "SearchContent", - transitionSpec = { fadeIn() togetherWith fadeOut() }, - ) { state -> - when { - state == SearchViewModel.UIState.Idle -> { - SearchEmptyView(padding, stringResource(R.string.game_page_search_suggestion)) - } - state == SearchViewModel.UIState.Loading -> { - SearchLoadingView(padding) - } - state == SearchViewModel.UIState.Ready && searchGames.itemCount == 0 -> { - SearchEmptyView(padding, stringResource(id = R.string.empty_view_default)) - } - else -> { - SearchResultsView( - padding, - searchGames, - onGameClick, - onGameLongClick, - onGameFavoriteToggle, - ) - } + + state == SearchViewModel.UIState.Ready && searchGames.itemCount == 0 -> { + SearchEmptyView(modifier, stringResource(id = R.string.empty_view_default)) + } + + else -> { + SearchResultsView( + modifier, + searchGames, + onGameClick, + onGameLongClick, + onGameFavoriteToggle, + ) } } } @@ -177,13 +77,13 @@ fun SearchScreen( @OptIn(ExperimentalFoundationApi::class) @Composable private fun SearchResultsView( - padding: MergedPaddingValues, + modifier: Modifier, games: LazyPagingItems, onGameClick: (Game) -> Unit, onGameLongClick: (Game) -> Unit, onGameFavoriteToggle: (Game, Boolean) -> Unit, ) { - LazyColumn(contentPadding = padding.asPaddingValues()) { + LazyColumn(modifier = modifier) { items(games.itemCount, key = { games[it]?.id ?: it }) { index -> val game = games[index] ?: return@items @@ -202,22 +102,19 @@ private fun SearchResultsView( @Composable private fun SearchEmptyView( - padding: MergedPaddingValues, + modifier: Modifier, text: String, ) { LemuroidEmptyView( - modifier = Modifier.padding(padding.asPaddingValues()), + modifier = modifier, text = text, ) } @Composable -private fun SearchLoadingView(padding: MergedPaddingValues) { +private fun SearchLoadingView(modifier: Modifier) { Box( - modifier = - Modifier - .fillMaxSize() - .padding(padding.asPaddingValues()), + modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { CircularProgressIndicator() diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/advanced/AdvancedSettingsScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/advanced/AdvancedSettingsScreen.kt index 32d2c1447e..9da7a169a1 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/advanced/AdvancedSettingsScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/advanced/AdvancedSettingsScreen.kt @@ -1,6 +1,6 @@ package com.swordfish.lemuroid.app.mobile.feature.settings.advanced -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton @@ -16,7 +16,6 @@ import androidx.navigation.NavHostController import com.alorma.compose.settings.storage.disk.rememberPreferenceIntSettingState import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.feature.main.MainRoute -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.app.utils.android.settings.LemuroidCardSettingsGroup import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsList import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsMenuLink @@ -28,7 +27,7 @@ import com.swordfish.lemuroid.app.utils.android.settings.indexPreferenceState @Composable fun AdvancedSettingsScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: AdvancedSettingsViewModel, navController: NavHostController, ) { @@ -38,9 +37,7 @@ fun AdvancedSettingsScreen( .value LemuroidSettingsPage( - modifier = - Modifier - .padding(padding.asPaddingValues()), + modifier = modifier.fillMaxSize(), ) { if (uiState?.cache == null) { return@LemuroidSettingsPage diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/bios/BiosSettingsScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/bios/BiosSettingsScreen.kt index 5cb683936b..b9d1ce127d 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/bios/BiosSettingsScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/bios/BiosSettingsScreen.kt @@ -1,13 +1,12 @@ package com.swordfish.lemuroid.app.mobile.feature.settings.bios -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import com.swordfish.lemuroid.R -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.app.utils.android.settings.LemuroidCardSettingsGroup import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsMenuLink import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsPage @@ -15,7 +14,7 @@ import com.swordfish.lemuroid.lib.bios.Bios @Composable fun BiosScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: BiosSettingsViewModel, ) { val uiState = @@ -23,7 +22,7 @@ fun BiosScreen( .collectAsState() .value - LemuroidSettingsPage(modifier = Modifier.padding(padding.asPaddingValues())) { + LemuroidSettingsPage(modifier = modifier.fillMaxSize()) { if (uiState.detected.isNotEmpty()) { DetectedEntries(uiState.detected) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/coreselection/CoresSelectionScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/coreselection/CoresSelectionScreen.kt index 4e1081238d..cd778819bb 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/coreselection/CoresSelectionScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/coreselection/CoresSelectionScreen.kt @@ -1,37 +1,29 @@ package com.swordfish.lemuroid.app.mobile.feature.settings.coreselection import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.alorma.compose.settings.storage.memory.rememberMemoryIntSettingState -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.app.utils.android.settings.LemuroidCardSettingsGroup import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsList import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsPage @Composable fun CoresSelectionScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: CoresSelectionViewModel, ) { val applicationContext = LocalContext.current.applicationContext val cores = viewModel.getSelectedCores().collectAsState(emptyList()).value - val indexingInProgress = viewModel.indexingInProgress.observeAsState(false).value + val indexingInProgress = viewModel.indexingInProgress.collectAsState(false).value - LemuroidSettingsPage( - modifier = - Modifier - .fillMaxWidth() - .padding(padding.asPaddingValues()), - ) { + LemuroidSettingsPage(modifier = modifier.fillMaxWidth()) { LemuroidCardSettingsGroup { cores.forEach { (system, core) -> val state = rememberMemoryIntSettingState(system.systemCoreConfigs.indexOf(core)) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/general/SettingsScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/general/SettingsScreen.kt index 315a49b863..94093af556 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/general/SettingsScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/general/SettingsScreen.kt @@ -1,11 +1,9 @@ package com.swordfish.lemuroid.app.mobile.feature.settings.general import android.net.Uri -import androidx.compose.foundation.layout.padding import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -16,7 +14,6 @@ import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.feature.main.MainRoute import com.swordfish.lemuroid.app.mobile.feature.main.navigateToRoute import com.swordfish.lemuroid.app.shared.library.LibraryIndexScheduler -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.app.utils.android.settings.LemuroidCardSettingsGroup import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsList import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsMenuLink @@ -28,7 +25,7 @@ import com.swordfish.lemuroid.app.utils.android.stringListResource @Composable fun SettingsScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: SettingsViewModel, navController: NavController, ) { @@ -39,15 +36,15 @@ fun SettingsScreen( val scanInProgress = viewModel.directoryScanInProgress - .observeAsState(false) + .collectAsState(false) .value val indexingInProgress = viewModel.indexingInProgress - .observeAsState(false) + .collectAsState(false) .value - LemuroidSettingsPage(modifier = Modifier.padding(padding.asPaddingValues())) { + LemuroidSettingsPage(modifier = modifier) { RomsSettings( state = state, onChangeFolder = { viewModel.changeLocalStorageFolder() }, diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/inputdevices/InputDevicesSettingsScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/inputdevices/InputDevicesSettingsScreen.kt index 37e6e6e888..11d8fd79a8 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/inputdevices/InputDevicesSettingsScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/inputdevices/InputDevicesSettingsScreen.kt @@ -3,7 +3,7 @@ package com.swordfish.lemuroid.app.mobile.feature.settings.inputdevices import android.content.Intent import android.view.InputDevice import android.view.KeyEvent -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState @@ -16,7 +16,6 @@ import com.swordfish.lemuroid.app.shared.input.InputBindingUpdater import com.swordfish.lemuroid.app.shared.input.InputDeviceManager import com.swordfish.lemuroid.app.shared.input.InputKey import com.swordfish.lemuroid.app.shared.input.lemuroiddevice.getLemuroidInputDevice -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.app.utils.android.settings.LemuroidCardSettingsGroup import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsList import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsMenuLink @@ -27,7 +26,7 @@ import com.swordfish.lemuroid.app.utils.android.settings.indexPreferenceState @Composable fun InputDevicesSettingsScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: InputDevicesSettingsViewModel, ) { val state = @@ -35,7 +34,7 @@ fun InputDevicesSettingsScreen( .collectAsState(InputDevicesSettingsViewModel.State()) .value - LemuroidSettingsPage(modifier = Modifier.padding(padding.asPaddingValues())) { + LemuroidSettingsPage(modifier = modifier.fillMaxSize()) { EnabledDeviceCategory(state) state.bindings.forEach { (device, bindings) -> DeviceBindingCategory(device, bindings) diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/savesync/SaveSyncSettingsScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/savesync/SaveSyncSettingsScreen.kt index 7166b65c7b..4450cccb42 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/savesync/SaveSyncSettingsScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/settings/savesync/SaveSyncSettingsScreen.kt @@ -1,17 +1,15 @@ package com.swordfish.lemuroid.app.mobile.feature.settings.savesync import android.content.Intent -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues import com.swordfish.lemuroid.app.utils.android.settings.LemuroidCardSettingsGroup import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsListMultiSelect import com.swordfish.lemuroid.app.utils.android.settings.LemuroidSettingsMenuLink @@ -22,7 +20,7 @@ import com.swordfish.lemuroid.app.utils.android.stringsSetPreferenceState @Composable fun SaveSyncSettingsScreen( - padding: MergedPaddingValues, + modifier: Modifier = Modifier, viewModel: SaveSyncSettingsViewModel, ) { val context = LocalContext.current @@ -34,10 +32,10 @@ fun SaveSyncSettingsScreen( val isSyncInProgress = viewModel.indexingInProgress - .observeAsState(true) + .collectAsState(true) .value - LemuroidSettingsPage(modifier = Modifier.padding(padding.asPaddingValues())) { + LemuroidSettingsPage(modifier = modifier.fillMaxSize()) { LemuroidCardSettingsGroup { LemuroidSettingsMenuLink( title = { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemScreen.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemScreen.kt index c35e0a9953..d752f7c765 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemScreen.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/feature/systems/MetaSystemScreen.kt @@ -1,7 +1,9 @@ package com.swordfish.lemuroid.app.mobile.feature.systems import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.runtime.Composable @@ -12,17 +14,16 @@ import androidx.navigation.NavController import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidEmptyView import com.swordfish.lemuroid.app.mobile.shared.compose.ui.LemuroidSystemCard import com.swordfish.lemuroid.app.shared.systems.MetaSystemInfo -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues @Composable fun MetaSystemsScreen( - paddings: MergedPaddingValues, + modifier: Modifier = Modifier, navController: NavController, viewModel: MetaSystemsViewModel, ) { val metaSystems = viewModel.availableMetaSystems.collectAsState(emptyList()) MetaSystemsScreen( - paddings = paddings, + modifier = modifier, metaSystems = metaSystems.value, onSystemClicked = { navController.navigate("systems/${it.metaSystem.name}") }, ) @@ -30,10 +31,10 @@ fun MetaSystemsScreen( @OptIn(ExperimentalFoundationApi::class) @Composable -fun MetaSystemsScreen( +private fun MetaSystemsScreen( + modifier: Modifier = Modifier, metaSystems: List, onSystemClicked: (MetaSystemInfo) -> Unit, - paddings: MergedPaddingValues, ) { if (metaSystems.isEmpty()) { LemuroidEmptyView() @@ -41,8 +42,11 @@ fun MetaSystemsScreen( } LazyVerticalGrid( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(16.dp), columns = GridCells.Adaptive(144.dp), - contentPadding = (paddings + PaddingValues(8.dp)).asPaddingValues(), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { items(metaSystems.size, key = { metaSystems[it].metaSystem }) { index -> val system = metaSystems[index] diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidScaffold.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidScaffold.kt deleted file mode 100644 index 92896bc11b..0000000000 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidScaffold.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.swordfish.lemuroid.app.mobile.shared.compose.ui - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.material3.Scaffold -import androidx.compose.runtime.Composable -import androidx.navigation.NavController -import com.swordfish.lemuroid.app.mobile.feature.main.MainRoute -import com.swordfish.lemuroid.app.mobile.feature.main.MainViewModel -import com.swordfish.lemuroid.app.utils.android.compose.MergedPaddingValues - -@Composable -fun LemuroidScaffold( - route: MainRoute, - navController: NavController, - mainUIState: MainViewModel.UiState, - outerPadding: PaddingValues, - content: @Composable (MergedPaddingValues) -> Unit, - onHelpPressed: () -> Unit, -) { - Scaffold( - topBar = { LemuroidTopAppBar(route, navController, mainUIState, onHelpPressed) }, - ) { innerPadding -> - - val paddings = - mutableListOf().apply { - add(innerPadding) - - if (route.showBottomNavigation) { - add(outerPadding) - } - } - - content(MergedPaddingValues(paddings)) - } -} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidSystemCard.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidSystemCard.kt index 04ac5de677..9ebf3deef7 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidSystemCard.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidSystemCard.kt @@ -2,19 +2,15 @@ package com.swordfish.lemuroid.app.mobile.shared.compose.ui import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.systems.MetaSystemInfo @Composable -@OptIn(ExperimentalMaterial3Api::class) fun LemuroidSystemCard( modifier: Modifier = Modifier, system: MetaSystemInfo, @@ -36,7 +32,7 @@ fun LemuroidSystemCard( } ElevatedCard( - modifier = modifier.padding(8.dp), + modifier = modifier, onClick = onClick, ) { Column( diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidTopAppBar.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidTopAppBar.kt deleted file mode 100644 index 035849fbc2..0000000000 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/mobile/shared/compose/ui/LemuroidTopAppBar.kt +++ /dev/null @@ -1,142 +0,0 @@ -package com.swordfish.lemuroid.app.mobile.shared.compose.ui - -import android.content.Context -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.outlined.CloudSync -import androidx.compose.material.icons.outlined.Info -import androidx.compose.material.icons.outlined.Settings -import androidx.compose.material3.BottomAppBarDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.surfaceColorAtElevation -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.navigation.NavController -import com.swordfish.lemuroid.R -import com.swordfish.lemuroid.app.mobile.feature.main.MainRoute -import com.swordfish.lemuroid.app.mobile.feature.main.MainViewModel -import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun LemuroidTopAppBar( - route: MainRoute, - navController: NavController, - mainUIState: MainViewModel.UiState, - onHelpPressed: () -> Unit, -) { - val context = LocalContext.current - LemuroidTopAppBarContainer(mainUIState.operationInProgress) { - val topBarColor = - MaterialTheme.colorScheme.surfaceColorAtElevation( - BottomAppBarDefaults.ContainerElevation, - ) - - TopAppBar( - title = { Text(text = stringResource(route.titleId)) }, - colors = - TopAppBarDefaults.topAppBarColors( - scrolledContainerColor = topBarColor, - containerColor = topBarColor, - ), - navigationIcon = { - AnimatedVisibility( - visible = route.parent != null, - enter = fadeIn(), - exit = fadeOut(), - ) { - IconButton(onClick = { navController.popBackStack() }) { - Icon( - Icons.AutoMirrored.Filled.ArrowBack, - stringResource(id = R.string.back), - ) - } - } - }, - actions = { - LemuroidTopBarActions( - route = route, - navController = navController, - context = context, - saveSyncEnabled = mainUIState.saveSyncEnabled, - onHelpPressed = onHelpPressed, - operationsInProgress = mainUIState.operationInProgress, - ) - }, - ) - } -} - -@Composable -fun LemuroidTopBarActions( - route: MainRoute, - navController: NavController, - context: Context, - saveSyncEnabled: Boolean, - operationsInProgress: Boolean, - onHelpPressed: () -> Unit, -) { - Row { - IconButton( - onClick = { onHelpPressed() }, - ) { - Icon( - Icons.Outlined.Info, - stringResource(R.string.mobile_settings_help), - ) - } - if (saveSyncEnabled) { - IconButton( - onClick = { SaveSyncWork.enqueueManualWork(context.applicationContext) }, - enabled = !operationsInProgress, - ) { - Icon( - Icons.Outlined.CloudSync, - stringResource(R.string.save_sync), - ) - } - } - if (route.showBottomNavigation) { - IconButton( - onClick = { navController.navigate(MainRoute.SETTINGS.route) }, - ) { - Icon( - Icons.Outlined.Settings, - stringResource(R.string.settings), - ) - } - } - } -} - -@Composable -fun LemuroidTopAppBarContainer( - displayProgress: Boolean, - content: @Composable () -> Unit, -) { - Column { - Surface(tonalElevation = BottomAppBarDefaults.ContainerElevation) { - content() - } - - AnimatedVisibility(displayProgress) { - LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) - } - } -} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt index d169a2e217..39d5f07d24 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/game/ExternalGameLauncherActivity.kt @@ -5,8 +5,6 @@ import android.os.Bundle import android.view.View import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LiveData -import androidx.lifecycle.asFlow import androidx.lifecycle.lifecycleScope import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.shared.ImmersiveActivity @@ -24,6 +22,7 @@ import com.swordfish.lemuroid.lib.library.db.RetrogradeDatabase import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.filter @@ -102,7 +101,6 @@ class ExternalGameLauncherActivity : ImmersiveActivity() { private suspend fun waitPendingOperations() { getLoadingLiveData() - .asFlow() .filter { !it } .first() } @@ -111,7 +109,7 @@ class ExternalGameLauncherActivity : ImmersiveActivity() { displayErrorDialog(R.string.game_loader_error_load_game, R.string.ok) { finish() } } - private fun getLoadingLiveData(): LiveData { + private fun getLoadingLiveData(): Flow { return PendingOperationsMonitor(applicationContext).anyOperationInProgress() } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/PendingOperationsMonitor.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/PendingOperationsMonitor.kt index 4527a59d07..7c73a10d68 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/PendingOperationsMonitor.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/shared/library/PendingOperationsMonitor.kt @@ -1,13 +1,14 @@ package com.swordfish.lemuroid.app.shared.library import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.map import androidx.work.WorkInfo import androidx.work.WorkManager import com.swordfish.lemuroid.app.shared.savesync.SaveSyncWork -import com.swordfish.lemuroid.app.utils.livedata.combineLatest -import com.swordfish.lemuroid.app.utils.livedata.throttle +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map class PendingOperationsMonitor(private val appContext: Context) { enum class Operation(val uniqueId: String, val isPeriodic: Boolean) { @@ -17,32 +18,35 @@ class PendingOperationsMonitor(private val appContext: Context) { SAVES_SYNC_ONE_SHOT(SaveSyncWork.UNIQUE_WORK_ID, false), } - fun anyOperationInProgress(): LiveData { + fun anyOperationInProgress(): Flow { return operationsInProgress(*Operation.values()) } - fun anySaveOperationInProgress(): LiveData { + fun anySaveOperationInProgress(): Flow { return operationsInProgress(Operation.SAVES_SYNC_ONE_SHOT, Operation.SAVES_SYNC_PERIODIC) } - fun anyLibraryOperationInProgress(): LiveData { + fun anyLibraryOperationInProgress(): Flow { return operationsInProgress(Operation.LIBRARY_INDEX, Operation.CORE_UPDATE) } - fun isDirectoryScanInProgress(): LiveData { + fun isDirectoryScanInProgress(): Flow { return operationsInProgress(Operation.LIBRARY_INDEX) } - private fun operationsInProgress(vararg operations: Operation): LiveData { - return operations - .map { operationInProgress(it) } - .reduce { first, second -> first.combineLatest(second) { b1, b2 -> b1 || b2 } } - .throttle(100) + @OptIn(FlowPreview::class) + private fun operationsInProgress(vararg operations: Operation): Flow { + val operationFlows = operations.map { operationInProgress(it) } + val result = + combine(operationFlows) { operationInProgress -> + operationInProgress.any { it } + } + return result.debounce(100) } - private fun operationInProgress(operation: Operation): LiveData { + private fun operationInProgress(operation: Operation): Flow { return WorkManager.getInstance(appContext) - .getWorkInfosForUniqueWorkLiveData(operation.uniqueId) + .getWorkInfosForUniqueWorkFlow(operation.uniqueId) .map { if (operation.isPeriodic) isPeriodicJobRunning(it) else isJobRunning(it) } } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt index b6abd40c78..a70405a50a 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/home/TVHomeViewModel.kt @@ -3,7 +3,6 @@ package com.swordfish.lemuroid.app.tv.home import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor import com.swordfish.lemuroid.app.shared.systems.MetaSystemInfo @@ -87,10 +86,10 @@ class TVHomeViewModel(retrogradeDb: RetrogradeDatabase, appContext: Context) : V } private fun directoryScanInProgress(appContext: Context) = - PendingOperationsMonitor(appContext).isDirectoryScanInProgress().asFlow() + PendingOperationsMonitor(appContext).isDirectoryScanInProgress() private fun indexingInProgress(appContext: Context) = - PendingOperationsMonitor(appContext).anyLibraryOperationInProgress().asFlow() + PendingOperationsMonitor(appContext).anyLibraryOperationInProgress() private fun availableSystems( retrogradeDb: RetrogradeDatabase, diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt index 360707cf6c..b28fd7a89b 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVActivity.kt @@ -9,6 +9,7 @@ import android.view.View import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.core.view.isVisible +import androidx.lifecycle.Lifecycle import androidx.lifecycle.ViewModelProvider import com.swordfish.lemuroid.R import com.swordfish.lemuroid.app.mobile.feature.shortcuts.ShortcutsGenerator @@ -24,6 +25,8 @@ import com.swordfish.lemuroid.app.tv.home.TVHomeFragment import com.swordfish.lemuroid.app.tv.search.TVSearchFragment import com.swordfish.lemuroid.app.tv.shared.BaseTVActivity import com.swordfish.lemuroid.app.tv.shared.TVHelper +import com.swordfish.lemuroid.common.coroutines.launchOnState +import com.swordfish.lemuroid.common.coroutines.safeCollect import com.swordfish.lemuroid.common.coroutines.safeLaunch import com.swordfish.lemuroid.lib.injection.PerActivity import com.swordfish.lemuroid.lib.injection.PerFragment @@ -52,8 +55,10 @@ class MainTVActivity : BaseTVActivity(), BusyActivity { val factory = MainTVViewModel.Factory(applicationContext) mainViewModel = ViewModelProvider(this, factory).get(MainTVViewModel::class.java) - mainViewModel?.inProgress?.observe(this) { - findViewById(R.id.tv_loading).isVisible = it + launchOnState(Lifecycle.State.CREATED) { + mainViewModel?.inProgress?.safeCollect { + findViewById(R.id.tv_loading).isVisible = it + } } ensureLegacyStoragePermissionsIfNeeded() diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt index 495b4b0211..012d459b08 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/main/MainTVViewModel.kt @@ -3,7 +3,10 @@ package com.swordfish.lemuroid.app.tv.main import android.content.Context import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewModelScope import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn class MainTVViewModel(appContext: Context) : ViewModel() { class Factory(private val appContext: Context) : ViewModelProvider.Factory { @@ -12,5 +15,12 @@ class MainTVViewModel(appContext: Context) : ViewModel() { } } - val inProgress = PendingOperationsMonitor(appContext).anyOperationInProgress() + val inProgress = + PendingOperationsMonitor(appContext) + .anyOperationInProgress() + .stateIn( + scope = viewModelScope, + started = SharingStarted.Lazily, + initialValue = false, + ) } diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt index 32d6b80c58..441ac06b1a 100644 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt +++ b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/tv/settings/TVSettingsFragment.kt @@ -15,6 +15,7 @@ import com.swordfish.lemuroid.app.shared.library.PendingOperationsMonitor import com.swordfish.lemuroid.app.shared.settings.SaveSyncPreferences import com.swordfish.lemuroid.app.shared.settings.SettingsInteractor import com.swordfish.lemuroid.common.coroutines.launchOnState +import com.swordfish.lemuroid.common.coroutines.safeCollect import com.swordfish.lemuroid.common.kotlin.NTuple2 import com.swordfish.lemuroid.lib.preferences.SharedPreferencesHelper import com.swordfish.lemuroid.lib.savesync.SaveSyncManager @@ -78,6 +79,16 @@ class TVSettingsFragment : LeanbackPreferenceFragmentCompat() { .distinctUntilChanged() .collect { refreshGamePadBindingsScreen(it) } } + + launchOnState(Lifecycle.State.RESUMED) { + getSaveSyncScreen()?.let { screen -> + PendingOperationsMonitor(requireContext()) + .anySaveOperationInProgress() + .safeCollect { syncInProgress -> + saveSyncPreferences.updatePreferences(screen, syncInProgress) + } + } + } } override fun onCreatePreferences( @@ -110,16 +121,7 @@ class TVSettingsFragment : LeanbackPreferenceFragmentCompat() { override fun onResume() { super.onResume() - refreshSaveSyncScreen() - - getSaveSyncScreen()?.let { screen -> - PendingOperationsMonitor(requireContext()) - .anySaveOperationInProgress() - .observe(this) { syncInProgress -> - saveSyncPreferences.updatePreferences(screen, syncInProgress) - } - } } private fun getGamePadPreferenceScreen(): PreferenceScreen? { diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/CombinedLiveData.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/CombinedLiveData.kt deleted file mode 100644 index 237c18e1ab..0000000000 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/CombinedLiveData.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.swordfish.lemuroid.app.utils.livedata - -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData -import androidx.lifecycle.Observer - -class CombinedLiveData( - source1: LiveData, - source2: LiveData, - private val combine: (data1: T, data2: K) -> S, -) : MediatorLiveData() { - private var data1: T? = null - private var data2: K? = null - - init { - super.addSource(source1) { - data1 = it - emitIfNecessary() - } - super.addSource(source2) { - data2 = it - emitIfNecessary() - } - } - - private fun emitIfNecessary() { - val currentData1 = data1 - val currentData2 = data2 - if (currentData1 != null && currentData2 != null) { - value = combine(currentData1, currentData2) - } - } - - override fun addSource( - source: LiveData, - onChanged: Observer, - ) { - throw UnsupportedOperationException() - } - - override fun removeSource(toRemote: LiveData) { - throw UnsupportedOperationException() - } -} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/LiveDataUtils.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/LiveDataUtils.kt deleted file mode 100644 index 1b990d6c4f..0000000000 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/LiveDataUtils.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.swordfish.lemuroid.app.utils.livedata - -import androidx.lifecycle.LiveData - -fun LiveData.combineLatest( - other: LiveData, - combine: (data1: T, data2: K) -> S, -): LiveData { - return CombinedLiveData(this, other, combine) -} - -fun LiveData.throttle(delayMs: Long): LiveData { - return ThrottledLiveData(this, delayMs) -} diff --git a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/ThrottledLiveData.kt b/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/ThrottledLiveData.kt deleted file mode 100644 index 5e6fd58d6c..0000000000 --- a/lemuroid-app/src/main/java/com/swordfish/lemuroid/app/utils/livedata/ThrottledLiveData.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.swordfish.lemuroid.app.utils.livedata - -import android.os.Handler -import android.os.Looper -import androidx.lifecycle.LiveData -import androidx.lifecycle.MediatorLiveData - -/** - * LiveData throttling value emissions so they don't happen more often than [delayMs]. - */ -class ThrottledLiveData(source: LiveData, delayMs: Long) : MediatorLiveData() { - private val handler = Handler(Looper.getMainLooper()) - var delayMs = delayMs - private set - - private var isValueDelayed = false - private var delayedValue: T? = null - private var delayRunnable: Runnable? = null - set(value) { - field?.let { handler.removeCallbacks(it) } - value?.let { handler.postDelayed(it, delayMs) } - field = value - } - private val objDelayRunnable = Runnable { if (consumeDelayedValue()) startDelay() } - - init { - addSource(source) { newValue -> - if (delayRunnable == null) { - value = newValue - startDelay() - } else { - isValueDelayed = true - delayedValue = newValue - } - } - } - - override fun onInactive() { - super.onInactive() - consumeDelayedValue() - } - - // start counting the delay or clear it if conditions are not met - private fun startDelay() { - delayRunnable = if (delayMs > 0 && hasActiveObservers()) objDelayRunnable else null - } - - private fun consumeDelayedValue(): Boolean { - delayRunnable = null - return if (isValueDelayed) { - value = delayedValue - delayedValue = null - isValueDelayed = false - true - } else { - false - } - } -}