mirror of
https://github.com/ARMSX2/ARMSX2.git
synced 2026-08-24 16:50:16 -07:00
Second screen: cover art and more of what RetroAchievements knows
Cover art tile. The panel had the game as a line of text, and on a screen sitting beside you the cover is what makes it read as "this game" at a glance. It resolves through the same two sources the library uses -- a user-set custom cover first, then the fetched URL -- so the panel shows what the library shows, including a cover picked by hand. The only tile that is a picture rather than text, and it re-resolves on the tick keyed on the game, so switching game changes it without rebuilding the panel. Three more RetroAchievements read-outs: points earned against the total, which is the figure RA itself leads with; the latest unlock on its own; and the rich presence line, which is the one thing here that says something a number cannot. Two things fixed while adding them. The unlock tracking lived inside the Achievements tile's own text builder, so the new Latest-unlock tile would have read "—" forever unless the Achievements tile happened to be placed as well -- it is now a per-tick step that runs regardless of which tiles exist. And the achievements JSON was parsed per tile, so placing all three would have parsed the same string three times a tick for identical results; it is parsed once now and shared.
This commit is contained in:
@@ -526,6 +526,7 @@ object SecondScreen {
|
||||
) return null
|
||||
return macroAction(id).styleAsTile(action = true)
|
||||
}
|
||||
if (tile == SecondScreenTile.COVER) return buildCoverTile()
|
||||
if (tile.stat) {
|
||||
return TextView(context).styleAsTile(action = false).also {
|
||||
(it as TextView).text = I18n.get(tile.labelKey)
|
||||
@@ -546,6 +547,65 @@ object SecondScreen {
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* The cover art tile: the only tile that is a picture rather than text.
|
||||
*
|
||||
* Loading goes through the same two sources the library uses -- a user-set custom cover
|
||||
* file first, then the fetched cover URL -- so the panel shows whatever the library
|
||||
* shows, including a per-game cover the user chose by hand. The URL path uses Coil's
|
||||
* ImageLoader directly because a Presentation is plain Views; the file path decodes
|
||||
* inline, since it is local and already on disk.
|
||||
*
|
||||
* Re-resolved on the panel tick rather than once at build, so it follows a game change
|
||||
* without the panel being rebuilt.
|
||||
*/
|
||||
private fun buildCoverTile(): View =
|
||||
android.widget.ImageView(context).apply {
|
||||
scaleType = android.widget.ImageView.ScaleType.CENTER_CROP
|
||||
adjustViewBounds = true
|
||||
background = android.graphics.drawable.GradientDrawable().apply {
|
||||
cornerRadius = dp * 14f
|
||||
setColor(TILE_STAT)
|
||||
setStroke((dp * 1f).toInt(), BORDER)
|
||||
}
|
||||
clipToOutline = true
|
||||
outlineProvider = object : android.view.ViewOutlineProvider() {
|
||||
override fun getOutline(v: View, o: android.graphics.Outline) {
|
||||
o.setRoundRect(0, 0, v.width, v.height, dp * 14f)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** The cover currently shown, so the tick only reloads when the game actually changes. */
|
||||
private var coverKey: String? = null
|
||||
|
||||
private fun updateCover(view: android.widget.ImageView) {
|
||||
val game = MainActivityRuntime.currentGame.value
|
||||
val key = game?.serial ?: game?.title
|
||||
if (key == coverKey) return
|
||||
coverKey = key
|
||||
if (game == null) { view.setImageDrawable(null); return }
|
||||
val custom = runCatching { com.armsx2.CustomCovers.fileFor(context, game) }.getOrNull()
|
||||
if (custom != null) {
|
||||
runCatching {
|
||||
view.setImageBitmap(android.graphics.BitmapFactory.decodeFile(custom.absolutePath))
|
||||
}
|
||||
return
|
||||
}
|
||||
val url = game.coverUrl ?: run { view.setImageDrawable(null); return }
|
||||
runCatching {
|
||||
val loader = coil.ImageLoader(context)
|
||||
val req = coil.request.ImageRequest.Builder(context)
|
||||
.data(url)
|
||||
.target(
|
||||
onSuccess = { d -> view.setImageDrawable(d) },
|
||||
onError = { _ -> view.setImageDrawable(null) },
|
||||
)
|
||||
.build()
|
||||
loader.enqueue(req)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A tile's face: the glyph on its own line, larger and in the accent, over the label.
|
||||
*
|
||||
@@ -732,6 +792,7 @@ object SecondScreen {
|
||||
// panel tick — that interval IS the mitigation Cotcho asked about. Cheap to call:
|
||||
// Thermals.poll returns immediately until the interval is up.
|
||||
runCatching { Thermals.poll(context, tempIntervalMs()) }
|
||||
runCatching { trackAchievements() }
|
||||
|
||||
// Read charge straight from BatteryManager rather than plumbing state over from the
|
||||
// main-display status cluster — this panel ticks on its own and the call is cheap.
|
||||
@@ -792,6 +853,19 @@ object SecondScreen {
|
||||
SecondScreenTile.GPU_TEMP -> "GPU\n" + (Thermals.format(Thermals.gpu) ?: "—")
|
||||
SecondScreenTile.BATTERY_TEMP -> "BATT\n" + (Thermals.format(Thermals.battery) ?: "—")
|
||||
SecondScreenTile.ACHIEVEMENTS -> achievementSummary()
|
||||
// The picture tile updates itself; the when only produces text.
|
||||
SecondScreenTile.COVER -> {
|
||||
(view as? android.widget.ImageView)?.let { updateCover(it) }
|
||||
null
|
||||
}
|
||||
SecondScreenTile.RA_POINTS -> raPoints()
|
||||
SecondScreenTile.RA_RECENT ->
|
||||
I18n.get(tile.labelKey) + "\n" + (lastUnlock ?: "—")
|
||||
// RetroAchievements' own description of where you are in the game. It is the
|
||||
// one line that says something a number cannot.
|
||||
SecondScreenTile.RICH_PRESENCE ->
|
||||
runCatching { NativeApp.getRichPresence() }.getOrDefault("").ifBlank { null }
|
||||
?: (I18n.get(tile.labelKey) + "\n—")
|
||||
// Action tiles that carry state show it, so the panel reads as a status
|
||||
// display and not just a remote control.
|
||||
// State is carried by the GLYPH, not by an extra line. Appending one was
|
||||
@@ -831,20 +905,46 @@ object SecondScreen {
|
||||
* recently THIS SESSION. RetroAchievements' own snapshot carries no unlock timestamp, so
|
||||
* "recent" is tracked by watching the locked→unlocked edge on the panel's own tick rather
|
||||
* than invented from list order. */
|
||||
private fun achievementSummary(): String {
|
||||
/**
|
||||
* This tick's achievements, parsed ONCE.
|
||||
*
|
||||
* Three tiles read this now, and each used to parse the JSON for itself -- so placing all
|
||||
* three meant three parses of the same string every tick, for identical results.
|
||||
*/
|
||||
private var raItems: List<com.armsx2.ui.achievements.AchievementItem> = emptyList()
|
||||
|
||||
/**
|
||||
* Refresh [raItems] and note any new unlock.
|
||||
*
|
||||
* Called once per tick regardless of which tiles are placed. It used to live inside the
|
||||
* Achievements tile's own text builder, which meant the Latest-unlock tile read "—"
|
||||
* forever unless the Achievements tile happened to be on the panel as well.
|
||||
*/
|
||||
private fun trackAchievements() {
|
||||
val json = runCatching { NativeApp.getAchievementsJSON() }.getOrDefault("")
|
||||
val items = runCatching { com.armsx2.ui.achievements.parseAchievementItems(json) }
|
||||
raItems = runCatching { com.armsx2.ui.achievements.parseAchievementItems(json) }
|
||||
.getOrDefault(emptyList())
|
||||
if (items.isEmpty()) return I18n.get("secondScreen.tile.achievements") + "\n—"
|
||||
val unlocked = items.filter { it.unlocked }
|
||||
unlocked.map { it.id }.toSet().let { ids ->
|
||||
val fresh = ids - seenUnlocked
|
||||
if (seenUnlocked.isNotEmpty() && fresh.isNotEmpty())
|
||||
lastUnlock = unlocked.firstOrNull { it.id in fresh }?.title
|
||||
seenUnlocked = ids
|
||||
}
|
||||
if (raItems.isEmpty()) return
|
||||
val ids = raItems.filter { it.unlocked }.map { it.id }.toSet()
|
||||
val fresh = ids - seenUnlocked
|
||||
// The first poll of a session seeds the set without announcing: everything already
|
||||
// unlocked is not news.
|
||||
if (seenUnlocked.isNotEmpty() && fresh.isNotEmpty())
|
||||
lastUnlock = raItems.firstOrNull { it.id in fresh }?.title
|
||||
seenUnlocked = ids
|
||||
}
|
||||
|
||||
/** Earned / total points, which is the figure RA itself leads with. */
|
||||
private fun raPoints(): String {
|
||||
if (raItems.isEmpty()) return I18n.get("secondScreen.tile.raPoints") + "\n—"
|
||||
val earned = raItems.filter { it.unlocked }.sumOf { it.points }
|
||||
return "RA\n$earned/${raItems.sumOf { it.points }}"
|
||||
}
|
||||
|
||||
private fun achievementSummary(): String {
|
||||
if (raItems.isEmpty()) return I18n.get("secondScreen.tile.achievements") + "\n—"
|
||||
return buildString {
|
||||
append(unlocked.size).append('/').append(items.size)
|
||||
append(raItems.count { it.unlocked }).append('/').append(raItems.size)
|
||||
lastUnlock?.let { append('\n').append(it) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,14 @@ enum class SecondScreenTile(val id: String, val labelKey: String, val stat: Bool
|
||||
// Thermals (Cotcho, Mike22). Stat tiles like the rest -- a device with no readable zone
|
||||
// simply shows a dash rather than the tile being hidden, so the grid does not reflow
|
||||
// depending on what the kernel happens to expose.
|
||||
// The game's cover, and more of what RetroAchievements already knows. The panel had the
|
||||
// title as text and a bare unlocked-count; on a screen sitting beside you, the cover is what
|
||||
// makes it read as "this game" at a glance.
|
||||
COVER("cover", "secondScreen.tile.cover", stat = true),
|
||||
RA_POINTS("rapoints", "secondScreen.tile.raPoints", stat = true),
|
||||
RA_RECENT("rarecent", "secondScreen.tile.raRecent", stat = true),
|
||||
RICH_PRESENCE("presence", "secondScreen.tile.presence", stat = true),
|
||||
|
||||
// The rest of what the in-game OSD shows (Mike22). Backed by new JNI getters -- until those
|
||||
// existed, FPS was the only figure the panel could reach.
|
||||
VPS("vps", "secondScreen.tile.vps", stat = true),
|
||||
|
||||
@@ -1703,6 +1703,10 @@ private val BASE_EN: Map<String, String> = mapOf(
|
||||
"touch.editor.scopeGlobal" to "Editing Global Default touch layout",
|
||||
"touch.editor.show" to "Show",
|
||||
"touch.editor.tapHoldOff" to "Tap-Hold Off",
|
||||
"secondScreen.tile.cover" to "Cover",
|
||||
"secondScreen.tile.raPoints" to "RA points",
|
||||
"secondScreen.tile.raRecent" to "Latest unlock",
|
||||
"secondScreen.tile.presence" to "Now playing",
|
||||
"secondScreen.tile.vps" to "VPS",
|
||||
"secondScreen.tile.cpuLoad" to "EE load",
|
||||
"secondScreen.tile.gsLoad" to "GS load",
|
||||
|
||||
Reference in New Issue
Block a user