diff --git a/app/src/main/app/shell/UnifiedActivity.kt b/app/src/main/app/shell/UnifiedActivity.kt index 11b8e3ff6..68eb0b331 100644 --- a/app/src/main/app/shell/UnifiedActivity.kt +++ b/app/src/main/app/shell/UnifiedActivity.kt @@ -82,6 +82,7 @@ import androidx.compose.material.icons.automirrored.outlined.ExitToApp import androidx.compose.material.icons.automirrored.outlined.OpenInNew import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.saveable.rememberSaveable @@ -3455,6 +3456,7 @@ class UnifiedActivity : var customApps by remember { mutableStateOf>(emptyList()) } var localLibraryRefreshKey by remember { mutableIntStateOf(0) } var shortcutsLoaded by remember { mutableStateOf(false) } + var pullRefreshing by remember { mutableStateOf(false) } LaunchedEffect(shortcutRefreshKey, localLibraryRefreshKey) { shortcutsLoaded = false @@ -3531,7 +3533,7 @@ class UnifiedActivity : var libraryLoaded by remember { mutableStateOf(false) } // Suppress transient empty states before background recomputation starts. val scanInputToken = - remember(steamApps, epicApps, gogApps, customApps, libraryRefreshKey) { Any() } + remember(steamApps, epicApps, gogApps, customApps, libraryRefreshKey, localLibraryRefreshKey) { Any() } var processedScanToken by remember { mutableStateOf(null) } LaunchedEffect(scanInputToken) { @@ -3590,6 +3592,7 @@ class UnifiedActivity : } libraryLoaded = true processedScanToken = scanInputToken + pullRefreshing = false } } } @@ -3883,8 +3886,20 @@ class UnifiedActivity : navigateToSettings(SettingsNavItem.STORES) } } else if (anyLoggedIn) { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - EmptyStateMessage(stringResource(R.string.library_games_no_games_installed)) + PullToRefreshBox( + isRefreshing = pullRefreshing, + onRefresh = { + pullRefreshing = true + localLibraryRefreshKey++ + }, + modifier = Modifier.fillMaxSize(), + ) { + Box( + Modifier.fillMaxSize().verticalScroll(rememberScrollState()), + contentAlignment = Alignment.Center, + ) { + EmptyStateMessage(stringResource(R.string.library_games_no_games_installed)) + } } } return @@ -4053,136 +4068,145 @@ class UnifiedActivity : } } - when (layoutMode) { - LibraryLayoutMode.GRID_4 -> { - FourByTwoGridView( - items = displayedApps, - modifier = Modifier.tabScreenPadding(), - gridState = gridState, - contentPadding = TabGridContentPadding, - clipContent = false, - keyOf = { it.id }, - ) { app, index, rowHeight -> - GameCapsule( - app = app, - gogGame = visibleGogByPseudoId[app.id], - epicGame = visibleEpicByPseudoId[app.id], - iconRefreshKey = iconRefreshKey, - artworkCacheRefreshKey = artworkCacheRefreshKey, - isFocusedOverride = index == focusIndex, - isControllerActive = isControllerConnected, - customArtworkPath = visibleCustomGridArtworkPathByAppId[app.id] ?: visibleCustomArtworkPathByAppId[app.id], - customIconPath = visibleCustomIconPathByAppId[app.id], - onClick = { - detailGogGame = visibleGogByPseudoId[app.id] - detailApp = app - }, - onLongClick = { - openSettingsForApp(index, app) - }, - modifier = - Modifier - .height(rowHeight) - .then( - if (index in focusRequesters.indices) { - Modifier.focusRequester(focusRequesters[index]) - } else { - Modifier - }, - ), - ) + PullToRefreshBox( + isRefreshing = pullRefreshing, + onRefresh = { + pullRefreshing = true + localLibraryRefreshKey++ + }, + modifier = Modifier.fillMaxSize(), + ) { + when (layoutMode) { + LibraryLayoutMode.GRID_4 -> { + FourByTwoGridView( + items = displayedApps, + modifier = Modifier.tabScreenPadding(), + gridState = gridState, + contentPadding = TabGridContentPadding, + clipContent = false, + keyOf = { it.id }, + ) { app, index, rowHeight -> + GameCapsule( + app = app, + gogGame = visibleGogByPseudoId[app.id], + epicGame = visibleEpicByPseudoId[app.id], + iconRefreshKey = iconRefreshKey, + artworkCacheRefreshKey = artworkCacheRefreshKey, + isFocusedOverride = index == focusIndex, + isControllerActive = isControllerConnected, + customArtworkPath = visibleCustomGridArtworkPathByAppId[app.id] ?: visibleCustomArtworkPathByAppId[app.id], + customIconPath = visibleCustomIconPathByAppId[app.id], + onClick = { + detailGogGame = visibleGogByPseudoId[app.id] + detailApp = app + }, + onLongClick = { + openSettingsForApp(index, app) + }, + modifier = + Modifier + .height(rowHeight) + .then( + if (index in focusRequesters.indices) { + Modifier.focusRequester(focusRequesters[index]) + } else { + Modifier + }, + ), + ) + } } - } - LibraryLayoutMode.CAROUSEL -> { - CarouselView( - items = displayedApps, - modifier = Modifier.tabScreenPadding(top = TabCarouselTopPadding, bottom = TabCarouselBottomPadding), - listState = carouselState, - selectedIndex = focusIndex, - onCenteredIndexChanged = { centeredIndex -> - if (activity != null && activity.libraryFocusIndex.value != centeredIndex) { - activity.libraryFocusIndex.value = centeredIndex - } - }, - ) { app, index, isSelected, cardWidth, cardHeight -> - GameCapsule( - app = app, - gogGame = visibleGogByPseudoId[app.id], - epicGame = visibleEpicByPseudoId[app.id], - iconRefreshKey = iconRefreshKey, - artworkCacheRefreshKey = artworkCacheRefreshKey, - isFocusedOverride = isSelected, - isControllerActive = isControllerConnected, - customArtworkPath = visibleCustomCarouselArtworkPathByAppId[app.id] ?: visibleCustomArtworkPathByAppId[app.id], - customIconPath = visibleCustomIconPathByAppId[app.id], - onClick = { - detailGogGame = visibleGogByPseudoId[app.id] - detailApp = app + LibraryLayoutMode.CAROUSEL -> { + CarouselView( + items = displayedApps, + modifier = Modifier.tabScreenPadding(top = TabCarouselTopPadding, bottom = TabCarouselBottomPadding), + listState = carouselState, + selectedIndex = focusIndex, + onCenteredIndexChanged = { centeredIndex -> + if (activity != null && activity.libraryFocusIndex.value != centeredIndex) { + activity.libraryFocusIndex.value = centeredIndex + } }, - onLongClick = { openSettingsForApp(index, app) }, - useLibraryCapsule = true, - modifier = - Modifier - .fillMaxSize() - .then( - if (index in focusRequesters.indices) { - Modifier.focusRequester(focusRequesters[index]) - } else { - Modifier - }, - ), - ) + ) { app, index, isSelected, cardWidth, cardHeight -> + GameCapsule( + app = app, + gogGame = visibleGogByPseudoId[app.id], + epicGame = visibleEpicByPseudoId[app.id], + iconRefreshKey = iconRefreshKey, + artworkCacheRefreshKey = artworkCacheRefreshKey, + isFocusedOverride = isSelected, + isControllerActive = isControllerConnected, + customArtworkPath = visibleCustomCarouselArtworkPathByAppId[app.id] ?: visibleCustomArtworkPathByAppId[app.id], + customIconPath = visibleCustomIconPathByAppId[app.id], + onClick = { + detailGogGame = visibleGogByPseudoId[app.id] + detailApp = app + }, + onLongClick = { openSettingsForApp(index, app) }, + useLibraryCapsule = true, + modifier = + Modifier + .fillMaxSize() + .then( + if (index in focusRequesters.indices) { + Modifier.focusRequester(focusRequesters[index]) + } else { + Modifier + }, + ), + ) + } } - } - LibraryLayoutMode.LIST -> { - val listViewState = rememberLazyListState() - ListView( - items = displayedApps, - modifier = Modifier.tabScreenPadding(), - listState = listViewState, - contentPadding = TabListContentPadding, - selectedIndex = focusIndex, - onSelectedIndexChanged = { newIdx -> - activity?.libraryFocusIndex?.value = newIdx - }, - keyOf = { it.id }, - ) { app, index, isSelected -> - GameCapsule( - app = app, - gogGame = visibleGogByPseudoId[app.id], - epicGame = visibleEpicByPseudoId[app.id], - iconRefreshKey = iconRefreshKey, - artworkCacheRefreshKey = artworkCacheRefreshKey, - isFocusedOverride = isSelected, - isControllerActive = isControllerConnected, - customArtworkPath = visibleCustomListArtworkPathByAppId[app.id] ?: visibleCustomArtworkPathByAppId[app.id], - customIconPath = visibleCustomIconPathByAppId[app.id], - onClick = { - detailGogGame = visibleGogByPseudoId[app.id] - detailApp = app + LibraryLayoutMode.LIST -> { + val listViewState = rememberLazyListState() + ListView( + items = displayedApps, + modifier = Modifier.tabScreenPadding(), + listState = listViewState, + contentPadding = TabListContentPadding, + selectedIndex = focusIndex, + onSelectedIndexChanged = { newIdx -> + activity?.libraryFocusIndex?.value = newIdx }, - onLongClick = { openSettingsForApp(index, app) }, - listMode = true, - modifier = - Modifier - .then( - if (index in focusRequesters.indices) { - Modifier.focusRequester(focusRequesters[index]) - } else { - Modifier - }, - ), + keyOf = { it.id }, + ) { app, index, isSelected -> + GameCapsule( + app = app, + gogGame = visibleGogByPseudoId[app.id], + epicGame = visibleEpicByPseudoId[app.id], + iconRefreshKey = iconRefreshKey, + artworkCacheRefreshKey = artworkCacheRefreshKey, + isFocusedOverride = isSelected, + isControllerActive = isControllerConnected, + customArtworkPath = visibleCustomListArtworkPathByAppId[app.id] ?: visibleCustomArtworkPathByAppId[app.id], + customIconPath = visibleCustomIconPathByAppId[app.id], + onClick = { + detailGogGame = visibleGogByPseudoId[app.id] + detailApp = app + }, + onLongClick = { openSettingsForApp(index, app) }, + listMode = true, + modifier = + Modifier + .then( + if (index in focusRequesters.indices) { + Modifier.focusRequester(focusRequesters[index]) + } else { + Modifier + }, + ), + ) + } + JoystickListScroll( + listState = listViewState, + stickFlow = activity?.rightStickScrollState, + minSpeed = 2.5f, + maxSpeed = 16f, + quadratic = true, ) } - JoystickListScroll( - listState = listViewState, - stickFlow = activity?.rightStickScrollState, - minSpeed = 2.5f, - maxSpeed = 16f, - quadratic = true, - ) } } diff --git a/app/src/main/feature/stores/steam/service/SteamService.kt b/app/src/main/feature/stores/steam/service/SteamService.kt index 665e65695..f65da1ef3 100644 --- a/app/src/main/feature/stores/steam/service/SteamService.kt +++ b/app/src/main/feature/stores/steam/service/SteamService.kt @@ -1258,7 +1258,11 @@ class SteamService : Service() { ) } - fun getAppInfoOf(appId: Int): SteamApp? = runBlocking(Dispatchers.IO) { instance?.appDao?.findApp(appId) } + fun getAppInfoOf(appId: Int): SteamApp? = + runBlocking(Dispatchers.IO) { + val dao = instance?.appDao ?: runCatching { PluviaDatabase.getInstance().steamAppDao() }.getOrNull() + dao?.findApp(appId) + } fun getDownloadingAppInfoOf(appId: Int): DownloadingAppInfo? = runBlocking(Dispatchers.IO) { @@ -1316,7 +1320,11 @@ class SteamService : Service() { fun getHiddenDlcAppsOf(appId: Int): List? = runBlocking(Dispatchers.IO) { instance?.appDao?.findHiddenDLCApps(appId) } - fun getInstalledApp(appId: Int): AppInfo? = runBlocking(Dispatchers.IO) { instance?.appInfoDao?.getInstalledApp(appId) } + fun getInstalledApp(appId: Int): AppInfo? = + runBlocking(Dispatchers.IO) { + val dao = instance?.appInfoDao ?: runCatching { PluviaDatabase.getInstance().appInfoDao() }.getOrNull() + dao?.getInstalledApp(appId) + } fun getInstalledDepotsOf(appId: Int): List? = getTrustedInstalledAppInfo(appId)?.downloadedDepots @@ -1355,6 +1363,7 @@ class SteamService : Service() { private fun tryRecoverInstalledAppInfo(appId: Int): AppInfo? { val dirPath = getAppDirPath(appId) + if (dirPath.isBlank()) return null val hasCompleteMarker = MarkerUtils.hasMarker(dirPath, Marker.DOWNLOAD_COMPLETE_MARKER) val hasInProgressMarker = MarkerUtils.hasMarker(dirPath, Marker.DOWNLOAD_IN_PROGRESS_MARKER) if (!hasCompleteMarker || hasInProgressMarker) return null @@ -2144,6 +2153,12 @@ class SteamService : Service() { if (oldName.isNotEmpty() && oldName != appName) add(oldName) } + // No resolvable folder name (metadata unavailable) — never fall back to a shared root. + if (candidateNames.isEmpty()) { + Timber.w("getAppDirPath: no metadata to resolve install dir for appId=%d", gameId) + return "" + } + // Respect user-selected default download folder val context = PluviaApp.instance.applicationContext if (context != null) {