Browse Source
- 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
6 changed files with 761 additions and 9 deletions
@ -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() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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…
Reference in new issue