Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
11 changes: 3 additions & 8 deletions app/src/main/java/app/gamenative/data/LibraryItem.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package app.gamenative.data

import app.gamenative.Constants
import app.gamenative.utils.CustomGameScanner

enum class GameSource {
STEAM,
Expand Down Expand Up @@ -55,13 +54,9 @@ data class LibraryItem(
""
}
GameSource.CUSTOM_GAME -> {
// Attempt to resolve a local icon from the selected/unique exe folder
val localPath = CustomGameScanner.findIconFileForCustomGame(appId)
if (!localPath.isNullOrEmpty()) {
if (localPath.startsWith("file://")) localPath else "file://$localPath"
} else {
""
}
// Return empty; icons are fetched asynchronously in UI components
// to avoid blocking the main thread with filesystem scans.
""
}
GameSource.GOG -> {
// GoG Images are typically the full URL, but have fallback just in case.
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/java/app/gamenative/service/SteamService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1410,7 +1410,7 @@ class SteamService : Service(), IChallengeUrlChanged {
fun downloadApp(appId: Int, dlcAppIds: List<Int>, branch: String = "public", isUpdateOrVerify: Boolean): DownloadInfo? {
if (!checkWifiOrNotify()) return null
return getAppInfoOf(appId)?.let { appInfo ->
val container = ContainerManager(instance!!.applicationContext).getContainerById("STEAM_${appId}")
val container = ContainerManager.getInstance(instance!!.applicationContext).getContainerById("STEAM_${appId}")
val containerLanguage = if (container != null) {
container.language
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ class CustomGameAppScreen : BaseAppScreen() {

if (shouldExtract) {
// First, try using the container's selected executable if available
val containerManager = com.winlator.container.ContainerManager(context)
val containerManager = com.winlator.container.ContainerManager.getInstance(context)
val hasContainer = containerManager.hasContainer(libraryItem.appId)
Timber.tag("CustomGameAppScreen").d("Container exists: $hasContainer")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1015,7 +1015,7 @@ class SteamAppScreen : BaseAppScreen() {
}
try {
val info = withContext(Dispatchers.IO) {
val container = ContainerManager(context).getContainerById("STEAM_$gameId")
val container = ContainerManager.getInstance(context).getContainerById("STEAM_$gameId")
val language = container?.language ?: PrefManager.containerLanguage
val depots = SteamService.getDownloadableDepots(gameId, language)
Timber.i("There are ${depots.size} depots belonging to ${libraryItem.appId}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,9 @@ internal fun ListViewCard(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
// Game icon
// Game icon - start with empty to avoid synchronous LibraryItem getter
val iconUrl by produceState(
initialValue = appInfo.clientIconUrl,
initialValue = "",
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
key1 = appInfo.appId,
key2 = appInfo.clientIconUrl,
) {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -220,13 +220,7 @@ private fun InstallStatusBadge(
}
val isDownloading = downloadInfo != null && downloadProgress < 1f
var isInstalled by remember(appInfo.appId) {
mutableStateOf(
if (isSteam) {
SteamService.isAppInstalled(appInfo.gameId)
} else {
true // Custom Games always installed
},
)
mutableStateOf(appInfo.isInstalled)
}

LaunchedEffect(isRefreshing) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
Expand All @@ -18,6 +19,7 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
Expand Down Expand Up @@ -301,19 +303,24 @@ internal fun LibraryListPane(
Modifier
}

if (item.index > 0 && currentLayout == PaneType.LIST) {
HorizontalDivider()
Column {
if (listIndex > 0 && currentLayout == PaneType.LIST) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = horizontalPadding),
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
)
}
AppItem(
modifier = appItemModifier,
appInfo = item,
onClick = { onNavigate(item.appId) },
paneType = currentLayout,
onFocus = { targetOfScroll = item.index },
imageRefreshCounter = state.imageRefreshCounter,
compatibilityStatus = state.compatibilityMap[item.name],
gameStats = state.statsFor(item),
)
}
AppItem(
modifier = appItemModifier,
appInfo = item,
onClick = { onNavigate(item.appId) },
paneType = currentLayout,
onFocus = { targetOfScroll = item.index },
imageRefreshCounter = state.imageRefreshCounter,
compatibilityStatus = state.compatibilityMap[item.name],
gameStats = state.statsFor(item),
)
}
}
if (state.appInfoList.size < state.totalAppsInFilter) {
Expand Down Expand Up @@ -354,12 +361,17 @@ internal fun LibraryListPane(
),
) {
items(totalSkeletonCount) { index ->
if (index > 0 && currentLayout == PaneType.LIST) {
HorizontalDivider()
Column {
if (index > 0 && currentLayout == PaneType.LIST) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = horizontalPadding),
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.3f)
)
}
GameSkeletonLoader(
paneType = currentLayout,
)
}
GameSkeletonLoader(
paneType = currentLayout,
)
}
}
}
Expand Down
31 changes: 20 additions & 11 deletions app/src/main/java/app/gamenative/utils/CustomGameScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ object CustomGameScanner {

// 2) Try extracting from the selected container executable
try {
val cm = ContainerManager(context)
val cm = ContainerManager.getInstance(context)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Concurrent icon resolutions can both observe a null singleton and each rescan home/, defeating this change's per-item scan avoidance. Make ContainerManager.getInstance safely synchronized (or initialize it atomically) so all callers share one loaded manager.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At app/src/main/java/app/gamenative/utils/CustomGameScanner.kt, line 176:

<comment>Concurrent icon resolutions can both observe a null singleton and each rescan `home/`, defeating this change's per-item scan avoidance. Make `ContainerManager.getInstance` safely synchronized (or initialize it atomically) so all callers share one loaded manager.</comment>

<file context>
@@ -173,7 +173,7 @@ object CustomGameScanner {
         // 2) Try extracting from the selected container executable
         try {
-            val cm = ContainerManager(context)
+            val cm = ContainerManager.getInstance(context)
             if (cm.hasContainer(appId)) {
                 val container = cm.getContainerById(appId)
</file context>

if (cm.hasContainer(appId)) {
val container = cm.getContainerById(appId)
val relExe = container.executablePath
Expand Down Expand Up @@ -518,15 +518,19 @@ object CustomGameScanner {
if (manualFolders.isNotEmpty()) {
val existingAppIds = mutableSetOf<String>()
for (manualPath in manualFolders) {
// Filter by query if provided
if (q.isNotEmpty()) {
val folderName = File(manualPath).name
if (!folderName.contains(q, ignoreCase = true)) continue
}
try {
// Filter by query if provided
if (q.isNotEmpty()) {
val folderName = File(manualPath).name
if (!folderName.contains(q, ignoreCase = true)) continue
}

val manualItem = createLibraryItemFromFolder(manualPath)
if (manualItem != null && existingAppIds.add(manualItem.appId)) {
items.add(manualItem.copy(index = indexCounter++))
val manualItem = createLibraryItemFromFolder(manualPath)
if (manualItem != null && existingAppIds.add(manualItem.appId)) {
items.add(manualItem.copy(index = indexCounter++))
}
} catch (e: Exception) {
Timber.tag("CustomGameScanner").e(e, "Error scanning custom game folder: $manualPath")
}
}
}
Expand Down Expand Up @@ -565,8 +569,13 @@ object CustomGameScanner {

fun createLibraryItemFromFolder(folderPath: String): LibraryItem? {
val folder = File(folderPath)
if (!folder.exists() || !folder.isDirectory) {
Timber.tag("CustomGameScanner").w("Folder does not exist or is not a directory: $folderPath")
try {
if (!folder.exists() || !folder.isDirectory) {
Timber.tag("CustomGameScanner").w("Folder does not exist or is not a directory: $folderPath")
return null
}
} catch (e: Exception) {
Timber.tag("CustomGameScanner").e(e, "Error accessing folder: $folderPath")
return null
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ public class ContainerManager {
private final ArrayList<Container> containers = new ArrayList<>();
private final File homeDir;
private final Context context;
private static ContainerManager instance;

public static ContainerManager getInstance(Context context) {
if (instance == null) {
instance = new ContainerManager(context.getApplicationContext());
}
return instance;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

public ContainerManager(Context context) {
this.context = context;
Expand Down