diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c7416fc..aeddf2d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -42,8 +42,8 @@ android { minSdk = 29 targetSdk = 34 - versionCode = 3 - versionName = "1.2.0" + versionCode = 4 + versionName = "1.3.0" buildConfigField("boolean", "GOLD", "false") val iconValue = "@mipmap/ic_launcher" diff --git a/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt b/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt index a7c9275..ab8b807 100644 --- a/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt +++ b/app/src/main/java/com/OxGames/Pluvia/PrefManager.kt @@ -14,6 +14,7 @@ import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import com.OxGames.Pluvia.enums.AppTheme import com.OxGames.Pluvia.service.SteamService +import com.OxGames.Pluvia.ui.enums.AppFilter import com.OxGames.Pluvia.ui.enums.HomeDestination import com.OxGames.Pluvia.ui.enums.Orientation import com.materialkolor.PaletteStyle @@ -356,6 +357,16 @@ object PrefManager { /** * Get or Set the last known Persona State. See [EPersonaState] */ + private val LIBRARY_FILTER = intPreferencesKey("library_filter") + var libraryFilter: EnumSet + get() { + val value = getPref(LIBRARY_FILTER, AppFilter.toFlags(EnumSet.of(AppFilter.GAME))) + return AppFilter.fromFlags(value) + } + set(value) { + setPref(LIBRARY_FILTER, AppFilter.toFlags(value)) + } + private val PERSONA_STATE = intPreferencesKey("persona_state") var personaState: EPersonaState get() { diff --git a/app/src/main/java/com/OxGames/Pluvia/db/dao/SteamAppDao.kt b/app/src/main/java/com/OxGames/Pluvia/db/dao/SteamAppDao.kt index b95df3e..53c08fa 100644 --- a/app/src/main/java/com/OxGames/Pluvia/db/dao/SteamAppDao.kt +++ b/app/src/main/java/com/OxGames/Pluvia/db/dao/SteamAppDao.kt @@ -31,8 +31,11 @@ interface SteamAppDao { invalidPkgId: Int = INVALID_PKG_ID, ): Flow> - @Query("SELECT * FROM steam_app WHERE received_pics = 0") - fun getAllAppsWithoutPICS(): Flow> + @Query("SELECT * FROM steam_app WHERE received_pics = 0 AND package_id != :invalidPkgId AND owner_account_id = :ownerId") + fun getAllOwnedAppsWithoutPICS( + ownerId: Int, + invalidPkgId: Int = INVALID_PKG_ID, + ): Flow> @Query("SELECT * FROM steam_app WHERE id = :appId") fun findApp(appId: Int): Flow diff --git a/app/src/main/java/com/OxGames/Pluvia/enums/AppType.kt b/app/src/main/java/com/OxGames/Pluvia/enums/AppType.kt index c7f0e11..0dc5a0d 100644 --- a/app/src/main/java/com/OxGames/Pluvia/enums/AppType.kt +++ b/app/src/main/java/com/OxGames/Pluvia/enums/AppType.kt @@ -67,7 +67,7 @@ enum class AppType(val code: Int) { } fun toFlags(value: EnumSet): Int { - return value.map { it.code }.reduce { first, second -> first or second } + return value.map { it.code }.reduceOrNull { first, second -> first or second } ?: invalid.code } fun fromCode(code: Int): AppType { diff --git a/app/src/main/java/com/OxGames/Pluvia/enums/OS.kt b/app/src/main/java/com/OxGames/Pluvia/enums/OS.kt index f9bd4d5..c1cf58d 100644 --- a/app/src/main/java/com/OxGames/Pluvia/enums/OS.kt +++ b/app/src/main/java/com/OxGames/Pluvia/enums/OS.kt @@ -38,7 +38,7 @@ enum class OS(val code: Int) { } fun code(value: EnumSet): Int { - return value.map { it.code }.reduce { first, second -> first or second } + return value.map { it.code }.reduceOrNull { first, second -> first or second } ?: none.code } } } diff --git a/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt b/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt index 586cf1b..ba2b5b7 100644 --- a/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt +++ b/app/src/main/java/com/OxGames/Pluvia/service/SteamService.kt @@ -1399,12 +1399,22 @@ class SteamService : Service(), IChallengeUrlChanged { // Timber.d("Adding ${appToAdd?.name} with appId of ${appToAdd?.id} and pkgId of ${appToAdd?.packageId}") val appIds = picsChangesCallback.appChanges.values - .filter { it.changeNumber != appDao.findApp(it.id).first()?.lastChangeNumber } + .filter { changeData -> + // only queue PICS requests for apps existing in the db that have changed + appDao.findApp(changeData.id).first()?.let { + changeData.changeNumber != it.lastChangeNumber + } == true + } .map { AppRequest(it.id) }.toTypedArray() queueAppPICSRequests(*appIds) val pkgsWithChanges = picsChangesCallback.packageChanges.values - .filter { it.changeNumber != licenseDao.findLicense(it.id).first()?.lastChangeNumber } + .filter { changeData -> + // only queue PICS requests for pkgs existing in the db that have changed + licenseDao.findLicense(changeData.id).first()?.let { + changeData.changeNumber != it.lastChangeNumber + } == true + } val pkgsForAccessTokens = pkgsWithChanges.filter { it.isNeedsToken }.map { it.id } val accessTokens = _steamApps?.picsGetAccessTokens( emptyList(), @@ -1624,7 +1634,10 @@ class SteamService : Service(), IChallengeUrlChanged { if (callback.result == EResult.OK) { dbScope.launch { // check first if any apps already exist in the db that need PICS - val apps = appDao.getAllAppsWithoutPICS().firstOrNull()?.map { AppRequest(it.id) }?.toTypedArray() + val apps = appDao.getAllOwnedAppsWithoutPICS(userSteamId!!.accountID.toInt()) + .firstOrNull() + ?.map { AppRequest(it.id) } + ?.toTypedArray() Timber.d("${apps?.size ?: 0} app(s) need PICS") if (apps?.isNotEmpty() == true) { queueAppPICSRequests(*apps) @@ -1698,13 +1711,17 @@ class SteamService : Service(), IChallengeUrlChanged { licenseDao.updateApps(pkg.id, appIds) licenseDao.updateDepots(pkg.id, depotIds) - val steamAppsToAdd = appIds.map { appId -> - appDao.findApp(appId).first()?.copy(packageId = pkg.id) - ?: SteamApp(id = appId, packageId = pkg.id) - }.toTypedArray() - appDao.insert(*steamAppsToAdd) + val license = licenseDao.findLicense(pkg.id).first() + // only add the apps belonging to the license if the user owns it + if (license?.ownerAccountId == userSteamId?.accountID?.toInt()) { + val steamAppsToAdd = appIds.map { appId -> + appDao.findApp(appId).first()?.copy(packageId = pkg.id) + ?: SteamApp(id = appId, packageId = pkg.id) + }.toTypedArray() - queueAppPICSRequests(*appIds.map { AppRequest(it) }.toTypedArray()) + appDao.insert(*steamAppsToAdd) + queueAppPICSRequests(*appIds.map { AppRequest(it) }.toTypedArray()) + } } } diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/component/FlowFilterChip.kt b/app/src/main/java/com/OxGames/Pluvia/ui/component/FlowFilterChip.kt new file mode 100644 index 0000000..9c8c8e7 --- /dev/null +++ b/app/src/main/java/com/OxGames/Pluvia/ui/component/FlowFilterChip.kt @@ -0,0 +1,24 @@ +package com.OxGames.Pluvia.ui.component + +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.FilterChip +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun FlowFilterChip( + selected: Boolean, + onClick: () -> Unit, + label: @Composable (() -> Unit), + modifier: Modifier = Modifier, + leadingIcon: @Composable (() -> Unit), +) { + FilterChip( + modifier = Modifier.padding(end = 8.dp).then(modifier), + onClick = onClick, + label = label, + selected = selected, + leadingIcon = leadingIcon, + ) +} diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/data/LibraryState.kt b/app/src/main/java/com/OxGames/Pluvia/ui/data/LibraryState.kt index 6b9ff3d..55f7a92 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/data/LibraryState.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/data/LibraryState.kt @@ -1,12 +1,14 @@ package com.OxGames.Pluvia.ui.data +import com.OxGames.Pluvia.PrefManager import com.OxGames.Pluvia.data.LibraryItem -import com.OxGames.Pluvia.ui.enums.FabFilter +import com.OxGames.Pluvia.ui.enums.AppFilter import java.util.EnumSet data class LibraryState( - val appInfoSortType: EnumSet = EnumSet.of(FabFilter.ALPHABETIC, FabFilter.GAME), + val appInfoSortType: EnumSet = PrefManager.libraryFilter, val appInfoList: List = emptyList(), + val modalBottomSheet: Boolean = false, val isSearching: Boolean = false, val searchQuery: String = "", diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/enums/AppFilter.kt b/app/src/main/java/com/OxGames/Pluvia/ui/enums/AppFilter.kt new file mode 100644 index 0000000..56d78a9 --- /dev/null +++ b/app/src/main/java/com/OxGames/Pluvia/ui/enums/AppFilter.kt @@ -0,0 +1,82 @@ +package com.OxGames.Pluvia.ui.enums + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AvTimer +import androidx.compose.material.icons.filled.Build +import androidx.compose.material.icons.filled.Computer +import androidx.compose.material.icons.filled.InstallMobile +import androidx.compose.material.icons.filled.VideogameAsset +import androidx.compose.ui.graphics.vector.ImageVector +import com.OxGames.Pluvia.enums.AppType +import java.util.EnumSet + +enum class AppFilter( + val code: Int, + val displayText: String, + val icon: ImageVector, +) { + INSTALLED( + code = 0x01, + displayText = "Installed", + icon = Icons.Default.InstallMobile, + ), + GAME( + code = 0x02, + displayText = "Game", + icon = Icons.Default.VideogameAsset, + ), + APPLICATION( + code = 0x04, + displayText = "Application", + icon = Icons.Default.Computer, + ), + TOOL( + code = 0x08, + displayText = "Tool", + icon = Icons.Default.Build, + ), + DEMO( + code = 0x10, + displayText = "Demo", + icon = Icons.Default.AvTimer, + ), + // ALPHABETIC( + // code = 0x20, + // displayText = "Alphabetic", + // icon = Icons.Default.SortByAlpha, + // ), + ; + + companion object { + fun getAppType(appFilter: EnumSet): EnumSet { + val output: EnumSet = EnumSet.noneOf(AppType::class.java) + if (appFilter.contains(GAME)) { + output.add(AppType.game) + } + if (appFilter.contains(APPLICATION)) { + output.add(AppType.application) + } + if (appFilter.contains(TOOL)) { + output.add(AppType.tool) + } + if (appFilter.contains(DEMO)) { + output.add(AppType.demo) + } + return output + } + + fun fromFlags(flags: Int): EnumSet { + val result = EnumSet.noneOf(AppFilter::class.java) + AppFilter.entries.forEach { appFilter -> + if (flags and appFilter.code == appFilter.code) { + result.add(appFilter) + } + } + return result + } + + fun toFlags(value: EnumSet): Int { + return value.map { it.code }.reduceOrNull { first, second -> first or second } ?: 0 + } + } +} diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/enums/FabFilter.kt b/app/src/main/java/com/OxGames/Pluvia/ui/enums/FabFilter.kt deleted file mode 100644 index b572667..0000000 --- a/app/src/main/java/com/OxGames/Pluvia/ui/enums/FabFilter.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.OxGames.Pluvia.ui.enums - -import com.OxGames.Pluvia.enums.AppType -import java.util.EnumSet - -enum class FabFilter(val code: Int) { - INSTALLED(0x01), - ALPHABETIC(0x02), - GAME(0x04), - APPLICATION(0x08), - TOOL(0x10), - DEMO(0x20), - ; - - companion object { - fun getAppType(fabFilter: EnumSet): EnumSet { - val output: EnumSet = EnumSet.noneOf(AppType::class.java) - if (fabFilter.contains(GAME)) { - output.add(AppType.game) - } - if (fabFilter.contains(APPLICATION)) { - output.add(AppType.application) - } - if (fabFilter.contains(TOOL)) { - output.add(AppType.tool) - } - if (fabFilter.contains(DEMO)) { - output.add(AppType.demo) - } - return output - } - } -} diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/model/LibraryViewModel.kt b/app/src/main/java/com/OxGames/Pluvia/ui/model/LibraryViewModel.kt index ca7f082..06384c9 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/model/LibraryViewModel.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/model/LibraryViewModel.kt @@ -6,13 +6,15 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.OxGames.Pluvia.PrefManager import com.OxGames.Pluvia.data.LibraryItem import com.OxGames.Pluvia.data.SteamApp import com.OxGames.Pluvia.db.dao.SteamAppDao import com.OxGames.Pluvia.service.SteamService import com.OxGames.Pluvia.ui.data.LibraryState -import com.OxGames.Pluvia.ui.enums.FabFilter +import com.OxGames.Pluvia.ui.enums.AppFilter import dagger.hilt.android.lifecycle.HiltViewModel +import java.util.EnumSet import javax.inject.Inject import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -53,6 +55,10 @@ class LibraryViewModel @Inject constructor( } } + fun onModalBottomSheet(value: Boolean) { + _state.update { it.copy(modalBottomSheet = value) } + } + fun onIsSearching(value: Boolean) { _state.update { it.copy(isSearching = value) } if (!value) { @@ -66,14 +72,18 @@ class LibraryViewModel @Inject constructor( } // TODO: include other sort types - fun onFabFilter(value: FabFilter) { + fun onFilterChanged(value: AppFilter) { _state.update { currentState -> - val updatedFilter = currentState.appInfoSortType + val updatedFilter = EnumSet.copyOf(currentState.appInfoSortType) + if (updatedFilter.contains(value)) { updatedFilter.remove(value) } else { updatedFilter.add(value) } + + PrefManager.libraryFilter = updatedFilter + currentState.copy(appInfoSortType = updatedFilter) } @@ -84,7 +94,7 @@ class LibraryViewModel @Inject constructor( Timber.d("onFilterApps") viewModelScope.launch { val currentState = _state.value - val currentFilter = FabFilter.getAppType(currentState.appInfoSortType) + val currentFilter = AppFilter.getAppType(currentState.appInfoSortType) val filteredList = appList .asSequence() @@ -99,7 +109,7 @@ class LibraryViewModel @Inject constructor( } } .filter { item -> - if (currentState.appInfoSortType.contains(FabFilter.INSTALLED)) { + if (currentState.appInfoSortType.contains(AppFilter.INSTALLED)) { SteamService.isAppInstalled(item.id) } else { true diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/LibraryScreen.kt b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/LibraryScreen.kt index ff67744..ade885a 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/LibraryScreen.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/LibraryScreen.kt @@ -5,31 +5,35 @@ import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.SheetState 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.navigation.BackNavigationBehavior import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.OxGames.Pluvia.data.LibraryItem import com.OxGames.Pluvia.service.SteamService -import com.OxGames.Pluvia.ui.component.fabmenu.state.FloatingActionMenuState -import com.OxGames.Pluvia.ui.component.fabmenu.state.FloatingActionMenuValue -import com.OxGames.Pluvia.ui.component.fabmenu.state.rememberFloatingActionMenuState import com.OxGames.Pluvia.ui.data.LibraryState -import com.OxGames.Pluvia.ui.enums.FabFilter +import com.OxGames.Pluvia.ui.enums.AppFilter import com.OxGames.Pluvia.ui.internal.fakeAppInfo import com.OxGames.Pluvia.ui.model.LibraryViewModel import com.OxGames.Pluvia.ui.screen.library.components.LibraryDetailPane import com.OxGames.Pluvia.ui.screen.library.components.LibraryListPane import com.OxGames.Pluvia.ui.theme.PluviaTheme +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeLibraryScreen( viewModel: LibraryViewModel = hiltViewModel(), @@ -38,13 +42,14 @@ fun HomeLibraryScreen( onLogout: () -> Unit, ) { val state by viewModel.state.collectAsStateWithLifecycle() - val fabState = rememberFloatingActionMenuState() + val sheetState = rememberModalBottomSheetState() LibraryScreenContent( state = state, listState = viewModel.listState, - fabState = fabState, - onFabFilter = viewModel::onFabFilter, + sheetState = sheetState, + onFilterChanged = viewModel::onFilterChanged, + onModalBottomSheet = viewModel::onModalBottomSheet, onIsSearching = viewModel::onIsSearching, onSearchQuery = viewModel::onSearchQuery, onClickPlay = onClickPlay, @@ -53,15 +58,16 @@ fun HomeLibraryScreen( ) } -@OptIn(ExperimentalMaterial3AdaptiveApi::class) +@OptIn(ExperimentalMaterial3AdaptiveApi::class, ExperimentalMaterial3Api::class) @Composable private fun LibraryScreenContent( state: LibraryState, listState: LazyListState, - fabState: FloatingActionMenuState, + sheetState: SheetState, + onFilterChanged: (AppFilter) -> Unit, + onModalBottomSheet: (Boolean) -> Unit, onIsSearching: (Boolean) -> Unit, onSearchQuery: (String) -> Unit, - onFabFilter: (FabFilter) -> Unit, onClickPlay: (Int, Boolean) -> Unit, onSettings: () -> Unit, onLogout: () -> Unit, @@ -82,12 +88,13 @@ private fun LibraryScreenContent( LibraryListPane( state = state, listState = listState, - fabState = fabState, + sheetState = sheetState, + onFilterChanged = onFilterChanged, + onModalBottomSheet = onModalBottomSheet, onIsSearching = onIsSearching, onSearchQuery = onSearchQuery, onSettings = onSettings, onLogout = onLogout, - onFabFilter = onFabFilter, onNavigate = { item -> navigator.navigateTo( pane = ListDetailPaneScaffoldRole.Detail, @@ -117,6 +124,7 @@ private fun LibraryScreenContent( * PREVIEW * ***********/ +@OptIn(ExperimentalMaterial3Api::class) @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) @Preview( uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL, @@ -128,11 +136,11 @@ private fun LibraryScreenContent( ) @Composable private fun Preview_LibraryScreenContent() { - PluviaTheme { - LibraryScreenContent( - listState = rememberLazyListState(), - state = LibraryState( - appInfoList = List(14) { idx -> + val sheetState = rememberModalBottomSheetState() + var state by remember { + mutableStateOf( + LibraryState( + appInfoList = List(15) { idx -> val item = fakeAppInfo(idx) LibraryItem( index = idx, @@ -142,13 +150,24 @@ private fun Preview_LibraryScreenContent() { ) }, ), - fabState = rememberFloatingActionMenuState(FloatingActionMenuValue.Open), - onIsSearching = { }, - onSearchQuery = { }, - onFabFilter = { }, + ) + } + PluviaTheme { + LibraryScreenContent( + listState = rememberLazyListState(), + state = state, + sheetState = sheetState, + onIsSearching = {}, + onSearchQuery = {}, + onFilterChanged = { }, + onModalBottomSheet = { + val currentState = state.modalBottomSheet + println("State: $currentState") + state = state.copy(modalBottomSheet = !currentState) + }, onClickPlay = { _, _ -> }, - onSettings = { }, - onLogout = { }, + onSettings = {}, + onLogout = {}, ) } } diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryBottomSheet.kt b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryBottomSheet.kt new file mode 100644 index 0000000..1d0379c --- /dev/null +++ b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryBottomSheet.kt @@ -0,0 +1,96 @@ +package com.OxGames.Pluvia.ui.screen.library.components + +import android.content.res.Configuration +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.OxGames.Pluvia.ui.component.FlowFilterChip +import com.OxGames.Pluvia.ui.enums.AppFilter +import com.OxGames.Pluvia.ui.theme.PluviaTheme +import java.util.EnumSet + +@Composable +@OptIn(ExperimentalLayoutApi::class) +fun LibraryBottomSheet( + selectedFilters: EnumSet, + onFilterChanged: (AppFilter) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 32.dp), + ) { + Text(text = "Filters", style = MaterialTheme.typography.titleLarge) + + Spacer(modifier = Modifier.height(18.dp)) + + FlowRow { + AppFilter.entries.forEach { appFilter -> + FlowFilterChip( + onClick = { onFilterChanged(appFilter) }, + label = { Text(text = appFilter.displayText) }, + selected = selectedFilters.contains(appFilter), + leadingIcon = { Icon(imageVector = appFilter.icon, contentDescription = null) }, + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) // A little extra padding. + } +} + +/*********** + * PREVIEW * + ***********/ + +@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +@Preview +@Composable +private fun Preview_LibraryBottomSheet() { + PluviaTheme { + Surface { + LibraryBottomSheet( + selectedFilters = EnumSet.of(AppFilter.GAME, AppFilter.DEMO), + onFilterChanged = { }, + ) + } + } +} + +// Note: Previews seem to be broken for this, run it manually + +// @OptIn(ExperimentalMaterial3Api::class) +// @Preview(uiMode = Configuration.UI_MODE_NIGHT_YES or Configuration.UI_MODE_TYPE_NORMAL) +// @Preview +// @Composable +// private fun Preview_LibraryBottomSheet_AsSheet() { +// PluviaTheme { +// Scaffold { paddingValues -> +// Box( +// modifier = Modifier +// .fillMaxSize() +// .padding(paddingValues), +// ) { +// Text(text = "Hello World") +// +// +// ModalBottomSheet( +// onDismissRequest = { }, +// content = { LibraryBottomSheet() }, +// ) +// } +// } +// } +// } diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryFab.kt b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryFab.kt index 0394673..e9dd435 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryFab.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryFab.kt @@ -14,13 +14,13 @@ import com.OxGames.Pluvia.ui.component.fabmenu.FloatingActionMenu import com.OxGames.Pluvia.ui.component.fabmenu.FloatingActionMenuItem import com.OxGames.Pluvia.ui.component.fabmenu.state.FloatingActionMenuState import com.OxGames.Pluvia.ui.data.LibraryState -import com.OxGames.Pluvia.ui.enums.FabFilter +import com.OxGames.Pluvia.ui.enums.AppFilter @Composable internal fun LibraryFab( fabState: FloatingActionMenuState, state: LibraryState, - onFabFilter: (FabFilter) -> Unit, + onFabFilter: (AppFilter) -> Unit, ) { FloatingActionMenu( state = fabState, @@ -29,45 +29,45 @@ internal fun LibraryFab( ) { FloatingActionMenuItem( labelText = "Installed", - isSelected = state.appInfoSortType.contains(FabFilter.INSTALLED), + isSelected = state.appInfoSortType.contains(AppFilter.INSTALLED), onClick = { - onFabFilter(FabFilter.INSTALLED) + onFabFilter(AppFilter.INSTALLED) fabState.close() }, content = { Icon(Icons.Filled.InstallMobile, "Installed") }, ) FloatingActionMenuItem( labelText = "Game", - isSelected = state.appInfoSortType.contains(FabFilter.GAME), + isSelected = state.appInfoSortType.contains(AppFilter.GAME), onClick = { - onFabFilter(FabFilter.GAME) + onFabFilter(AppFilter.GAME) fabState.close() }, content = { Icon(Icons.Filled.VideogameAsset, "Game") }, ) FloatingActionMenuItem( labelText = "Application", - isSelected = state.appInfoSortType.contains(FabFilter.APPLICATION), + isSelected = state.appInfoSortType.contains(AppFilter.APPLICATION), onClick = { - onFabFilter(FabFilter.APPLICATION) + onFabFilter(AppFilter.APPLICATION) fabState.close() }, content = { Icon(Icons.Filled.Computer, "Application") }, ) FloatingActionMenuItem( labelText = "Tool", - isSelected = state.appInfoSortType.contains(FabFilter.TOOL), + isSelected = state.appInfoSortType.contains(AppFilter.TOOL), onClick = { - onFabFilter(FabFilter.TOOL) + onFabFilter(AppFilter.TOOL) fabState.close() }, content = { Icon(Icons.Filled.Build, "Tool") }, ) FloatingActionMenuItem( labelText = "Demo", - isSelected = state.appInfoSortType.contains(FabFilter.DEMO), + isSelected = state.appInfoSortType.contains(AppFilter.DEMO), onClick = { - onFabFilter(FabFilter.DEMO) + onFabFilter(AppFilter.DEMO) fabState.close() }, content = { Icon(Icons.Filled.AvTimer, "Demo") }, diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryList.kt b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryList.kt index c79a9e9..d1bf41f 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryList.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryList.kt @@ -20,7 +20,6 @@ import com.OxGames.Pluvia.data.LibraryItem @Composable internal fun LibraryList( - paddingValues: PaddingValues, contentPaddingValues: PaddingValues, listState: LazyListState, list: List, @@ -28,9 +27,7 @@ internal fun LibraryList( ) { if (list.isEmpty()) { Box( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { Surface( @@ -47,9 +44,7 @@ internal fun LibraryList( } } else { LazyColumn( - modifier = Modifier - .padding(paddingValues) - .fillMaxSize(), + modifier = Modifier.fillMaxSize(), state = listState, contentPadding = contentPaddingValues, ) { diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryListPane.kt b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryListPane.kt index efba48b..8ee2c64 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryListPane.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibraryListPane.kt @@ -1,43 +1,65 @@ package com.OxGames.Pluvia.ui.screen.library.components +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold +import androidx.compose.material3.SheetState import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.OxGames.Pluvia.data.LibraryItem -import com.OxGames.Pluvia.ui.component.fabmenu.state.FloatingActionMenuState -import com.OxGames.Pluvia.ui.component.fabmenu.state.FloatingActionMenuValue -import com.OxGames.Pluvia.ui.component.fabmenu.state.rememberFloatingActionMenuState import com.OxGames.Pluvia.ui.data.LibraryState -import com.OxGames.Pluvia.ui.enums.FabFilter +import com.OxGames.Pluvia.ui.enums.AppFilter import com.OxGames.Pluvia.ui.internal.fakeAppInfo import com.OxGames.Pluvia.ui.theme.PluviaTheme +@OptIn(ExperimentalMaterial3Api::class) @Composable internal fun LibraryListPane( state: LibraryState, - fabState: FloatingActionMenuState, listState: LazyListState, - onFabFilter: (FabFilter) -> Unit, + sheetState: SheetState, + onFilterChanged: (AppFilter) -> Unit, + onModalBottomSheet: (Boolean) -> Unit, onIsSearching: (Boolean) -> Unit, onLogout: () -> Unit, onNavigate: (Int) -> Unit, onSearchQuery: (String) -> Unit, onSettings: () -> Unit, ) { + val expandedFab by remember { derivedStateOf { listState.firstVisibleItemIndex == 0 } } val snackBarHost = remember { SnackbarHostState() } Scaffold( @@ -55,29 +77,55 @@ internal fun LibraryListPane( ) }, floatingActionButton = { - LibraryFab( - fabState = fabState, - state = state, - onFabFilter = onFabFilter, - ) + AnimatedVisibility( + visible = !state.isSearching, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + ) { + ExtendedFloatingActionButton( + text = { Text(text = "Filters") }, + expanded = expandedFab, + icon = { Icon(imageVector = Icons.Default.FilterList, contentDescription = null) }, + onClick = { onModalBottomSheet(true) }, + ) + } }, ) { paddingValues -> val statusBarPadding = WindowInsets.statusBars.asPaddingValues().calculateTopPadding() - LibraryList( - list = state.appInfoList, - listState = listState, - paddingValues = PaddingValues( - start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), - end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), - top = statusBarPadding, - bottom = paddingValues.calculateBottomPadding(), - ), - contentPaddingValues = PaddingValues( - top = paddingValues.calculateTopPadding().minus(statusBarPadding), - bottom = 72.dp, - ), - onItemClick = onNavigate, - ) + Box( + modifier = Modifier + .fillMaxSize() + .padding( + PaddingValues( + start = paddingValues.calculateStartPadding(LayoutDirection.Ltr), + end = paddingValues.calculateEndPadding(LayoutDirection.Ltr), + top = statusBarPadding, + bottom = paddingValues.calculateBottomPadding(), + ), + ), + ) { + LibraryList( + list = state.appInfoList, + listState = listState, + contentPaddingValues = PaddingValues( + top = paddingValues.calculateTopPadding().minus(statusBarPadding), + bottom = 72.dp, + ), + onItemClick = onNavigate, + ) + if (state.modalBottomSheet) { + ModalBottomSheet( + onDismissRequest = { onModalBottomSheet(false) }, + sheetState = sheetState, + content = { + LibraryBottomSheet( + selectedFilters = state.appInfoSortType, + onFilterChanged = onFilterChanged, + ) + }, + ) + } + } } } @@ -85,29 +133,41 @@ internal fun LibraryListPane( * PREVIEW * ***********/ +@OptIn(ExperimentalMaterial3Api::class) @Preview(uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES or android.content.res.Configuration.UI_MODE_TYPE_NORMAL) @Preview @Composable private fun Preview_LibraryListPane() { + val sheetState = rememberModalBottomSheetState() + var state by remember { + mutableStateOf( + LibraryState( + appInfoList = List(15) { idx -> + val item = fakeAppInfo(idx) + LibraryItem( + index = idx, + appId = item.id, + name = item.name, + iconHash = item.iconHash, + ) + }, + ), + ) + } PluviaTheme { Surface { LibraryListPane( listState = rememberLazyListState(), - state = LibraryState( - appInfoList = List(14) { idx -> - val item = fakeAppInfo(idx) - LibraryItem( - index = idx, - appId = item.id, - name = item.name, - iconHash = item.iconHash, - ) - }, - ), - fabState = rememberFloatingActionMenuState(FloatingActionMenuValue.Open), + state = state, + sheetState = sheetState, + onFilterChanged = { }, + onModalBottomSheet = { + val currentState = state.modalBottomSheet + println("State: $currentState") + state = state.copy(modalBottomSheet = !currentState) + }, onIsSearching = { }, onSearchQuery = { }, - onFabFilter = { }, onSettings = { }, onLogout = { }, onNavigate = { }, diff --git a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibrarySearchBar.kt b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibrarySearchBar.kt index 7f72b84..06b2ff8 100644 --- a/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibrarySearchBar.kt +++ b/app/src/main/java/com/OxGames/Pluvia/ui/screen/library/components/LibrarySearchBar.kt @@ -107,7 +107,6 @@ internal fun LibrarySearchBar( content = { if (state.isSearching) { LibraryList( - paddingValues = PaddingValues(), contentPaddingValues = PaddingValues(bottom = 72.dp), listState = listState, list = state.appInfoList,