Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pluvia-pr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 12 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
29 changes: 21 additions & 8 deletions app/src/main/java/app/gamenative/ui/enums/LibraryTab.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.gamenative.ui.enums

import androidx.annotation.StringRes
import app.gamenative.BuildConfig
import app.gamenative.R

enum class LibraryTab(
Expand Down Expand Up @@ -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<LibraryTab> =
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> = 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> = 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]
}
}
}
32 changes: 20 additions & 12 deletions app/src/main/java/app/gamenative/ui/model/LibraryViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -69,6 +70,8 @@ class LibraryViewModel @Inject constructor(
private val _state = MutableStateFlow(LibraryState(isLoading = true))
val state: StateFlow<LibraryState> = _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))

Expand Down Expand Up @@ -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) }
Expand Down Expand Up @@ -248,22 +252,24 @@ 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)
}
onFilterApps(0)
}

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)
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -755,15 +763,15 @@ 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),
steamCount = if (currentState.showSteamInLibrary) steamEntries.size else 0,
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,
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
Loading
Loading