Skip to content

Commit 9c0eca7

Browse files
committed
Improve home screen with smart sorting, ratings, and View All navigation
- Sort platform games by: installed, recently played, community score - Add View All card at end of platform rows to jump to Library - Display ratings below game info (community, user rating, difficulty) - Add platform logo caching with black background removal - Support deep-linking to Library with platform pre-selected
1 parent 323327f commit 9c0eca7

11 files changed

Lines changed: 453 additions & 70 deletions

File tree

app/src/main/kotlin/com/nendo/argosy/MainActivity.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class MainActivity : ComponentActivity() {
3434
enableEdgeToEdge()
3535
hideSystemUI()
3636
imageCacheManager.resumePendingCache()
37+
imageCacheManager.resumePendingLogoCache()
3738

3839
setContent {
3940
ALauncherTheme {

app/src/main/kotlin/com/nendo/argosy/data/cache/ImageCacheManager.kt

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package com.nendo.argosy.data.cache
33
import android.content.Context
44
import android.graphics.Bitmap
55
import android.graphics.BitmapFactory
6+
import android.graphics.Color
67
import android.util.Log
78
import com.nendo.argosy.data.local.dao.GameDao
9+
import com.nendo.argosy.data.local.dao.PlatformDao
810
import dagger.hilt.android.qualifiers.ApplicationContext
911
import kotlinx.coroutines.CoroutineScope
1012
import kotlinx.coroutines.Dispatchers
@@ -50,16 +52,24 @@ data class ScreenshotCacheRequest(
5052
val gameTitle: String = ""
5153
)
5254

55+
data class PlatformLogoCacheRequest(
56+
val platformId: String,
57+
val logoUrl: String
58+
)
59+
5360
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
5461
@Singleton
5562
class ImageCacheManager @Inject constructor(
5663
@ApplicationContext private val context: Context,
57-
private val gameDao: GameDao
64+
private val gameDao: GameDao,
65+
private val platformDao: PlatformDao
5866
) {
5967
private val cacheDir: File by lazy {
6068
File(context.cacheDir, "images").also { it.mkdirs() }
6169
}
6270

71+
private val logoQueue = Channel<PlatformLogoCacheRequest>(Channel.UNLIMITED)
72+
6373
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
6474
private val queue = Channel<ImageCacheRequest>(Channel.UNLIMITED)
6575
private val screenshotQueue = Channel<ScreenshotCacheRequest>(Channel.UNLIMITED)
@@ -309,4 +319,106 @@ class ImageCacheManager @Inject constructor(
309319
}
310320
}
311321
}
322+
323+
fun queuePlatformLogoCache(platformId: String, logoUrl: String) {
324+
scope.launch {
325+
logoQueue.send(PlatformLogoCacheRequest(platformId, logoUrl))
326+
startLogoProcessingIfNeeded()
327+
}
328+
}
329+
330+
private var isProcessingLogos = false
331+
332+
private fun startLogoProcessingIfNeeded() {
333+
if (isProcessingLogos) return
334+
isProcessingLogos = true
335+
336+
scope.launch {
337+
Log.d(TAG, "Starting platform logo cache processing")
338+
339+
for (request in logoQueue) {
340+
try {
341+
processLogoRequest(request)
342+
} catch (e: Exception) {
343+
Log.e(TAG, "Failed to process logo for ${request.platformId}: ${e.message}")
344+
}
345+
346+
if (logoQueue.isEmpty) break
347+
}
348+
isProcessingLogos = false
349+
}
350+
}
351+
352+
private suspend fun processLogoRequest(request: PlatformLogoCacheRequest) {
353+
val fileName = "logo_${request.platformId}_${request.logoUrl.md5Hash()}.png"
354+
val cachedFile = File(cacheDir, fileName)
355+
356+
if (cachedFile.exists()) {
357+
platformDao.updateLogoPath(request.platformId, cachedFile.absolutePath)
358+
return
359+
}
360+
361+
val bitmap = downloadBitmap(request.logoUrl) ?: return
362+
val transparentBitmap = removeBlackBackground(bitmap)
363+
bitmap.recycle()
364+
365+
FileOutputStream(cachedFile).use { out ->
366+
transparentBitmap.compress(Bitmap.CompressFormat.PNG, 100, out)
367+
}
368+
transparentBitmap.recycle()
369+
370+
Log.d(TAG, "Cached logo for platform ${request.platformId}: ${cachedFile.length() / 1024}KB")
371+
platformDao.updateLogoPath(request.platformId, cachedFile.absolutePath)
372+
}
373+
374+
private fun downloadBitmap(url: String): Bitmap? {
375+
return try {
376+
val connection = URL(url).openConnection()
377+
connection.connectTimeout = 10_000
378+
connection.readTimeout = 30_000
379+
connection.getInputStream().use { inputStream ->
380+
BitmapFactory.decodeStream(inputStream)
381+
}
382+
} catch (e: Exception) {
383+
Log.e(TAG, "Failed to download bitmap: ${e.message}")
384+
null
385+
}
386+
}
387+
388+
private fun removeBlackBackground(source: Bitmap): Bitmap {
389+
val width = source.width
390+
val height = source.height
391+
val result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
392+
393+
val pixels = IntArray(width * height)
394+
source.getPixels(pixels, 0, width, 0, 0, width, height)
395+
396+
for (i in pixels.indices) {
397+
val pixel = pixels[i]
398+
val r = Color.red(pixel)
399+
val g = Color.green(pixel)
400+
val b = Color.blue(pixel)
401+
402+
// Check if pixel is near-black (threshold of 30 for each channel)
403+
if (r < 30 && g < 30 && b < 30) {
404+
pixels[i] = Color.TRANSPARENT
405+
}
406+
}
407+
408+
result.setPixels(pixels, 0, width, 0, 0, width, height)
409+
return result
410+
}
411+
412+
fun resumePendingLogoCache() {
413+
scope.launch {
414+
val uncached = platformDao.getPlatformsWithRemoteLogos()
415+
if (uncached.isEmpty()) return@launch
416+
417+
Log.d(TAG, "Resuming cache for ${uncached.size} platforms with uncached logos")
418+
uncached.forEach { platform ->
419+
val url = platform.logoPath ?: return@forEach
420+
queuePlatformLogoCache(platform.id, url)
421+
}
422+
}
423+
}
312424
}

app/src/main/kotlin/com/nendo/argosy/data/local/dao/GameDao.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,20 @@ interface GameDao {
1616
@Query("SELECT * FROM games WHERE platformId = :platformId AND isHidden = 0 ORDER BY sortTitle ASC")
1717
fun observeByPlatform(platformId: String): Flow<List<GameEntity>>
1818

19+
@Query("""
20+
SELECT * FROM games
21+
WHERE platformId = :platformId AND isHidden = 0
22+
ORDER BY
23+
CASE WHEN localPath IS NOT NULL THEN 0 ELSE 1 END,
24+
CASE WHEN lastPlayed IS NULL THEN 1 ELSE 0 END,
25+
lastPlayed DESC,
26+
CASE WHEN rating IS NULL THEN 1 ELSE 0 END,
27+
rating DESC,
28+
sortTitle ASC
29+
LIMIT :limit
30+
""")
31+
fun observeByPlatformSorted(platformId: String, limit: Int = 20): Flow<List<GameEntity>>
32+
1933
@Query("SELECT * FROM games WHERE isHidden = 0 ORDER BY sortTitle ASC")
2034
fun observeAll(): Flow<List<GameEntity>>
2135

app/src/main/kotlin/com/nendo/argosy/data/local/dao/PlatformDao.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,10 @@ interface PlatformDao {
4040

4141
@Query("UPDATE platforms SET isVisible = :visible WHERE id = :platformId")
4242
suspend fun updateVisibility(platformId: String, visible: Boolean)
43+
44+
@Query("UPDATE platforms SET logoPath = :path WHERE id = :platformId")
45+
suspend fun updateLogoPath(platformId: String, path: String)
46+
47+
@Query("SELECT * FROM platforms WHERE logoPath LIKE 'http%'")
48+
suspend fun getPlatformsWithRemoteLogos(): List<PlatformEntity>
4349
}

app/src/main/kotlin/com/nendo/argosy/data/remote/romm/RomMRepository.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,14 +323,15 @@ class RomMRepository @Inject constructor(
323323
val existing = platformDao.getById(remote.slug)
324324
val platformDef = PlatformDefinitions.getById(remote.slug)
325325

326+
val logoUrl = remote.logoUrl?.let { buildMediaUrl(it) }
326327
val entity = PlatformEntity(
327328
id = remote.slug,
328329
name = platformDef?.name ?: remote.name,
329330
shortName = platformDef?.shortName ?: remote.name,
330331
romExtensions = platformDef?.extensions?.joinToString(",") ?: "",
331332
gameCount = remote.romCount,
332333
isVisible = existing?.isVisible ?: true,
333-
logoPath = existing?.logoPath,
334+
logoPath = logoUrl ?: existing?.logoPath,
334335
sortOrder = platformDef?.sortOrder ?: existing?.sortOrder ?: 0,
335336
lastScanned = existing?.lastScanned
336337
)
@@ -340,6 +341,11 @@ class RomMRepository @Inject constructor(
340341
} else {
341342
platformDao.update(entity)
342343
}
344+
345+
// Queue logo for caching with black background removal
346+
if (logoUrl != null && logoUrl.startsWith("http")) {
347+
imageCacheManager.queuePlatformLogoCache(remote.slug, logoUrl)
348+
}
343349
}
344350

345351
private suspend fun syncRom(rom: RomMRom, platformSlug: String): Pair<Boolean, GameEntity> {

app/src/main/kotlin/com/nendo/argosy/ui/navigation/NavGraph.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,24 @@ fun NavGraph(
4747
onGameSelect = { gameId ->
4848
navController.navigate(Screen.GameDetail.createRoute(gameId))
4949
},
50+
onNavigateToLibrary = { platformId ->
51+
navController.navigate(Screen.Library.createRoute(platformId))
52+
},
5053
onDrawerToggle = onDrawerToggle
5154
)
5255
}
5356

54-
composable(Screen.Library.route) {
57+
composable(
58+
route = Screen.Library.route,
59+
arguments = listOf(navArgument("platformId") {
60+
type = NavType.StringType
61+
nullable = true
62+
defaultValue = null
63+
})
64+
) { backStackEntry ->
65+
val platformId = backStackEntry.arguments?.getString("platformId")
5566
LibraryScreen(
67+
initialPlatformId = platformId,
5668
onGameSelect = { gameId ->
5769
navController.navigate(Screen.GameDetail.createRoute(gameId))
5870
},

app/src/main/kotlin/com/nendo/argosy/ui/navigation/Screen.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@ package com.nendo.argosy.ui.navigation
33
sealed class Screen(val route: String) {
44
data object FirstRun : Screen("first_run")
55
data object Home : Screen("home")
6-
data object Library : Screen("library")
6+
data object Library : Screen("library?platformId={platformId}") {
7+
fun createRoute(platformId: String? = null): String =
8+
if (platformId != null) "library?platformId=$platformId" else "library"
9+
}
710
data object Downloads : Screen("downloads")
811
data object Apps : Screen("apps")
912
data object Settings : Screen("settings")

0 commit comments

Comments
 (0)