diff --git a/.github/workflows/pluvia-pr-check.yml b/.github/workflows/pluvia-pr-check.yml index aac1e8f9f2..127f29765e 100644 --- a/.github/workflows/pluvia-pr-check.yml +++ b/.github/workflows/pluvia-pr-check.yml @@ -41,4 +41,4 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v4 - name: Run unit tests - run: ./gradlew :app:testDebugUnitTest + run: ./gradlew :app:testOssDebugUnitTest diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9f3715b023..df6ae774a0 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -107,6 +107,18 @@ android { ) } + flavorDimensions += "distribution" + productFlavors { + create("oss") { + dimension = "distribution" + buildConfigField("boolean", "ENABLE_CUSTOM_GAMES", "true") + } + create("playstore") { + dimension = "distribution" + buildConfigField("boolean", "ENABLE_CUSTOM_GAMES", "false") + } + } + buildTypes { debug { isDebuggable = true diff --git a/app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt b/app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt index 853fb01a24..03e5d837e3 100644 --- a/app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt +++ b/app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt @@ -1,6 +1,7 @@ package app.gamenative.ui.enums import androidx.annotation.StringRes +import app.gamenative.BuildConfig import app.gamenative.R enum class LibraryTab( @@ -68,16 +69,28 @@ enum class LibraryTab( ); companion object { - fun LibraryTab.next(): LibraryTab { - val values = entries - val nextIndex = (ordinal + 1) % values.size - return values[nextIndex] + fun availableTabs(): List = + if (BuildConfig.ENABLE_CUSTOM_GAMES) { + entries + } else { + entries.filterNot { it == LOCAL } + } + + fun sanitize(tab: LibraryTab): LibraryTab = + availableTabs().find { it == tab } ?: ALL + + fun LibraryTab.next(availableTabs: List = LibraryTab.availableTabs()): LibraryTab { + val currentIndex = availableTabs.indexOf(this) + if (currentIndex == -1) return ALL + val nextIndex = (currentIndex + 1) % availableTabs.size + return availableTabs[nextIndex] } - fun LibraryTab.previous(): LibraryTab { - val values = entries - val prevIndex = if (ordinal == 0) values.size - 1 else ordinal - 1 - return values[prevIndex] + fun LibraryTab.previous(availableTabs: List = LibraryTab.availableTabs()): LibraryTab { + val currentIndex = availableTabs.indexOf(this) + if (currentIndex == -1) return ALL + val prevIndex = if (currentIndex == 0) availableTabs.size - 1 else currentIndex - 1 + return availableTabs[prevIndex] } } } diff --git a/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt b/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt index 245ee3d9fc..dbd7242e0c 100644 --- a/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt +++ b/app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt @@ -7,16 +7,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import app.gamenative.BuildConfig import app.gamenative.PluviaApp import app.gamenative.PrefManager +import app.gamenative.data.AmazonGame +import app.gamenative.data.EpicGame +import app.gamenative.data.GOGGame import app.gamenative.data.GameCompatibilityStatus import app.gamenative.data.GameSource import app.gamenative.data.LibraryItem import app.gamenative.data.SteamApp import app.gamenative.events.AndroidEvent -import app.gamenative.data.GOGGame -import app.gamenative.data.EpicGame -import app.gamenative.data.AmazonGame import app.gamenative.db.dao.SteamAppDao import app.gamenative.db.dao.GOGGameDao import app.gamenative.db.dao.EpicGameDao @@ -69,6 +70,8 @@ class LibraryViewModel @Inject constructor( private val _state = MutableStateFlow(LibraryState(isLoading = true)) val state: StateFlow = _state.asStateFlow() + private val customGamesEnabled = BuildConfig.ENABLE_CUSTOM_GAMES + // Keep the library scroll state. This will last longer as the VM will stay alive. var listState: LazyGridState by mutableStateOf(LazyGridState(0, 0)) @@ -214,6 +217,7 @@ class LibraryViewModel @Inject constructor( } GameSource.CUSTOM_GAME -> { + if (!customGamesEnabled) return val newValue = !current.showCustomGamesInLibrary PrefManager.showCustomGamesInLibrary = newValue _state.update { it.copy(showCustomGamesInLibrary = newValue) } @@ -248,13 +252,14 @@ class LibraryViewModel @Inject constructor( } fun onTabChanged(tab: LibraryTab) { - _state.update { it.copy(currentTab = tab) } + _state.update { it.copy(currentTab = LibraryTab.sanitize(tab)) } onFilterApps(0) // Reset to first page and refresh } fun onNextTab() { + val availableTabs = LibraryTab.availableTabs() _state.update { currentState -> - val nextTab = currentState.currentTab.next() + val nextTab = LibraryTab.sanitize(currentState.currentTab).next(availableTabs) Timber.tag("LibraryViewModel").d("Tab next via bumper: ${currentState.currentTab} -> $nextTab") currentState.copy(currentTab = nextTab) } @@ -262,8 +267,9 @@ class LibraryViewModel @Inject constructor( } fun onPreviousTab() { + val availableTabs = LibraryTab.availableTabs() _state.update { currentState -> - val previousTab = currentState.currentTab.previous() + val previousTab = LibraryTab.sanitize(currentState.currentTab).previous(availableTabs) Timber.tag("LibraryViewModel").d("Tab previous via bumper: ${currentState.currentTab} -> $previousTab") currentState.copy(currentTab = previousTab) } @@ -348,6 +354,8 @@ class LibraryViewModel @Inject constructor( } fun addCustomGameFolder(path: String) { + if (!customGamesEnabled) return + viewModelScope.launch(Dispatchers.IO) { val normalizedPath = File(path).absolutePath val libraryItem = CustomGameScanner.createLibraryItemFromFolder(normalizedPath) @@ -486,8 +494,8 @@ class LibraryViewModel @Inject constructor( } // Scan Custom Games roots and create UI items (filtered by search query inside scanner) - // Only include custom games if GAME filter is selected - val customGameItems = if (currentState.appInfoSortType.contains(AppFilter.GAME)) { + // Only include custom games if the build supports them and GAME filter is selected + val customGameItems = if (customGamesEnabled && currentState.appInfoSortType.contains(AppFilter.GAME)) { CustomGameScanner.scanAsLibraryItems( query = currentState.searchQuery, ) @@ -642,13 +650,13 @@ class LibraryViewModel @Inject constructor( // Compute effective source filters based on current tab // ALL tab uses user preferences, other tabs override with their presets // Use captured currentState (not _state.value) to avoid TOCTOU race - val currentTab = currentState.currentTab + val currentTab = LibraryTab.sanitize(currentState.currentTab) val includeSteam = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { currentState.showSteamInLibrary } else { currentTab.showSteam } - val includeOpen = if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { + val includeOpen = customGamesEnabled && if (currentTab == app.gamenative.ui.enums.LibraryTab.ALL) { currentState.showCustomGamesInLibrary } else { currentTab.showCustom @@ -755,7 +763,7 @@ class LibraryViewModel @Inject constructor( // Per-source counts for tab badges // Use user prefs + auth state only (not current tab) so badges stay stable across tab switches allCount = (if (currentState.showSteamInLibrary) steamEntries.size else 0) + - (if (currentState.showCustomGamesInLibrary) customEntries.size else 0) + + (if (customGamesEnabled && currentState.showCustomGamesInLibrary) customEntries.size else 0) + (if (currentState.showGOGInLibrary && GOGService.hasStoredCredentials(context)) gogEntries.size else 0) + (if (currentState.showEpicInLibrary && EpicService.hasStoredCredentials(context)) epicEntries.size else 0) + (if (currentState.showAmazonInLibrary && AmazonService.hasStoredCredentials(context)) amazonEntries.size else 0), @@ -763,7 +771,7 @@ class LibraryViewModel @Inject constructor( gogCount = if (currentState.showGOGInLibrary && GOGService.hasStoredCredentials(context)) gogEntries.size else 0, epicCount = if (currentState.showEpicInLibrary && EpicService.hasStoredCredentials(context)) epicEntries.size else 0, amazonCount = if (currentState.showAmazonInLibrary && AmazonService.hasStoredCredentials(context)) amazonEntries.size else 0, - localCount = if (currentState.showCustomGamesInLibrary) customEntries.size else 0, + localCount = if (customGamesEnabled && currentState.showCustomGamesInLibrary) customEntries.size else 0, ) } } diff --git a/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt b/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt index 942c861779..de970de675 100644 --- a/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt +++ b/app/src/main/java/app/gamenative/ui/screen/library/LibraryScreen.kt @@ -4,7 +4,6 @@ import android.content.Intent import android.content.res.Configuration import android.view.KeyEvent import android.view.MotionEvent -import app.gamenative.ui.util.SnackbarManager import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -66,6 +65,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope +import app.gamenative.BuildConfig import app.gamenative.PrefManager import app.gamenative.PluviaApp import app.gamenative.R @@ -101,6 +101,7 @@ import app.gamenative.ui.screen.library.components.SystemMenu import app.gamenative.ui.theme.PluviaTheme import app.gamenative.ui.util.PlatformAuthUiHelpers import app.gamenative.ui.util.PlatformLogoutCallbacks +import app.gamenative.ui.util.SnackbarManager import app.gamenative.service.amazon.AmazonService import app.gamenative.service.epic.EpicService import app.gamenative.service.gog.GOGService @@ -180,6 +181,7 @@ private fun LibraryScreenContent( ) { val context = LocalContext.current val lifecycleScope = LocalLifecycleOwner.current.lifecycleScope + val customGamesEnabled = BuildConfig.ENABLE_CUSTOM_GAMES val gogOAuthLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult(), @@ -421,10 +423,18 @@ private fun LibraryScreenContent( // Handle opening folder picker (with dialog check) val onAddCustomGameClick = { - if (PrefManager.showAddCustomGameDialog) { - showAddCustomGameDialog = true - } else { - folderPicker.launchPicker() + if (customGamesEnabled) { + if (PrefManager.showAddCustomGameDialog) { + showAddCustomGameDialog = true + } else { + folderPicker.launchPicker() + } + } + } + + LaunchedEffect(customGamesEnabled, state.currentTab) { + if (!customGamesEnabled && state.currentTab == LibraryTab.LOCAL) { + onTabChanged(LibraryTab.ALL) } } @@ -820,7 +830,7 @@ private fun LibraryScreenContent( // X button - add custom game KeyEvent.KEYCODE_BUTTON_X -> { - if (selectedAppId == null && !state.isSearching && !state.isOptionsPanelOpen && !isSystemMenuOpen) { + if (customGamesEnabled && selectedAppId == null && !state.isSearching && !state.isOptionsPanelOpen && !isSystemMenuOpen) { onAddCustomGameClick() true } else { @@ -867,7 +877,7 @@ private fun LibraryScreenContent( LibraryTab.GOG -> !GOGService.hasStoredCredentials(context) LibraryTab.EPIC -> !EpicService.hasStoredCredentials(context) LibraryTab.AMAZON -> !AmazonService.hasStoredCredentials(context) - LibraryTab.LOCAL -> PrefManager.customGamesCount == 0 + LibraryTab.LOCAL -> customGamesEnabled && PrefManager.customGamesCount == 0 else -> false } if (showEmptyStateSplash) { @@ -1027,34 +1037,46 @@ private fun LibraryScreenContent( ), ) } else { - listOf( - LibraryActions.select, - GamepadAction( - button = GamepadButton.SELECT, - labelResId = R.string.options, - onClick = { onOptionsPanelToggle(true) }, - ), - GamepadAction( - button = GamepadButton.START, - labelResId = R.string.action_system, - onClick = { isSystemMenuOpen = true }, - ), - GamepadAction( - button = GamepadButton.B, - labelResId = R.string.menu, - onClick = { isSystemMenuOpen = true }, - ), - GamepadAction( - button = GamepadButton.Y, - labelResId = R.string.search, - onClick = { onIsSearching(true) }, - ), - GamepadAction( - button = GamepadButton.X, - labelResId = R.string.action_add_game, - onClick = onAddCustomGameClick, - ), - ) + buildList { + add(LibraryActions.select) + add( + GamepadAction( + button = GamepadButton.SELECT, + labelResId = R.string.options, + onClick = { onOptionsPanelToggle(true) }, + ), + ) + add( + GamepadAction( + button = GamepadButton.START, + labelResId = R.string.action_system, + onClick = { isSystemMenuOpen = true }, + ), + ) + add( + GamepadAction( + button = GamepadButton.B, + labelResId = R.string.menu, + onClick = { isSystemMenuOpen = true }, + ), + ) + add( + GamepadAction( + button = GamepadButton.Y, + labelResId = R.string.search, + onClick = { onIsSearching(true) }, + ), + ) + if (customGamesEnabled) { + add( + GamepadAction( + button = GamepadButton.X, + labelResId = R.string.action_add_game, + onClick = onAddCustomGameClick, + ), + ) + } + } } GamepadActionBar( diff --git a/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt b/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt index 61a9c613a2..08ff43dad6 100644 --- a/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt +++ b/app/src/main/java/app/gamenative/ui/screen/library/components/LibraryTabBar.kt @@ -5,8 +5,8 @@ import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.spring -import androidx.compose.foundation.background import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.focusGroup import androidx.compose.foundation.horizontalScroll @@ -58,6 +58,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp +import app.gamenative.BuildConfig import app.gamenative.R import app.gamenative.ui.enums.LibraryTab import app.gamenative.ui.theme.PluviaTheme @@ -133,13 +134,15 @@ private fun CompactLibraryTabBar( onNextTab: () -> Unit, modifier: Modifier = Modifier, ) { - val tabs = LibraryTab.entries - val currentIndex = tabs.indexOf(currentTab) + val tabs = LibraryTab.availableTabs() + val selectedTab = tabs.find { it == currentTab } ?: tabs.first() + val currentIndex = tabs.indexOf(selectedTab) + val showAddGameButton = BuildConfig.ENABLE_CUSTOM_GAMES val scrollState = rememberScrollState() val tabPositions = remember { mutableStateMapOf() } val tabWidths = remember { mutableStateMapOf() } - LaunchedEffect(currentTab) { + LaunchedEffect(selectedTab) { val pos = tabPositions[currentIndex] ?: return@LaunchedEffect val width = tabWidths[currentIndex] ?: return@LaunchedEffect val targetCenter = (pos + width / 2).toInt() @@ -206,7 +209,7 @@ private fun CompactLibraryTabBar( verticalAlignment = Alignment.CenterVertically, ) { tabs.forEachIndexed { index, tab -> - val isSelected = tab == currentTab + val isSelected = tab == selectedTab val tabInteractionSource = remember { MutableInteractionSource() } val isTabFocused by tabInteractionSource.collectIsFocusedAsState() Box( @@ -277,11 +280,13 @@ private fun CompactLibraryTabBar( contentDescription = stringResource(R.string.search), onClick = onSearchClick, ) - CompactIconButton( - icon = Icons.Default.Add, - contentDescription = stringResource(R.string.action_add_game), - onClick = onAddGameClick, - ) + if (showAddGameButton) { + CompactIconButton( + icon = Icons.Default.Add, + contentDescription = stringResource(R.string.action_add_game), + onClick = onAddGameClick, + ) + } CompactIconButton( icon = Icons.Default.Menu, contentDescription = stringResource(R.string.menu), @@ -371,9 +376,11 @@ private fun ExpandedLibraryTabBar( onNextTab: () -> Unit, modifier: Modifier = Modifier, ) { - val tabs = LibraryTab.entries - val currentIndex = tabs.indexOf(currentTab) + val tabs = LibraryTab.availableTabs() + val selectedTab = tabs.find { it == currentTab } ?: tabs.first() + val currentIndex = tabs.indexOf(selectedTab) val scrollState = rememberScrollState() + val showAddGameButton = BuildConfig.ENABLE_CUSTOM_GAMES val tabPositions = remember { mutableStateMapOf() } val tabWidths = remember { mutableStateMapOf() } @@ -398,7 +405,7 @@ private fun ExpandedLibraryTabBar( label = "indicatorWidth", ) - LaunchedEffect(currentTab) { + LaunchedEffect(selectedTab) { val pos = tabPositions[currentIndex] ?: return@LaunchedEffect val width = tabWidths[currentIndex] ?: return@LaunchedEffect val targetCenter = (pos + width / 2).toInt() @@ -496,7 +503,7 @@ private fun ExpandedLibraryTabBar( TabItem( tab = tab, count = tabCounts[tab], - isSelected = tab == currentTab, + isSelected = tab == selectedTab, onClick = { onTabSelected(tab) }, onPositioned = { position, width -> tabPositions[index] = position @@ -513,11 +520,13 @@ private fun ExpandedLibraryTabBar( onClick = onSearchClick, ) - IconActionButton( - icon = Icons.Default.Add, - contentDescription = stringResource(R.string.action_add_game), - onClick = onAddGameClick, - ) + if (showAddGameButton) { + IconActionButton( + icon = Icons.Default.Add, + contentDescription = stringResource(R.string.action_add_game), + onClick = onAddGameClick, + ) + } IconActionButton( icon = Icons.Default.Menu, diff --git a/ubuntufs/build.gradle.kts b/ubuntufs/build.gradle.kts index b3257cf0fe..4a029632c7 100644 --- a/ubuntufs/build.gradle.kts +++ b/ubuntufs/build.gradle.kts @@ -11,6 +11,16 @@ android { // testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + flavorDimensions += "distribution" + productFlavors { + create("oss") { + dimension = "distribution" + } + create("playstore") { + dimension = "distribution" + } + } + buildTypes { create("release-signed") { initWith(getByName("release"))