Skip to content
Merged
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
274 changes: 149 additions & 125 deletions app/src/main/app/shell/UnifiedActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -3455,6 +3456,7 @@ class UnifiedActivity :
var customApps by remember { mutableStateOf<List<SteamApp>>(emptyList()) }
var localLibraryRefreshKey by remember { mutableIntStateOf(0) }
var shortcutsLoaded by remember { mutableStateOf(false) }
var pullRefreshing by remember { mutableStateOf(false) }
LaunchedEffect(shortcutRefreshKey, localLibraryRefreshKey) {
shortcutsLoaded = false

Expand Down Expand Up @@ -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<Any?>(null) }

LaunchedEffect(scanInputToken) {
Expand Down Expand Up @@ -3590,6 +3592,7 @@ class UnifiedActivity :
}
libraryLoaded = true
processedScanToken = scanInputToken
pullRefreshing = false
}
}
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
}
}

Expand Down
19 changes: 17 additions & 2 deletions app/src/main/feature/stores/steam/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -1316,7 +1320,11 @@ class SteamService : Service() {

fun getHiddenDlcAppsOf(appId: Int): List<SteamApp>? = 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<Int>? = getTrustedInstalledAppInfo(appId)?.downloadedDepots

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