diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt new file mode 100644 index 0000000..5ab7627 --- /dev/null +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/controllers/DetectionController.kt @@ -0,0 +1,135 @@ +package com.quillstudios.pokegoalshelper.controllers + +import android.util.Log +import com.quillstudios.pokegoalshelper.YOLOOnnxDetector +import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUIEvents +import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUICallbacks +import org.opencv.core.Mat + +/** + * Controller handling detection business logic. + * Decouples UI interactions from YOLO detection implementation. + */ +class DetectionController( + private val yoloDetector: YOLOOnnxDetector +) : DetectionUIEvents { + + companion object { + private const val TAG = "DetectionController" + } + + private var uiCallbacks: DetectionUICallbacks? = null + private var currentSettings = DetectionSettings() + + /** + * Register UI callbacks for status updates + */ + fun setUICallbacks(callbacks: DetectionUICallbacks) { + uiCallbacks = callbacks + } + + /** + * Remove UI callbacks (cleanup) + */ + fun clearUICallbacks() { + uiCallbacks = null + } + + // === DetectionUIEvents Implementation === + + override fun onDetectionRequested() { + Log.d(TAG, "🔍 Detection requested via controller") + uiCallbacks?.onDetectionStarted() + + // Business logic will be handled by the service layer + // For now, just notify completion + uiCallbacks?.onDetectionCompleted(0) + } + + override fun onClassFilterChanged(className: String?) { + Log.i(TAG, "🔍 Class filter changed to: ${className ?: "ALL CLASSES"}") + + currentSettings.classFilter = className + + // Apply filter to YOLO detector + YOLOOnnxDetector.setClassFilter(className) + + // Notify UI of settings change + uiCallbacks?.onSettingsChanged( + currentSettings.classFilter, + currentSettings.debugMode, + currentSettings.coordinateMode + ) + } + + override fun onDebugModeToggled() { + currentSettings.debugMode = !currentSettings.debugMode + Log.i(TAG, "📊 Debug mode toggled: ${currentSettings.debugMode}") + + // Apply debug mode to YOLO detector + YOLOOnnxDetector.toggleShowAllConfidences() + + // Notify UI of settings change + uiCallbacks?.onSettingsChanged( + currentSettings.classFilter, + currentSettings.debugMode, + currentSettings.coordinateMode + ) + } + + override fun onCoordinateModeChanged(mode: String) { + Log.i(TAG, "🔧 Coordinate mode changed to: $mode") + + currentSettings.coordinateMode = mode + + // Apply coordinate mode to YOLO detector + YOLOOnnxDetector.setCoordinateMode(mode) + + // Notify UI of settings change + uiCallbacks?.onSettingsChanged( + currentSettings.classFilter, + currentSettings.debugMode, + currentSettings.coordinateMode + ) + } + + // === Business Logic Methods === + + /** + * Process detection on the given image + * This will be called by the service layer + */ + fun processDetection(inputMat: Mat): Int { + return try { + uiCallbacks?.onDetectionStarted() + + val detections = yoloDetector.detect(inputMat) + val detectionCount = detections.size + + Log.i(TAG, "✅ Detection completed: $detectionCount objects found") + uiCallbacks?.onDetectionCompleted(detectionCount) + + detectionCount + } catch (e: Exception) { + Log.e(TAG, "❌ Detection failed", e) + uiCallbacks?.onDetectionFailed(e.message ?: "Unknown error") + 0 + } + } + + /** + * Get current detection settings + */ + fun getCurrentSettings(): DetectionSettings { + return currentSettings.copy() + } +} + +/** + * Data class representing current detection settings + */ +data class DetectionSettings( + var classFilter: String? = null, + var debugMode: Boolean = false, + var coordinateMode: String = "HYBRID" +) \ No newline at end of file diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt new file mode 100644 index 0000000..dcb3690 --- /dev/null +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt @@ -0,0 +1,269 @@ +package com.quillstudios.pokegoalshelper.ui + +import android.content.Context +import android.graphics.PixelFormat +import android.os.Build +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.Button +import android.widget.LinearLayout +import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUIEvents +import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUICallbacks + +/** + * Floating orb UI component that handles user interactions. + * Implements CalcIV-style expandable menu system. + * + * This is pure UI logic - no business logic or direct detector calls. + */ +class FloatingOrbUI( + private val context: Context, + private val detectionEvents: DetectionUIEvents +) : DetectionUICallbacks { + + companion object { + private const val TAG = "FloatingOrbUI" + private const val ORB_SIZE = 120 + private const val MENU_BUTTON_WIDTH = 160 + private const val MENU_BUTTON_HEIGHT = 60 + } + + private var windowManager: WindowManager? = null + private var orbButton: View? = null + private var expandedMenu: View? = null + private var isMenuExpanded = false + private var isProcessing = false + + /** + * Initialize and show the floating orb + */ + fun show() { + try { + if (orbButton != null) return // Already shown + + windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + createFloatingOrb() + Log.d(TAG, "✅ Floating orb UI shown") + + } catch (e: Exception) { + Log.e(TAG, "❌ Error showing floating orb", e) + } + } + + /** + * Hide and cleanup the floating orb + */ + fun hide() { + try { + if (isMenuExpanded) { + collapseMenu() + } + + orbButton?.let { + windowManager?.removeView(it) + orbButton = null + } + + windowManager = null + Log.d(TAG, "🗑️ Floating orb UI hidden") + + } catch (e: Exception) { + Log.e(TAG, "❌ Error hiding floating orb", e) + } + } + + // === DetectionUICallbacks Implementation === + + override fun onDetectionStarted() { + isProcessing = true + updateOrbAppearance() + + // Auto-collapse menu during processing + if (isMenuExpanded) { + collapseMenu() + } + } + + override fun onDetectionCompleted(detectionCount: Int) { + isProcessing = false + updateOrbAppearance() + Log.d(TAG, "🎯 Detection completed: $detectionCount objects") + } + + override fun onDetectionFailed(error: String) { + isProcessing = false + updateOrbAppearance() + Log.e(TAG, "❌ Detection failed: $error") + } + + override fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String) { + Log.d(TAG, "⚙️ Settings updated - Filter: $filterClass, Debug: $debugMode, Mode: $coordinateMode") + // UI could update visual indicators here if needed + } + + // === Private UI Methods === + + private fun createFloatingOrb() { + orbButton = Button(context).apply { + text = "🎯" + textSize = 20f + setBackgroundResource(android.R.drawable.btn_default) + background.setTint(0xFF4CAF50.toInt()) // Green + setTextColor(0xFFFFFFFF.toInt()) + + width = ORB_SIZE + height = ORB_SIZE + layoutParams = ViewGroup.LayoutParams(ORB_SIZE, ORB_SIZE) + + setOnClickListener { handleOrbClick() } + } + + val params = WindowManager.LayoutParams( + ORB_SIZE, ORB_SIZE, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } else { + @Suppress("DEPRECATION") + WindowManager.LayoutParams.TYPE_PHONE + }, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT + ).apply { + gravity = Gravity.TOP or Gravity.START + x = 50 + y = 200 + } + + windowManager?.addView(orbButton, params) + } + + private fun handleOrbClick() { + if (isProcessing) { + Log.d(TAG, "⚠️ Ignoring click - detection in progress") + return + } + + if (isMenuExpanded) { + collapseMenu() + } else { + expandMenu() + } + } + + private fun expandMenu() { + if (isMenuExpanded || isProcessing) return + + val menuContainer = LinearLayout(context).apply { + orientation = LinearLayout.VERTICAL + setBackgroundColor(0xE0000000.toInt()) // Semi-transparent black + setPadding(16, 16, 16, 16) + } + + // Define menu options with their actions + val menuItems = listOf( + MenuOption("🔍 DETECT", 0xFF4CAF50.toInt()) { + detectionEvents.onDetectionRequested() + }, + MenuOption("SHINY", 0xFFFFD700.toInt()) { + detectionEvents.onClassFilterChanged("shiny_icon") + detectionEvents.onDetectionRequested() + }, + MenuOption("POKEBALL", 0xFFE91E63.toInt()) { + detectionEvents.onClassFilterChanged("ball_icon_cherishball") + detectionEvents.onDetectionRequested() + }, + MenuOption("ALL", 0xFF607D8B.toInt()) { + detectionEvents.onClassFilterChanged(null) + detectionEvents.onDetectionRequested() + }, + MenuOption("DEBUG", 0xFFFF5722.toInt()) { + detectionEvents.onDebugModeToggled() + detectionEvents.onDetectionRequested() + } + ) + + menuItems.forEach { option -> + val button = createMenuButton(option) + menuContainer.addView(button) + } + + expandedMenu = menuContainer + + val params = WindowManager.LayoutParams( + WindowManager.LayoutParams.WRAP_CONTENT, + WindowManager.LayoutParams.WRAP_CONTENT, + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } else { + @Suppress("DEPRECATION") + WindowManager.LayoutParams.TYPE_PHONE + }, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSLUCENT + ).apply { + gravity = Gravity.TOP or Gravity.START + x = 180 // Position next to the orb + y = 200 + } + + windowManager?.addView(expandedMenu, params) + isMenuExpanded = true + updateOrbAppearance() + } + + private fun collapseMenu() { + if (!isMenuExpanded) return + + expandedMenu?.let { windowManager?.removeView(it) } + expandedMenu = null + isMenuExpanded = false + updateOrbAppearance() + } + + private fun createMenuButton(option: MenuOption): Button { + return Button(context).apply { + text = option.text + textSize = 12f + setBackgroundColor(option.color) + setTextColor(0xFFFFFFFF.toInt()) + layoutParams = LinearLayout.LayoutParams(MENU_BUTTON_WIDTH, MENU_BUTTON_HEIGHT).apply { + setMargins(0, 0, 0, 8) + } + setOnClickListener { + option.action() + collapseMenu() + } + } + } + + private fun updateOrbAppearance() { + (orbButton as? Button)?.apply { + when { + isProcessing -> { + text = "⏳" + background.setTint(0xFFFF9800.toInt()) // Orange + } + isMenuExpanded -> { + text = "✖" + background.setTint(0xFFFF5722.toInt()) // Orange-red + } + else -> { + text = "🎯" + background.setTint(0xFF4CAF50.toInt()) // Green + } + } + } + } + + /** + * Data class for menu options + */ + private data class MenuOption( + val text: String, + val color: Int, + val action: () -> Unit + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt new file mode 100644 index 0000000..287bf72 --- /dev/null +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/interfaces/DetectionUIEvents.kt @@ -0,0 +1,60 @@ +package com.quillstudios.pokegoalshelper.ui.interfaces + +/** + * Interface for UI events related to detection functionality. + * UI components implement this to communicate with business logic controllers. + */ +interface DetectionUIEvents { + /** + * Triggered when user requests manual detection + */ + fun onDetectionRequested() + + /** + * Triggered when user changes class filter + * @param className Name of class to filter, or null for all classes + */ + fun onClassFilterChanged(className: String?) + + /** + * Triggered when user toggles debug mode + */ + fun onDebugModeToggled() + + /** + * Triggered when user changes coordinate transformation mode + * @param mode Transformation mode (DIRECT, LETTERBOX, HYBRID) + */ + fun onCoordinateModeChanged(mode: String) +} + +/** + * Interface for callbacks from business logic back to UI. + * UI components implement this to receive status updates. + */ +interface DetectionUICallbacks { + /** + * Called when detection starts processing + */ + fun onDetectionStarted() + + /** + * Called when detection completes successfully + * @param detectionCount Number of objects detected + */ + fun onDetectionCompleted(detectionCount: Int) + + /** + * Called when detection fails + * @param error Error message + */ + fun onDetectionFailed(error: String) + + /** + * Called when settings change + * @param filterClass Current class filter (null if showing all) + * @param debugMode Current debug mode state + * @param coordinateMode Current coordinate transformation mode + */ + fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String) +} \ No newline at end of file