2025-03-27 21:19:28 -04:00
|
|
|
package com.mintris.ui
|
|
|
|
|
|
|
|
import android.content.Context
|
|
|
|
import android.graphics.Color
|
|
|
|
import android.util.AttributeSet
|
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.View
|
|
|
|
import android.widget.FrameLayout
|
|
|
|
import android.widget.GridLayout
|
|
|
|
import android.widget.TextView
|
|
|
|
import androidx.cardview.widget.CardView
|
|
|
|
import com.mintris.R
|
|
|
|
import com.mintris.model.PlayerProgressionManager
|
2025-03-31 13:38:38 -04:00
|
|
|
import android.animation.ValueAnimator
|
|
|
|
import android.graphics.drawable.GradientDrawable
|
2025-03-31 20:17:40 -04:00
|
|
|
import android.util.Log
|
2025-03-27 21:19:28 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* UI component for selecting game themes
|
|
|
|
*/
|
|
|
|
class ThemeSelector @JvmOverloads constructor(
|
|
|
|
context: Context,
|
|
|
|
attrs: AttributeSet? = null,
|
|
|
|
defStyleAttr: Int = 0
|
|
|
|
) : FrameLayout(context, attrs, defStyleAttr) {
|
|
|
|
|
|
|
|
private val themesGrid: GridLayout
|
|
|
|
private val availableThemesLabel: TextView
|
|
|
|
|
|
|
|
// Callback when a theme is selected
|
|
|
|
var onThemeSelected: ((String) -> Unit)? = null
|
|
|
|
|
2025-03-31 13:38:38 -04:00
|
|
|
// Currently selected theme (persisted)
|
2025-03-27 21:19:28 -04:00
|
|
|
private var selectedTheme: String = PlayerProgressionManager.THEME_CLASSIC
|
|
|
|
|
2025-03-31 13:38:38 -04:00
|
|
|
// Theme cards map (themeId -> CardView)
|
2025-03-27 21:19:28 -04:00
|
|
|
private val themeCards = mutableMapOf<String, CardView>()
|
2025-03-31 13:38:38 -04:00
|
|
|
// Ordered list of theme IDs for navigation
|
|
|
|
private val themeIdList = mutableListOf<String>()
|
|
|
|
// Currently focused theme ID (for gamepad navigation within the selector)
|
|
|
|
private var focusedThemeId: String? = null
|
|
|
|
// Index of the currently focused theme in themeIdList
|
|
|
|
private var focusedIndex: Int = -1
|
|
|
|
// Flag indicating if the entire selector component has focus from the main menu
|
|
|
|
private var hasComponentFocus: Boolean = false
|
2025-03-27 21:19:28 -04:00
|
|
|
|
|
|
|
init {
|
|
|
|
// Inflate the layout
|
|
|
|
LayoutInflater.from(context).inflate(R.layout.theme_selector, this, true)
|
|
|
|
|
|
|
|
// Get references to views
|
|
|
|
themesGrid = findViewById(R.id.themes_grid)
|
|
|
|
availableThemesLabel = findViewById(R.id.available_themes_label)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the theme selector with unlocked themes
|
|
|
|
*/
|
|
|
|
fun updateThemes(unlockedThemes: Set<String>, currentTheme: String) {
|
2025-03-31 13:38:38 -04:00
|
|
|
// Clear existing theme cards and ID list
|
2025-03-27 21:19:28 -04:00
|
|
|
themesGrid.removeAllViews()
|
|
|
|
themeCards.clear()
|
2025-03-31 13:38:38 -04:00
|
|
|
themeIdList.clear()
|
2025-03-27 21:19:28 -04:00
|
|
|
|
|
|
|
// Update selected theme
|
|
|
|
selectedTheme = currentTheme
|
2025-03-31 13:38:38 -04:00
|
|
|
focusedThemeId = currentTheme // Initially focus the selected theme
|
|
|
|
focusedIndex = -1 // Reset index
|
2025-03-27 21:19:28 -04:00
|
|
|
|
2025-03-31 13:38:38 -04:00
|
|
|
// Get all possible themes and their details, sorted for consistent order
|
|
|
|
val allThemes = getThemes().entries.sortedWith(compareBy({ it.value.unlockLevel }, { it.value.displayName })).associate { it.key to it.value }
|
2025-03-27 21:19:28 -04:00
|
|
|
|
2025-03-31 13:38:38 -04:00
|
|
|
// Add theme cards to the grid and build the ID list
|
2025-03-27 21:19:28 -04:00
|
|
|
allThemes.forEach { (themeId, themeInfo) ->
|
|
|
|
val isUnlocked = unlockedThemes.contains(themeId)
|
|
|
|
val isSelected = themeId == selectedTheme
|
|
|
|
|
2025-03-31 13:38:38 -04:00
|
|
|
// Only add unlocked themes to the navigable list
|
|
|
|
if (isUnlocked) {
|
|
|
|
themeIdList.add(themeId)
|
|
|
|
}
|
|
|
|
|
2025-03-27 21:19:28 -04:00
|
|
|
val themeCard = createThemeCard(themeId, themeInfo, isUnlocked, isSelected)
|
|
|
|
themeCards[themeId] = themeCard
|
|
|
|
themesGrid.addView(themeCard)
|
2025-03-31 13:38:38 -04:00
|
|
|
|
|
|
|
// Update focused index if this is the currently selected/focused theme
|
|
|
|
if (isUnlocked && themeId == focusedThemeId) {
|
|
|
|
focusedIndex = themeIdList.indexOf(themeId)
|
|
|
|
}
|
2025-03-27 21:19:28 -04:00
|
|
|
}
|
2025-03-31 13:38:38 -04:00
|
|
|
|
|
|
|
// Ensure focus index is valid if the previously focused theme is no longer available/unlocked
|
|
|
|
if (focusedIndex == -1 && themeIdList.isNotEmpty()) {
|
|
|
|
focusedIndex = 0
|
|
|
|
focusedThemeId = themeIdList[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply initial focus highlight if the component has focus
|
|
|
|
highlightFocusedCard()
|
2025-03-27 21:19:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a card for a theme
|
|
|
|
*/
|
|
|
|
private fun createThemeCard(
|
|
|
|
themeId: String,
|
|
|
|
themeInfo: ThemeInfo,
|
|
|
|
isUnlocked: Boolean,
|
|
|
|
isSelected: Boolean
|
|
|
|
): CardView {
|
|
|
|
// Create the card
|
|
|
|
val card = CardView(context).apply {
|
|
|
|
id = View.generateViewId()
|
|
|
|
radius = 12f
|
|
|
|
cardElevation = if (isSelected) 8f else 2f
|
|
|
|
useCompatPadding = true
|
|
|
|
|
|
|
|
// Set card background color based on theme
|
|
|
|
setCardBackgroundColor(themeInfo.primaryColor)
|
|
|
|
|
|
|
|
// Set card dimensions
|
|
|
|
val cardSize = resources.getDimensionPixelSize(R.dimen.theme_card_size)
|
|
|
|
layoutParams = GridLayout.LayoutParams().apply {
|
|
|
|
width = cardSize
|
|
|
|
height = cardSize
|
|
|
|
columnSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
|
|
|
|
rowSpec = GridLayout.spec(GridLayout.UNDEFINED, 1f)
|
|
|
|
setMargins(8, 8, 8, 8)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply locked/selected state visuals
|
|
|
|
alpha = if (isUnlocked) 1.0f else 0.5f
|
|
|
|
}
|
|
|
|
|
2025-03-27 22:16:37 -04:00
|
|
|
// Create theme content container
|
|
|
|
val container = FrameLayout(context).apply {
|
|
|
|
layoutParams = FrameLayout.LayoutParams(
|
|
|
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
|
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2025-03-27 21:19:28 -04:00
|
|
|
// Create the theme preview
|
|
|
|
val themePreview = View(context).apply {
|
|
|
|
layoutParams = FrameLayout.LayoutParams(
|
|
|
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
|
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
|
|
)
|
|
|
|
|
|
|
|
// Set the background color
|
|
|
|
setBackgroundColor(themeInfo.primaryColor)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add a label with the theme name
|
|
|
|
val themeLabel = TextView(context).apply {
|
|
|
|
text = themeInfo.displayName
|
|
|
|
setTextColor(themeInfo.textColor)
|
|
|
|
textSize = 14f
|
|
|
|
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
|
|
|
|
|
|
|
// Position at the bottom of the card
|
|
|
|
layoutParams = FrameLayout.LayoutParams(
|
|
|
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
|
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
|
|
).apply {
|
|
|
|
gravity = android.view.Gravity.BOTTOM or android.view.Gravity.CENTER_HORIZONTAL
|
|
|
|
setMargins(4, 4, 4, 8)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-27 22:16:37 -04:00
|
|
|
// Add level requirement for locked themes
|
|
|
|
val levelRequirement = TextView(context).apply {
|
|
|
|
text = "Level ${themeInfo.unlockLevel}"
|
|
|
|
setTextColor(Color.WHITE)
|
|
|
|
textSize = 12f
|
|
|
|
textAlignment = View.TEXT_ALIGNMENT_CENTER
|
|
|
|
visibility = if (isUnlocked) View.GONE else View.VISIBLE
|
|
|
|
|
|
|
|
// Position at the center of the card
|
|
|
|
layoutParams = FrameLayout.LayoutParams(
|
|
|
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
|
|
|
FrameLayout.LayoutParams.WRAP_CONTENT
|
|
|
|
).apply {
|
|
|
|
gravity = android.view.Gravity.CENTER
|
|
|
|
}
|
2025-03-27 22:28:47 -04:00
|
|
|
// Make text bold and more visible for better readability
|
|
|
|
typeface = android.graphics.Typeface.DEFAULT_BOLD
|
|
|
|
setShadowLayer(3f, 1f, 1f, Color.BLACK)
|
2025-03-27 22:16:37 -04:00
|
|
|
}
|
|
|
|
|
2025-03-27 21:19:28 -04:00
|
|
|
// Add a lock icon if the theme is locked
|
|
|
|
val lockOverlay = View(context).apply {
|
|
|
|
layoutParams = FrameLayout.LayoutParams(
|
|
|
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
|
|
|
FrameLayout.LayoutParams.MATCH_PARENT
|
|
|
|
)
|
|
|
|
|
|
|
|
// Add lock icon or visual indicator
|
|
|
|
setBackgroundResource(R.drawable.lock_overlay)
|
|
|
|
visibility = if (isUnlocked) View.GONE else View.VISIBLE
|
|
|
|
}
|
|
|
|
|
2025-03-27 22:16:37 -04:00
|
|
|
// Add all elements to container
|
|
|
|
container.addView(themePreview)
|
|
|
|
container.addView(themeLabel)
|
|
|
|
container.addView(lockOverlay)
|
|
|
|
container.addView(levelRequirement)
|
|
|
|
|
|
|
|
// Add container to card
|
|
|
|
card.addView(container)
|
2025-03-27 21:19:28 -04:00
|
|
|
|
|
|
|
// Set up click listener only for unlocked themes
|
|
|
|
if (isUnlocked) {
|
|
|
|
card.setOnClickListener {
|
2025-03-31 13:38:38 -04:00
|
|
|
// Clicking directly selects the theme
|
2025-03-31 20:17:40 -04:00
|
|
|
Log.d("ThemeSelector", "Theme card clicked: $themeId (isUnlocked=$isUnlocked)")
|
2025-03-31 13:38:38 -04:00
|
|
|
focusedThemeId = themeId
|
|
|
|
focusedIndex = themeIdList.indexOf(themeId)
|
|
|
|
confirmSelection() // Directly confirm click selection
|
2025-03-27 21:19:28 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return card
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Data class for theme information
|
|
|
|
*/
|
|
|
|
data class ThemeInfo(
|
|
|
|
val displayName: String,
|
|
|
|
val primaryColor: Int,
|
|
|
|
val secondaryColor: Int,
|
|
|
|
val textColor: Int,
|
|
|
|
val unlockLevel: Int
|
|
|
|
)
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get all available themes with their details
|
|
|
|
*/
|
|
|
|
private fun getThemes(): Map<String, ThemeInfo> {
|
|
|
|
return mapOf(
|
|
|
|
PlayerProgressionManager.THEME_CLASSIC to ThemeInfo(
|
|
|
|
displayName = "Classic",
|
|
|
|
primaryColor = Color.parseColor("#000000"),
|
|
|
|
secondaryColor = Color.parseColor("#1F1F1F"),
|
|
|
|
textColor = Color.WHITE,
|
|
|
|
unlockLevel = 1
|
|
|
|
),
|
|
|
|
PlayerProgressionManager.THEME_NEON to ThemeInfo(
|
|
|
|
displayName = "Neon",
|
|
|
|
primaryColor = Color.parseColor("#0D0221"),
|
|
|
|
secondaryColor = Color.parseColor("#650D89"),
|
|
|
|
textColor = Color.parseColor("#FF00FF"),
|
|
|
|
unlockLevel = 5
|
|
|
|
),
|
|
|
|
PlayerProgressionManager.THEME_MONOCHROME to ThemeInfo(
|
|
|
|
displayName = "Monochrome",
|
|
|
|
primaryColor = Color.parseColor("#1A1A1A"),
|
|
|
|
secondaryColor = Color.parseColor("#333333"),
|
|
|
|
textColor = Color.LTGRAY,
|
|
|
|
unlockLevel = 10
|
|
|
|
),
|
|
|
|
PlayerProgressionManager.THEME_RETRO to ThemeInfo(
|
|
|
|
displayName = "Retro",
|
|
|
|
primaryColor = Color.parseColor("#3F2832"),
|
|
|
|
secondaryColor = Color.parseColor("#087E8B"),
|
|
|
|
textColor = Color.parseColor("#FF5A5F"),
|
|
|
|
unlockLevel = 15
|
|
|
|
),
|
|
|
|
PlayerProgressionManager.THEME_MINIMALIST to ThemeInfo(
|
|
|
|
displayName = "Minimalist",
|
|
|
|
primaryColor = Color.parseColor("#FFFFFF"),
|
|
|
|
secondaryColor = Color.parseColor("#F0F0F0"),
|
|
|
|
textColor = Color.BLACK,
|
|
|
|
unlockLevel = 20
|
|
|
|
),
|
|
|
|
PlayerProgressionManager.THEME_GALAXY to ThemeInfo(
|
|
|
|
displayName = "Galaxy",
|
|
|
|
primaryColor = Color.parseColor("#0B0C10"),
|
|
|
|
secondaryColor = Color.parseColor("#1F2833"),
|
|
|
|
textColor = Color.parseColor("#66FCF1"),
|
|
|
|
unlockLevel = 25
|
|
|
|
)
|
|
|
|
)
|
|
|
|
}
|
2025-03-31 13:38:38 -04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets whether the entire component has focus (from the parent menu).
|
|
|
|
* Controls the outer border visibility.
|
|
|
|
*/
|
|
|
|
fun setHasFocus(hasFocus: Boolean) {
|
|
|
|
hasComponentFocus = hasFocus
|
|
|
|
// Update visual state based on component focus
|
|
|
|
highlightFocusedCard() // Re-apply highlights
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Moves the internal focus to the next available theme.
|
|
|
|
*/
|
|
|
|
fun focusNextItem() {
|
|
|
|
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
|
|
|
|
|
|
|
focusedIndex = (focusedIndex + 1) % themeIdList.size
|
|
|
|
focusedThemeId = themeIdList[focusedIndex]
|
|
|
|
highlightFocusedCard()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Moves the internal focus to the previous available theme.
|
|
|
|
*/
|
|
|
|
fun focusPreviousItem() {
|
|
|
|
if (!hasComponentFocus || themeIdList.isEmpty()) return // Only navigate if component has focus
|
|
|
|
|
|
|
|
focusedIndex = (focusedIndex - 1 + themeIdList.size) % themeIdList.size
|
|
|
|
focusedThemeId = themeIdList[focusedIndex]
|
|
|
|
highlightFocusedCard()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Confirms the currently focused theme as the selected theme.
|
|
|
|
* Triggers the onThemeSelected callback.
|
|
|
|
*/
|
|
|
|
fun confirmSelection() {
|
2025-03-31 20:17:40 -04:00
|
|
|
Log.d("ThemeSelector", "confirmSelection called. Focused theme: $focusedThemeId")
|
|
|
|
if (focusedThemeId == null || focusedThemeId == selectedTheme) {
|
|
|
|
// No change needed if nothing is focused,
|
2025-03-31 13:38:38 -04:00
|
|
|
// or the focused item is already selected
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update the selected theme
|
|
|
|
selectedTheme = focusedThemeId!!
|
|
|
|
|
|
|
|
// Update visual states for all cards
|
|
|
|
highlightFocusedCard() // This will now mark the new theme as selected
|
|
|
|
|
|
|
|
// Trigger the callback
|
|
|
|
onThemeSelected?.invoke(selectedTheme)
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Updates the visual highlight state of the theme cards based on
|
|
|
|
* selection and internal focus.
|
|
|
|
*/
|
|
|
|
private fun highlightFocusedCard() {
|
|
|
|
if (themeCards.isEmpty()) return
|
|
|
|
|
|
|
|
val focusColor = Color.YELLOW // Color for the focused-but-not-selected item
|
|
|
|
val selectedColor = Color.WHITE // Color for the selected item (might be focused or not)
|
|
|
|
|
|
|
|
themeCards.forEach { (themeId, card) ->
|
|
|
|
val themeInfo = getThemes()[themeId] ?: return@forEach
|
|
|
|
val isUnlocked = themeIdList.contains(themeId) // Check if it's in the navigable list
|
|
|
|
|
|
|
|
if (!isUnlocked) {
|
|
|
|
// Keep locked themes visually distinct
|
|
|
|
card.alpha = 0.5f
|
|
|
|
card.cardElevation = 2f
|
|
|
|
card.background = null // Remove any border/background
|
|
|
|
card.setCardBackgroundColor(themeInfo.primaryColor)
|
|
|
|
return@forEach // Skip further styling for locked themes
|
|
|
|
}
|
|
|
|
|
|
|
|
// Reset unlocked cards first
|
|
|
|
card.alpha = 1.0f
|
|
|
|
card.cardElevation = 4f // Default elevation for unlocked cards
|
|
|
|
card.background = null
|
|
|
|
card.setCardBackgroundColor(themeInfo.primaryColor)
|
|
|
|
card.scaleX = 1.0f
|
|
|
|
card.scaleY = 1.0f
|
|
|
|
|
|
|
|
val isSelected = (themeId == selectedTheme)
|
|
|
|
val isFocused = (hasComponentFocus && themeId == focusedThemeId)
|
|
|
|
|
|
|
|
var borderColor = Color.TRANSPARENT
|
|
|
|
var borderWidth = 0
|
|
|
|
var elevation = 4f
|
|
|
|
var scale = 1.0f
|
|
|
|
|
|
|
|
if (isSelected) {
|
|
|
|
borderColor = selectedColor
|
|
|
|
borderWidth = 6 // Thick border for selected
|
|
|
|
elevation = 12f // Higher elevation for selected
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isFocused) {
|
|
|
|
// Focused item gets a distinct border (unless it's also selected)
|
|
|
|
if (!isSelected) {
|
|
|
|
borderColor = focusColor
|
|
|
|
borderWidth = 4 // Slightly thinner border for focused
|
|
|
|
}
|
|
|
|
elevation = 12f // Use high elevation for focus too
|
|
|
|
scale = 1.1f // Scale up the focused item
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply scale
|
|
|
|
card.scaleX = scale
|
|
|
|
card.scaleY = scale
|
|
|
|
|
|
|
|
// Apply border and elevation
|
|
|
|
if (borderWidth > 0) {
|
|
|
|
val gradientDrawable = GradientDrawable().apply {
|
|
|
|
setColor(themeInfo.primaryColor)
|
|
|
|
setStroke(borderWidth, borderColor)
|
|
|
|
cornerRadius = 12f // Keep consistent corner radius
|
|
|
|
}
|
|
|
|
card.background = gradientDrawable
|
|
|
|
} else {
|
|
|
|
card.background = null // Ensure no border if not selected/focused
|
|
|
|
}
|
|
|
|
card.cardElevation = elevation
|
|
|
|
}
|
|
|
|
}
|
2025-03-27 21:19:28 -04:00
|
|
|
}
|