From 3c1d730f3d3742d7f7d26c0a845a034def6e1c67 Mon Sep 17 00:00:00 2001 From: Quildra Date: Mon, 4 Aug 2025 18:08:41 +0100 Subject: [PATCH] feat: implement history list UI with expandable Pokemon cards (PGH-17) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .claude/settings.local.json | 9 +- app/build.gradle | 1 + .../pokegoalshelper/MainActivity.kt | 109 ++++- .../pokegoalshelper/di/ServiceLocator.kt | 15 + .../pokegoalshelper/ui/HistoryAdapter.kt | 443 ++++++++++++++++++ .../pokegoalshelper/ui/HistoryFragment.kt | 193 ++++++++ 6 files changed, 761 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryAdapter.kt create mode 100644 app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryFragment.kt diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e587c0e..fae51d4 100644 --- a/.claude/settings.local.json +++ b/.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": [] } diff --git a/app/build.gradle b/app/build.gradle index 6196324..5355dc1 100644 --- a/app/build.gradle +++ b/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 diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt index a11a3b5..b9ed252 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt +++ b/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 = {} ) } } \ No newline at end of file diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/di/ServiceLocator.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/di/ServiceLocator.kt index bf80877..ddabc3d 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/di/ServiceLocator.kt +++ b/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. diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryAdapter.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryAdapter.kt new file mode 100644 index 0000000..36962cd --- /dev/null +++ b/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() +{ + companion object + { + private const val TAG = "HistoryAdapter" + } + + data class HistoryItem( + val result: DetectionResult, + val isExpanded: Boolean = false + ) + + private val items = mutableListOf() + + 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) + { + 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("collapsed_content") + private val expandedContent = itemView.findViewWithTag("expanded_content") + private val expandedContainer = expandedContent.findViewWithTag("expanded_container") + + // Collapsed content views + private val statusIcon = collapsedContent.findViewWithTag("status_icon") + private val titleText = collapsedContent.findViewWithTag("title_text") + private val subtitleText = collapsedContent.findViewWithTag("subtitle_text") + private val chevronIcon = collapsedContent.findViewWithTag("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() + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryFragment.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/HistoryFragment.kt new file mode 100644 index 0000000..c7df704 --- /dev/null +++ b/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 + } + } +} \ No newline at end of file