Browse Source

feat: implement history list UI with expandable Pokemon cards (PGH-17)

- Created HistoryFragment with RecyclerView architecture for performance
- Implemented HistoryAdapter with expandable/collapsible Pokemon detection cards
- Added navigation integration between main app and history screen
- Connected to StorageInterface for detection result data retrieval
- Fixed MainActivity to extend FragmentActivity for fragment compatibility
- Added ServiceLocator synchronous storage access method
- Implemented delete functionality with proper state management
- Added empty state handling with informative messaging
- Enhanced build.gradle with navigation-compose dependency

Features:
- Scrollable detection history with smooth expand/collapse animations
- Compact view: Pokemon name, timestamp, processing time, status icons
- Expanded view: Complete Pokemon data organized in sections
- Delete buttons for result management
- Performance optimized for large lists with view recycling
- Proper error handling and lifecycle management

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
feature/pgh-1-results-display-history
Quildra 5 months ago
parent
commit
3c1d730f3d
  1. 9
      .claude/settings.local.json
  2. 1
      app/build.gradle
  3. 109
      app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt
  4. 15
      app/src/main/java/com/quillstudios/pokegoalshelper/di/ServiceLocator.kt
  5. 443
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryAdapter.kt
  6. 193
      app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryFragment.kt

9
.claude/settings.local.json

@ -12,7 +12,14 @@
"mcp__atlassian__createJiraIssue",
"mcp__atlassian__getConfluencePage",
"mcp__atlassian__getPagesInConfluenceSpace",
"mcp__atlassian__getJiraIssue"
"mcp__atlassian__getJiraIssue",
"Bash(JAVA_HOME=\"C:\\Program Files\\Android\\Android Studio\\jbr\" ./gradlew compileDebugKotlin)",
"Bash(JAVA_HOME=\"C:\\Program Files\\Android\\Android Studio\\jbr\" ./gradlew assembleDebug -x lint)",
"mcp__atlassian__searchJiraIssuesUsingJql",
"mcp__atlassian__editJiraIssue",
"mcp__atlassian__getTransitionsForJiraIssue",
"mcp__atlassian__transitionJiraIssue",
"mcp__atlassian__addCommentToJiraIssue"
],
"deny": []
}

1
app/build.gradle

@ -56,6 +56,7 @@ dependencies {
implementation libs.androidx.ui.graphics
implementation libs.androidx.ui.tooling.preview
implementation libs.androidx.material3
implementation 'androidx.navigation:navigation-compose:2.7.6'
testImplementation libs.junit
androidTestImplementation libs.androidx.junit
androidTestImplementation libs.androidx.espresso.core

109
app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt

@ -14,7 +14,7 @@ import android.util.Log
import com.quillstudios.pokegoalshelper.utils.PGHLog
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.activity.ComponentActivity
import androidx.fragment.app.FragmentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
@ -25,12 +25,19 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.navigation.compose.NavHost
import android.view.View
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme
import com.quillstudios.pokegoalshelper.ui.HistoryFragment
import com.quillstudios.pokegoalshelper.di.ServiceLocator
import org.opencv.android.OpenCVLoader
import org.opencv.core.Mat
import org.opencv.core.CvType
class MainActivity : ComponentActivity() {
class MainActivity : FragmentActivity() {
companion object {
private const val TAG = "MainActivity"
@ -157,15 +164,33 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
// Initialize ServiceLocator
ServiceLocator.initialize(applicationContext)
setContent {
PokeGoalsHelperTheme {
val navController = rememberNavController()
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
ScreenCaptureUI(
isCapturing = isCapturing,
onStartCapture = { requestScreenCapturePermission() },
onStopCapture = { stopScreenCaptureService() },
NavHost(
navController = navController,
startDestination = "main",
modifier = Modifier.padding(innerPadding)
)
) {
composable("main") {
ScreenCaptureUI(
isCapturing = isCapturing,
onStartCapture = { requestScreenCapturePermission() },
onStopCapture = { stopScreenCaptureService() },
onNavigateToHistory = { navController.navigate("history") }
)
}
composable("history") {
HistoryUI(
onNavigateBack = { navController.popBackStack() }
)
}
}
}
}
}
@ -182,6 +207,7 @@ fun ScreenCaptureUI(
isCapturing: Boolean,
onStartCapture: () -> Unit,
onStopCapture: () -> Unit,
onNavigateToHistory: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
@ -241,6 +267,18 @@ fun ScreenCaptureUI(
style = MaterialTheme.typography.bodySmall
)
}
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onNavigateToHistory,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary
),
modifier = Modifier.fillMaxWidth()
) {
Text("View Detection History")
}
}
}
@ -254,6 +292,60 @@ fun ScreenCaptureUI(
}
}
@Composable
fun HistoryUI(
onNavigateBack: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier.fillMaxSize()
) {
// Header with back button
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Detection History",
style = MaterialTheme.typography.headlineMedium
)
Button(
onClick = onNavigateBack,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text("← Back")
}
}
// HistoryFragment embedded in Compose
AndroidView(
modifier = Modifier.fillMaxSize(),
factory = { context ->
val fragmentManager = (context as androidx.fragment.app.FragmentActivity).supportFragmentManager
val historyFragment = HistoryFragment()
// Create a container for the fragment
val container = android.widget.FrameLayout(context).apply {
id = View.generateViewId()
}
// Add the fragment to the container
fragmentManager.beginTransaction()
.replace(container.id, historyFragment)
.commit()
container
}
)
}
}
@Preview(showBackground = true)
@Composable
fun ScreenCaptureUIPreview() {
@ -261,7 +353,8 @@ fun ScreenCaptureUIPreview() {
ScreenCaptureUI(
isCapturing = false,
onStartCapture = {},
onStopCapture = {}
onStopCapture = {},
onNavigateToHistory = {}
)
}
}

15
app/src/main/java/com/quillstudios/pokegoalshelper/di/ServiceLocator.kt

@ -44,6 +44,21 @@ object ServiceLocator
return _storageService!!
}
/**
* Get the storage service instance synchronously.
* Note: Service must already be initialized via getStorageService() first.
*/
fun getStorageServiceSync(): StorageInterface
{
return _storageService ?: run {
// Create service synchronously but initialization will happen later
val service = createStorageService()
_storageService = service
PGHLog.d(TAG, "Storage service created synchronously")
service
}
}
/**
* Manually set the storage service implementation.
* Useful for testing or switching implementations at runtime.

443
app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryAdapter.kt

@ -0,0 +1,443 @@
package com.quillstudios.pokegoalshelper.ui
import android.animation.ObjectAnimator
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.util.TypedValue
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import com.quillstudios.pokegoalshelper.models.DetectionResult
import com.quillstudios.pokegoalshelper.utils.PGHLog
import java.time.format.DateTimeFormatter
/**
* RecyclerView adapter for Pokemon detection history with expandable cards.
*
* Features:
* - Expandable/collapsible cards
* - Compact and detailed views
* - Delete functionality
* - Smooth animations
* - Performance optimized for large lists
*/
class HistoryAdapter(
private val onItemClick: (DetectionResult, Int) -> Unit,
private val onDeleteClick: (DetectionResult, Int) -> Unit
) : RecyclerView.Adapter<HistoryAdapter.HistoryViewHolder>()
{
companion object
{
private const val TAG = "HistoryAdapter"
}
data class HistoryItem(
val result: DetectionResult,
val isExpanded: Boolean = false
)
private val items = mutableListOf<HistoryItem>()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HistoryViewHolder
{
val cardView = createCardView(parent.context)
return HistoryViewHolder(cardView)
}
override fun onBindViewHolder(holder: HistoryViewHolder, position: Int)
{
val item = items[position]
holder.bind(item, position)
}
override fun getItemCount(): Int = items.size
fun updateResults(results: List<DetectionResult>)
{
items.clear()
items.addAll(results.map { HistoryItem(it, false) })
notifyDataSetChanged()
PGHLog.d(TAG, "Updated adapter with ${results.size} items")
}
fun removeItem(position: Int)
{
if (position in 0 until items.size) {
items.removeAt(position)
notifyItemRemoved(position)
PGHLog.d(TAG, "Removed item at position $position")
}
}
fun toggleExpansion(position: Int)
{
if (position in 0 until items.size) {
val item = items[position]
items[position] = item.copy(isExpanded = !item.isExpanded)
notifyItemChanged(position)
PGHLog.d(TAG, "Toggled expansion for position $position: ${items[position].isExpanded}")
}
}
private fun createCardView(context: Context): View
{
// Create main card container
val cardContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
background = createCardBackground(context)
setPadding(dpToPx(context, 16), dpToPx(context, 12), dpToPx(context, 16), dpToPx(context, 12))
layoutParams = RecyclerView.LayoutParams(
RecyclerView.LayoutParams.MATCH_PARENT,
RecyclerView.LayoutParams.WRAP_CONTENT
).apply {
setMargins(dpToPx(context, 8), dpToPx(context, 4), dpToPx(context, 8), dpToPx(context, 4))
}
}
// Collapsed content (always visible)
val collapsedContent = createCollapsedContent(context)
collapsedContent.tag = "collapsed_content"
// Expanded content (initially hidden)
val expandedContent = createExpandedContent(context)
expandedContent.tag = "expanded_content"
expandedContent.visibility = View.GONE
cardContainer.addView(collapsedContent)
cardContainer.addView(expandedContent)
return cardContainer
}
private fun createCollapsedContent(context: Context): View
{
return LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
gravity = android.view.Gravity.CENTER_VERTICAL
// Status icon
val statusIcon = ImageView(context).apply {
tag = "status_icon"
layoutParams = LinearLayout.LayoutParams(
dpToPx(context, 24),
dpToPx(context, 24)
).apply {
setMargins(0, 0, dpToPx(context, 12), 0)
}
}
// Main info container
val infoContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(
0,
ViewGroup.LayoutParams.WRAP_CONTENT,
1f
)
// Pokemon name/species
val titleText = TextView(context).apply {
tag = "title_text"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
typeface = android.graphics.Typeface.DEFAULT_BOLD
}
// Timestamp and confidence
val subtitleText = TextView(context).apply {
tag = "subtitle_text"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
}
addView(titleText)
addView(subtitleText)
}
// Expand chevron
val chevronIcon = TextView(context).apply {
tag = "chevron_icon"
text = ""
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(dpToPx(context, 8), 0, 0, 0)
}
}
addView(statusIcon)
addView(infoContainer)
addView(chevronIcon)
}
}
private fun createExpandedContent(context: Context): View
{
return ScrollView(context).apply {
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
dpToPx(context, 300) // Max height for expanded content
)
val contentContainer = LinearLayout(context).apply {
orientation = LinearLayout.VERTICAL
setPadding(0, dpToPx(context, 8), 0, 0)
tag = "expanded_container"
}
addView(contentContainer)
}
}
private fun createCardBackground(context: Context): GradientDrawable
{
return GradientDrawable().apply {
setColor(ContextCompat.getColor(context, android.R.color.black))
alpha = (0.8f * 255).toInt()
cornerRadius = dpToPx(context, 8).toFloat()
setStroke(1, ContextCompat.getColor(context, android.R.color.darker_gray))
}
}
private fun dpToPx(context: Context, dp: Int): Int
{
return TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
dp.toFloat(),
context.resources.displayMetrics
).toInt()
}
inner class HistoryViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
{
private val cardContainer = itemView as LinearLayout
private val collapsedContent = itemView.findViewWithTag<LinearLayout>("collapsed_content")
private val expandedContent = itemView.findViewWithTag<ScrollView>("expanded_content")
private val expandedContainer = expandedContent.findViewWithTag<LinearLayout>("expanded_container")
// Collapsed content views
private val statusIcon = collapsedContent.findViewWithTag<ImageView>("status_icon")
private val titleText = collapsedContent.findViewWithTag<TextView>("title_text")
private val subtitleText = collapsedContent.findViewWithTag<TextView>("subtitle_text")
private val chevronIcon = collapsedContent.findViewWithTag<TextView>("chevron_icon")
fun bind(item: HistoryItem, position: Int)
{
val result = item.result
val context = itemView.context
// Update collapsed content
updateCollapsedContent(result, context)
// Update expanded content if needed
if (item.isExpanded) {
updateExpandedContent(result, context)
showExpandedContent()
} else {
hideExpandedContent()
}
// Set click listeners
collapsedContent.setOnClickListener {
onItemClick(result, position)
}
// Add delete button in expanded state
if (item.isExpanded) {
addDeleteButton(result, position, context)
}
}
private fun updateCollapsedContent(result: DetectionResult, context: Context)
{
// Status icon
statusIcon.setImageResource(
if (result.success) android.R.drawable.ic_menu_myplaces
else android.R.drawable.ic_dialog_alert
)
statusIcon.setColorFilter(
ContextCompat.getColor(
context,
if (result.success) android.R.color.holo_green_light
else android.R.color.holo_red_light
)
)
// Title (Pokemon name or status)
titleText.text = when {
result.success && result.pokemonInfo?.species != null -> {
result.pokemonInfo.species +
(result.pokemonInfo.nationalDexNumber?.let { " (#$it)" } ?: "")
}
result.success -> "Pokemon Detected"
else -> "Detection Failed"
}
// Subtitle (timestamp and processing time)
val formatter = DateTimeFormatter.ofPattern("MMM dd, HH:mm")
subtitleText.text = "${result.timestamp.format(formatter)}${result.processingTimeMs}ms"
// Chevron rotation based on expansion state
val rotation = if (expandedContent.visibility == View.VISIBLE) 180f else 0f
chevronIcon.rotation = rotation
}
private fun updateExpandedContent(result: DetectionResult, context: Context)
{
expandedContainer.removeAllViews()
if (result.success && result.pokemonInfo != null) {
addPokemonInfoViews(result.pokemonInfo, context)
} else {
addErrorInfoViews(result, context)
}
// Technical info
addTechnicalInfoViews(result, context)
}
private fun addPokemonInfoViews(pokemonInfo: com.quillstudios.pokegoalshelper.PokemonInfo, context: Context)
{
// Basic info section
addSectionHeader("Basic Info", context)
pokemonInfo.level?.let { addInfoRow("Level", it.toString(), context) }
pokemonInfo.gender?.let { addInfoRow("Gender", it, context) }
pokemonInfo.nature?.let { addInfoRow("Nature", it, context) }
// Types section
if (pokemonInfo.primaryType != null || pokemonInfo.secondaryType != null) {
addSectionHeader("Types", context)
val typeText = when {
pokemonInfo.primaryType != null && pokemonInfo.secondaryType != null ->
"${pokemonInfo.primaryType} / ${pokemonInfo.secondaryType}"
pokemonInfo.primaryType != null -> pokemonInfo.primaryType
else -> "Unknown"
}
addInfoRow("Type", typeText, context)
pokemonInfo.teraType?.let { addInfoRow("Tera Type", it, context) }
}
// Special properties
if (pokemonInfo.isShiny || pokemonInfo.isAlpha || pokemonInfo.isFavorited) {
addSectionHeader("Properties", context)
if (pokemonInfo.isShiny) addInfoRow("Shiny", "✨ Yes", context)
if (pokemonInfo.isAlpha) addInfoRow("Alpha", "🅰 Yes", context)
if (pokemonInfo.isFavorited) addInfoRow("Favorited", "⭐ Yes", context)
}
}
private fun addErrorInfoViews(result: DetectionResult, context: Context)
{
addSectionHeader("Error Details", context)
addInfoRow("Status", if (result.success) "No Pokemon found" else "Detection failed", context)
result.errorMessage?.let { addInfoRow("Error", it, context) }
}
private fun addTechnicalInfoViews(result: DetectionResult, context: Context)
{
addSectionHeader("Technical Info", context)
addInfoRow("Processing Time", "${result.processingTimeMs}ms", context)
addInfoRow("Detections Found", result.detections.size.toString(), context)
addInfoRow("Timestamp", result.timestamp.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), context)
}
private fun addSectionHeader(title: String, context: Context)
{
val header = TextView(context).apply {
text = title
setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f)
setTextColor(ContextCompat.getColor(context, android.R.color.holo_blue_light))
typeface = android.graphics.Typeface.DEFAULT_BOLD
setPadding(0, dpToPx(context, 8), 0, dpToPx(context, 4))
}
expandedContainer.addView(header)
}
private fun addInfoRow(label: String, value: String, context: Context)
{
val row = LinearLayout(context).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(0, dpToPx(context, 2), 0, dpToPx(context, 2))
}
val labelView = TextView(context).apply {
text = "$label:"
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray))
layoutParams = LinearLayout.LayoutParams(
dpToPx(context, 100),
ViewGroup.LayoutParams.WRAP_CONTENT
)
}
val valueView = TextView(context).apply {
text = value
setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f)
setTextColor(ContextCompat.getColor(context, android.R.color.white))
layoutParams = LinearLayout.LayoutParams(
0,
ViewGroup.LayoutParams.WRAP_CONTENT,
1f
)
}
row.addView(labelView)
row.addView(valueView)
expandedContainer.addView(row)
}
private fun addDeleteButton(result: DetectionResult, position: Int, context: Context)
{
val deleteButton = Button(context).apply {
text = "Delete"
setTextColor(ContextCompat.getColor(context, android.R.color.white))
background = GradientDrawable().apply {
setColor(ContextCompat.getColor(context, android.R.color.holo_red_light))
cornerRadius = dpToPx(context, 4).toFloat()
}
setPadding(dpToPx(context, 16), dpToPx(context, 8), dpToPx(context, 16), dpToPx(context, 8))
setOnClickListener {
onDeleteClick(result, position)
}
layoutParams = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT
).apply {
setMargins(0, dpToPx(context, 8), 0, 0)
}
}
expandedContainer.addView(deleteButton)
}
private fun showExpandedContent()
{
expandedContent.visibility = View.VISIBLE
// Animate chevron rotation
ObjectAnimator.ofFloat(chevronIcon, "rotation", 0f, 180f).apply {
duration = 200L
start()
}
}
private fun hideExpandedContent()
{
expandedContent.visibility = View.GONE
// Animate chevron rotation
ObjectAnimator.ofFloat(chevronIcon, "rotation", 180f, 0f).apply {
duration = 200L
start()
}
}
}
}

193
app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryFragment.kt

@ -0,0 +1,193 @@
package com.quillstudios.pokegoalshelper.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.quillstudios.pokegoalshelper.models.DetectionResult
import com.quillstudios.pokegoalshelper.storage.StorageInterface
import com.quillstudios.pokegoalshelper.di.ServiceLocator
import com.quillstudios.pokegoalshelper.utils.PGHLog
import kotlinx.coroutines.launch
/**
* History Fragment displaying scrollable list of Pokemon detection results.
*
* Features:
* - RecyclerView with expandable cards
* - Empty state handling
* - Delete and refresh functionality
* - Performance optimized for large lists
*/
class HistoryFragment : Fragment()
{
companion object
{
private const val TAG = "HistoryFragment"
}
private lateinit var recyclerView: RecyclerView
private lateinit var historyAdapter: HistoryAdapter
private lateinit var emptyStateView: View
private val storage: StorageInterface by lazy { ServiceLocator.getStorageServiceSync() }
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View?
{
// Create the root view programmatically since we don't have XML layouts
return createHistoryView()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?)
{
super.onViewCreated(view, savedInstanceState)
setupRecyclerView()
loadHistory()
}
private fun createHistoryView(): View
{
val context = requireContext()
// Create main container
val container = androidx.constraintlayout.widget.ConstraintLayout(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
// Create RecyclerView
recyclerView = RecyclerView(context).apply {
id = View.generateViewId()
layoutParams = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams(
0, 0
).apply {
topToTop = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
bottomToBottom = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
startToStart = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
endToEnd = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
}
}
// Create empty state view
emptyStateView = android.widget.TextView(context).apply {
id = View.generateViewId()
text = "No Pokemon detections yet\\n\\nStart analyzing Pokemon Home screens to see results here!"
textAlignment = View.TEXT_ALIGNMENT_CENTER
setTextSize(android.util.TypedValue.COMPLEX_UNIT_SP, 16f)
setPadding(32, 32, 32, 32)
visibility = View.GONE
layoutParams = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams(
androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.WRAP_CONTENT,
androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.WRAP_CONTENT
).apply {
topToTop = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
bottomToBottom = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
startToStart = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
endToEnd = androidx.constraintlayout.widget.ConstraintLayout.LayoutParams.PARENT_ID
}
}
container.addView(recyclerView)
container.addView(emptyStateView)
return container
}
private fun setupRecyclerView()
{
historyAdapter = HistoryAdapter(
onItemClick = { result, position -> toggleItemExpansion(result, position) },
onDeleteClick = { result, position -> deleteResult(result, position) }
)
recyclerView.apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = historyAdapter
// Add item decoration for spacing
addItemDecoration(androidx.recyclerview.widget.DividerItemDecoration(
requireContext(),
androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
))
}
PGHLog.d(TAG, "RecyclerView setup complete")
}
private fun loadHistory()
{
lifecycleScope.launch {
try {
PGHLog.d(TAG, "Loading detection history...")
val results = storage.getDetectionResults()
if (results.isEmpty()) {
showEmptyState(true)
PGHLog.d(TAG, "No results found - showing empty state")
} else {
showEmptyState(false)
historyAdapter.updateResults(results)
PGHLog.d(TAG, "Loaded ${results.size} detection results")
}
} catch (e: Exception) {
PGHLog.e(TAG, "Error loading history", e)
showEmptyState(true)
}
}
}
fun refreshHistory()
{
PGHLog.d(TAG, "Refreshing history...")
loadHistory()
}
private fun deleteResult(result: DetectionResult, position: Int)
{
lifecycleScope.launch {
try {
val success = storage.deleteDetectionResult(result.id)
if (success) {
historyAdapter.removeItem(position)
PGHLog.d(TAG, "Deleted result: ${result.id}")
// Check if list is now empty
if (historyAdapter.itemCount == 0) {
showEmptyState(true)
}
} else {
PGHLog.w(TAG, "Failed to delete result: ${result.id}")
}
} catch (e: Exception) {
PGHLog.e(TAG, "Error deleting result", e)
}
}
}
private fun toggleItemExpansion(result: DetectionResult, position: Int)
{
historyAdapter.toggleExpansion(position)
PGHLog.d(TAG, "Toggled expansion for item at position $position")
}
private fun showEmptyState(show: Boolean)
{
if (show) {
recyclerView.visibility = View.GONE
emptyStateView.visibility = View.VISIBLE
} else {
recyclerView.visibility = View.VISIBLE
emptyStateView.visibility = View.GONE
}
}
}
Loading…
Cancel
Save