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
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ interface PlatformDao {
@Query("SELECT * FROM platforms WHERE gameCount > 0 AND isVisible = 1 AND syncEnabled = 1 ORDER BY sortOrder ASC, name ASC")
suspend fun getPlatformsWithGames(): List<PlatformEntity>

@Insert(onConflict = OnConflictStrategy.IGNORE)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(platform: PlatformEntity)

@Insert(onConflict = OnConflictStrategy.IGNORE)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertAll(platforms: List<PlatformEntity>)

@Update
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,24 @@ import androidx.lifecycle.compose.LifecycleEventEffect
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.FilterList
import androidx.compose.material.icons.automirrored.filled.Sort
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.FilterChip
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import com.nendo.argosy.data.local.entity.PlatformEntity
import com.nendo.argosy.ui.filebrowser.FileBrowserMode
import com.nendo.argosy.ui.filebrowser.FileBrowserScreen
import com.nendo.argosy.ui.input.LocalInputDispatcher
import com.nendo.argosy.ui.theme.Dimens
import com.nendo.argosy.util.PlatformFilterLogic

@Composable
fun FirstRunScreen(
Expand Down Expand Up @@ -218,10 +229,15 @@ fun FirstRunScreen(
)
FirstRunStep.PLATFORM_SELECT -> PlatformSelectStep(
platforms = uiState.platforms,
hasGames = uiState.platformFilterHasGames,
searchQuery = uiState.platformFilterSearchQuery,
focusedIndex = uiState.focusedIndex,
buttonFocusIndex = uiState.platformButtonFocus,
onToggle = { viewModel.togglePlatform(it) },
onToggleAll = { viewModel.toggleAllPlatforms() },
onSortModeChange = { viewModel.setPlatformFilterSortMode(it) },
onHasGamesChange = { viewModel.setPlatformFilterHasGames(it) },
onSearchQueryChange = { viewModel.setPlatformFilterSearchQuery(it) },
onContinue = { viewModel.proceedFromPlatformSelect() }
)
FirstRunStep.CORE_DOWNLOAD -> CoreDownloadStep(
Expand Down Expand Up @@ -831,10 +847,15 @@ private fun UsageStatsStep(
@Composable
private fun PlatformSelectStep(
platforms: List<PlatformEntity>,
hasGames: Boolean,
searchQuery: String,
focusedIndex: Int,
buttonFocusIndex: Int,
onToggle: (Long) -> Unit,
onToggleAll: () -> Unit,
onSortModeChange: (PlatformFilterLogic.SortMode) -> Unit,
onHasGamesChange: (Boolean) -> Unit,
onSearchQueryChange: (String) -> Unit,
onContinue: () -> Unit
) {
val listState = rememberLazyListState()
Expand All @@ -848,6 +869,13 @@ private fun PlatformSelectStep(
}
}

// Scroll to top when the filtered list changes (e.g. search, sort, filter)
LaunchedEffect(platforms) {
if (platforms.isNotEmpty()) {
listState.scrollToItem(0)
}
}

Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
Expand All @@ -870,6 +898,126 @@ private fun PlatformSelectStep(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(Dimens.spacingSm))

Row(
modifier = Modifier.fillMaxWidth(0.9f),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
var showSearch by remember { mutableStateOf(searchQuery.isNotEmpty()) }
var showSortMenu by remember { mutableStateOf(false) }

if (showSearch) {
TextField(
value = searchQuery,
onValueChange = onSearchQueryChange,
placeholder = { Text("Search platforms...") },
modifier = Modifier.weight(1f),
singleLine = true,
leadingIcon = {
Icon(Icons.Default.Search, "Search")
},
trailingIcon = {
IconButton(onClick = {
onSearchQueryChange("")
showSearch = false
}) {
Icon(Icons.Default.Close, "Clear")
}
},
colors = TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surfaceVariant,
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceVariant
)
)
} else {
Text(
text = "${platforms.size} platforms",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f)
)
}

Row(horizontalArrangement = Arrangement.spacedBy(Dimens.spacingXs)) {
if (!showSearch) {
IconButton(onClick = { showSearch = true }) {
Icon(Icons.Default.Search, "Search")
}
}

Box {
IconButton(onClick = { showSortMenu = true }) {
Icon(Icons.AutoMirrored.Filled.Sort, "Sort")
}
DropdownMenu(
expanded = showSortMenu,
onDismissRequest = { showSortMenu = false }
) {
DropdownMenuItem(
text = { Text("Default") },
onClick = {
onSortModeChange(PlatformFilterLogic.SortMode.DEFAULT)
showSortMenu = false
}
)
DropdownMenuItem(
text = { Text("Name (A-Z)") },
onClick = {
onSortModeChange(PlatformFilterLogic.SortMode.NAME_ASC)
showSortMenu = false
}
)
DropdownMenuItem(
text = { Text("Name (Z-A)") },
onClick = {
onSortModeChange(PlatformFilterLogic.SortMode.NAME_DESC)
showSortMenu = false
}
)
DropdownMenuItem(
text = { Text("Most Games") },
onClick = {
onSortModeChange(PlatformFilterLogic.SortMode.MOST_GAMES)
showSortMenu = false
}
)
DropdownMenuItem(
text = { Text("Least Games") },
onClick = {
onSortModeChange(PlatformFilterLogic.SortMode.LEAST_GAMES)
showSortMenu = false
}
)
}
}

if (hasGames) {
FilterChip(
selected = true,
onClick = { onHasGamesChange(false) },
label = { Text("Has Games") },
leadingIcon = {
Icon(
Icons.Default.FilterList,
contentDescription = null,
modifier = Modifier.size(Dimens.iconXs)
)
}
)
} else {
IconButton(
onClick = { onHasGamesChange(true) }
) {
Icon(
Icons.Default.FilterList,
"Show platforms with games"
)
}
}
}
}
Spacer(modifier = Modifier.height(Dimens.spacingMd))

LazyColumn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import com.nendo.argosy.data.remote.romm.RomMResult
import com.nendo.argosy.libretro.LibretroCoreManager
import com.nendo.argosy.libretro.LibretroCoreRegistry
import com.nendo.argosy.util.PermissionHelper
import com.nendo.argosy.util.PlatformFilterLogic
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
Expand Down Expand Up @@ -67,12 +68,17 @@ data class FirstRunUiState(
val hasUsageStatsPermission: Boolean = false,
val rommFocusField: Int? = null,
val platforms: List<PlatformEntity> = emptyList(),
val platformsAll: List<PlatformEntity> = emptyList(),
val platformFilterSortMode: PlatformFilterLogic.SortMode = PlatformFilterLogic.SortMode.DEFAULT,
val platformFilterHasGames: Boolean = false,
val platformFilterSearchQuery: String = "",
val platformButtonFocus: Int = 1,
val coreDownloads: List<CoreDownloadState> = emptyList(),
val coreDownloadComplete: Boolean = false
)

@HiltViewModel
@Suppress("TooManyFunctions")
class FirstRunViewModel @Inject constructor(
private val application: Application,
private val preferencesRepository: UserPreferencesRepository,
Expand Down Expand Up @@ -138,11 +144,20 @@ class FirstRunViewModel @Inject constructor(
viewModelScope.launch {
when (val result = romMRepository.fetchAndStorePlatforms(defaultSyncEnabled = false)) {
is RomMResult.Success -> {
_uiState.update { it.copy(platforms = result.data) }
val allPlatforms = result.data
val filtered = PlatformFilterLogic.filterAndSort(
items = allPlatforms,
searchQuery = _uiState.value.platformFilterSearchQuery,
hasGames = _uiState.value.platformFilterHasGames,
sortMode = _uiState.value.platformFilterSortMode,
nameSelector = { it.name },
countSelector = { it.gameCount }
)
_uiState.update { it.copy(platformsAll = allPlatforms, platforms = filtered) }
}
is RomMResult.Error -> {
val platforms = platformDao.observeAllPlatforms().first()
_uiState.update { it.copy(platforms = platforms) }
_uiState.update { it.copy(platformsAll = platforms, platforms = platforms) }
}
}
}
Expand Down Expand Up @@ -241,23 +256,57 @@ class FirstRunViewModel @Inject constructor(

fun togglePlatform(platformId: Long) {
viewModelScope.launch {
val platform = _uiState.value.platforms.find { it.id == platformId } ?: return@launch
val platform = _uiState.value.platformsAll.find { it.id == platformId } ?: return@launch
platformDao.updateSyncEnabled(platformId, !platform.syncEnabled)
val updatedPlatforms = platformDao.observeAllPlatforms().first()
_uiState.update { it.copy(platforms = updatedPlatforms) }
applyPlatformFilters()
}
}

fun toggleAllPlatforms() {
viewModelScope.launch {
val platforms = _uiState.value.platforms
val platforms = _uiState.value.platformsAll
val allEnabled = platforms.all { it.syncEnabled }
val newState = !allEnabled
platforms.forEach { platform ->
platformDao.updateSyncEnabled(platform.id, newState)
}
val updatedPlatforms = platformDao.observeAllPlatforms().first()
_uiState.update { it.copy(platforms = updatedPlatforms) }
applyPlatformFilters()
}
}

fun setPlatformFilterSortMode(mode: PlatformFilterLogic.SortMode) {
_uiState.update { it.copy(platformFilterSortMode = mode) }
applyPlatformFilters(resetFocus = true)
}

fun setPlatformFilterHasGames(enabled: Boolean) {
_uiState.update { it.copy(platformFilterHasGames = enabled) }
applyPlatformFilters(resetFocus = true)
}

fun setPlatformFilterSearchQuery(query: String) {
_uiState.update { it.copy(platformFilterSearchQuery = query) }
applyPlatformFilters(resetFocus = true)
}

private fun applyPlatformFilters(resetFocus: Boolean = false) {
viewModelScope.launch {
val allPlatforms = platformDao.observeAllPlatforms().first()
val filtered = PlatformFilterLogic.filterAndSort(
items = allPlatforms,
searchQuery = _uiState.value.platformFilterSearchQuery,
hasGames = _uiState.value.platformFilterHasGames,
sortMode = _uiState.value.platformFilterSortMode,
nameSelector = { it.name },
countSelector = { it.gameCount }
)
_uiState.update {
it.copy(
platformsAll = allPlatforms,
platforms = filtered,
focusedIndex = if (resetFocus) 0 else it.focusedIndex
)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import com.nendo.argosy.ui.input.SoundConfig
import com.nendo.argosy.ui.input.SoundPreset
import com.nendo.argosy.ui.input.SoundType
import com.nendo.argosy.util.LogLevel
import com.nendo.argosy.util.PlatformFilterLogic
import com.nendo.argosy.BuildConfig

enum class SettingsSection {
Expand Down Expand Up @@ -361,6 +362,10 @@ data class SyncSettingsState(
val showPlatformFiltersModal: Boolean = false,
val platformFiltersModalFocusIndex: Int = 0,
val platformFiltersList: List<PlatformFilterItem> = emptyList(),
val platformFiltersAllPlatforms: List<PlatformFilterItem> = emptyList(),
val platformFilterSortMode: PlatformFilterLogic.SortMode = PlatformFilterLogic.SortMode.DEFAULT,
val platformFilterHasGames: Boolean = false,
val platformFilterSearchQuery: String = "",
val isLoadingPlatforms: Boolean = false,
val enabledPlatformCount: Int = 0,
val totalGames: Int = 0,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ import com.nendo.argosy.ui.screens.settings.delegates.SoundSettingsDelegate
import com.nendo.argosy.ui.screens.settings.delegates.SteamSettingsDelegate
import com.nendo.argosy.ui.screens.settings.delegates.StorageSettingsDelegate
import com.nendo.argosy.ui.screens.settings.delegates.SyncSettingsDelegate
import com.nendo.argosy.ui.screens.settings.delegates.setPlatformFilterSortMode
import com.nendo.argosy.ui.screens.settings.delegates.setPlatformFilterHasGames
import com.nendo.argosy.ui.screens.settings.delegates.setPlatformFilterSearchQuery
import com.nendo.argosy.ui.screens.settings.sections.aboutMaxFocusIndex
import com.nendo.argosy.ui.screens.settings.sections.boxArtMaxFocusIndex
import com.nendo.argosy.ui.screens.settings.sections.builtinControlsMaxFocusIndex
Expand Down Expand Up @@ -2079,6 +2082,18 @@ class SettingsViewModel @Inject constructor(
syncDelegate.togglePlatformSyncEnabled(viewModelScope, platformId)
}

fun setPlatformFilterSortMode(mode: com.nendo.argosy.util.PlatformFilterLogic.SortMode) {
syncDelegate.setPlatformFilterSortMode(viewModelScope, mode)
}

fun setPlatformFilterHasGames(enabled: Boolean) {
syncDelegate.setPlatformFilterHasGames(viewModelScope, enabled)
}

fun setPlatformFilterSearchQuery(query: String) {
syncDelegate.setPlatformFilterSearchQuery(viewModelScope, query)
}

fun showRegionPicker() {
syncDelegate.showRegionPicker()
soundManager.play(SoundType.OPEN_MODAL)
Expand Down
Loading