diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b0605a5..8cf0b47 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -29,15 +29,6 @@
-
-
= 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.BOTTOM or Gravity.END
- x = dpToPx(FAB_MARGIN)
- y = dpToPx(FAB_MARGIN + 80) // Account for potential system UI
- }
-
- windowManager?.addView(fabContainer, params)
- }
-
- private fun createMainFab(): View {
- return FrameLayout(context).apply {
- val fabSize = dpToPx(FAB_SIZE_LARGE)
- layoutParams = FrameLayout.LayoutParams(fabSize, fabSize)
-
- // Create FAB background with Material 3 styling
- background = createFabBackground(
- color = getColor(android.R.color.holo_blue_bright),
- elevation = ELEVATION_RESTING
- )
-
- // Add icon
- val iconView = ImageView(context).apply {
- layoutParams = FrameLayout.LayoutParams(
- dpToPx(24), dpToPx(24), Gravity.CENTER
- )
- setImageResource(android.R.drawable.ic_menu_camera) // Using target icon
- setColorFilter(Color.WHITE)
- scaleType = ImageView.ScaleType.FIT_CENTER
- }
- addView(iconView)
-
- // Set up click handling
- setOnClickListener { handleFabClick() }
-
- // Add touch feedback
- isClickable = true
- isFocusable = true
-
- // Apply initial elevation
- elevation = ELEVATION_RESTING
- }
- }
-
- private fun createFabBackground(color: Int, elevation: Float): GradientDrawable {
- return GradientDrawable().apply {
- shape = GradientDrawable.OVAL
- setColor(color)
-
- // Add subtle gradient for depth
- colors = intArrayOf(
- lightenColor(color, 0.1f),
- color,
- darkenColor(color, 0.1f)
- )
- gradientType = GradientDrawable.RADIAL_GRADIENT
- gradientRadius = dpToPx(FAB_SIZE_LARGE).toFloat() / 2
-
- // Material 3 shadow simulation (since we can't use real elevation in overlays)
- setStroke(dpToPx(1), Color.argb(30, 0, 0, 0))
- }
- }
-
- private fun handleFabClick() {
- provideFeedback(VibrationEffect.EFFECT_CLICK)
-
- if (isProcessing) {
- Log.d(TAG, "⚠️ Ignoring click - detection in progress")
- return
- }
-
- if (isMenuExpanded) {
- collapseMenu()
- } else {
- expandMenu()
- }
- }
-
- private fun expandMenu() {
- if (isMenuExpanded || isProcessing) return
-
- // Create scrim for backdrop
- createScrim()
-
- // Create expanded menu
- expandedMenu = createExpandedMenu()
- fabContainer?.addView(expandedMenu)
-
- // Animate expansion
- animateMenuExpansion(true)
-
- isMenuExpanded = true
- updateFabState()
- }
-
- private fun collapseMenu(animate: Boolean = true) {
- if (!isMenuExpanded) return
-
- if (animate) {
- animateMenuExpansion(false) {
- removeMenuViews()
- }
- } else {
- removeMenuViews()
- }
-
- isMenuExpanded = false
- updateFabState()
- }
-
- private fun createScrim() {
- scrimView = View(context).apply {
- layoutParams = ViewGroup.LayoutParams(
- dpToPx(200), dpToPx(300)
- )
- setBackgroundColor(Color.argb(80, 0, 0, 0)) // Semi-transparent scrim
- alpha = 0f
- setOnClickListener { collapseMenu() }
- }
- fabContainer?.addView(scrimView, 0) // Add as first child (background)
- }
-
- private fun createExpandedMenu(): LinearLayout {
- return LinearLayout(context).apply {
- orientation = LinearLayout.VERTICAL
- layoutParams = FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT,
- Gravity.BOTTOM or Gravity.END
- ).apply {
- bottomMargin = dpToPx(FAB_SIZE_LARGE + 16)
- rightMargin = dpToPx(8)
- }
-
- // Create menu items
- val menuItems = listOf(
- FabMenuItem("🔍", "DETECT", getColor(android.R.color.holo_green_dark)) {
- detectionEvents.onDetectionRequested()
- },
- FabMenuItem("✨", "SHINY", getColor(android.R.color.holo_orange_light)) {
- detectionEvents.onClassFilterChanged("shiny_icon")
- detectionEvents.onDetectionRequested()
- },
- FabMenuItem("⚪", "BALL", getColor(android.R.color.holo_red_dark)) {
- detectionEvents.onClassFilterChanged("ball_icon_cherishball")
- detectionEvents.onDetectionRequested()
- },
- FabMenuItem("🎯", "ALL", getColor(android.R.color.darker_gray)) {
- detectionEvents.onClassFilterChanged(null)
- detectionEvents.onDetectionRequested()
- },
- FabMenuItem("🔧", "DEBUG", getColor(android.R.color.holo_purple)) {
- detectionEvents.onDebugModeToggled()
- detectionEvents.onDetectionRequested()
- }
- )
-
- menuItems.forEachIndexed { index, item ->
- val fabItem = createMenuFab(item)
- addView(fabItem)
-
- // Add spacing between items
- if (index < menuItems.size - 1) {
- val spacer = View(context).apply {
- layoutParams = LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(8)
- )
- }
- addView(spacer)
- }
- }
- }
- }
-
- private fun createMenuFab(item: FabMenuItem): View {
- return LinearLayout(context).apply {
- orientation = LinearLayout.HORIZONTAL
- gravity = Gravity.CENTER_VERTICAL
- layoutParams = LinearLayout.LayoutParams(
- ViewGroup.LayoutParams.WRAP_CONTENT,
- ViewGroup.LayoutParams.WRAP_CONTENT
- )
-
- // Add label
- val label = TextView(context).apply {
- text = item.label
- setTextColor(Color.WHITE)
- textSize = 12f
- setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(4))
- setBackgroundResource(android.R.drawable.btn_default)
- background.setTint(Color.argb(200, 0, 0, 0))
- alpha = 0f // Start invisible for animation
- }
- addView(label)
-
- // Add small FAB
- val miniFab = FrameLayout(context).apply {
- val size = dpToPx(FAB_SIZE_SMALL)
- layoutParams = LinearLayout.LayoutParams(size, size).apply {
- leftMargin = dpToPx(8)
- }
-
- background = createFabBackground(item.color, ELEVATION_MENU)
- elevation = ELEVATION_MENU
- alpha = 0f // Start invisible for animation
-
- // Add icon
- val iconView = TextView(context).apply {
- text = item.icon
- textSize = 16f
- gravity = Gravity.CENTER
- layoutParams = FrameLayout.LayoutParams(
- ViewGroup.LayoutParams.MATCH_PARENT,
- ViewGroup.LayoutParams.MATCH_PARENT
- )
- }
- addView(iconView)
-
- setOnClickListener {
- provideFeedback(VibrationEffect.EFFECT_CLICK)
- item.action()
- collapseMenu()
- }
-
- isClickable = true
- isFocusable = true
- }
- addView(miniFab)
- }
- }
-
- private fun animateMenuExpansion(expand: Boolean, onComplete: (() -> Unit)? = null) {
- currentAnimator?.cancel()
-
- val animatorSet = AnimatorSet()
- val animators = mutableListOf()
-
- // Animate scrim
- scrimView?.let { scrim ->
- animators.add(ObjectAnimator.ofFloat(
- scrim, "alpha",
- if (expand) 0f else 1f,
- if (expand) 1f else 0f
- ))
- }
-
- // Animate menu items
- expandedMenu?.let { menu ->
- for (i in 0 until menu.childCount) {
- val child = menu.getChildAt(i)
- if (child is LinearLayout) {
- // Animate each FAB item
- val delay = if (expand) i * 50L else (menu.childCount - i) * 30L
-
- val scaleX = ObjectAnimator.ofFloat(
- child, "scaleX",
- if (expand) 0f else 1f,
- if (expand) 1f else 0f
- )
- val scaleY = ObjectAnimator.ofFloat(
- child, "scaleY",
- if (expand) 0f else 1f,
- if (expand) 1f else 0f
- )
- val alpha = ObjectAnimator.ofFloat(
- child, "alpha",
- if (expand) 0f else 1f,
- if (expand) 1f else 0f
- )
-
- scaleX.startDelay = delay
- scaleY.startDelay = delay
- alpha.startDelay = delay
-
- animators.addAll(listOf(scaleX, scaleY, alpha))
- }
- }
- }
-
- // Configure animation set
- animatorSet.playTogether(animators as Collection)
- animatorSet.duration = ANIMATION_DURATION_MEDIUM
- animatorSet.interpolator = if (expand) OvershootInterpolator() else AccelerateDecelerateInterpolator()
-
- animatorSet.addListener(object : android.animation.AnimatorListenerAdapter() {
- override fun onAnimationEnd(animation: android.animation.Animator) {
- onComplete?.invoke()
- currentAnimator = null
- }
- })
-
- currentAnimator = animatorSet
- animatorSet.start()
- }
-
- private fun removeMenuViews() {
- scrimView?.let { fabContainer?.removeView(it) }
- expandedMenu?.let { fabContainer?.removeView(it) }
- scrimView = null
- expandedMenu = null
- }
-
- private fun updateFabState() {
- val iconView = (mainFab as? FrameLayout)?.getChildAt(0) as? ImageView
- val fab = mainFab as? FrameLayout
-
- when {
- isProcessing -> {
- iconView?.setImageResource(android.R.drawable.ic_popup_sync)
- fab?.background = createFabBackground(
- getColor(android.R.color.holo_orange_light),
- ELEVATION_PRESSED
- )
- // Add rotation animation for processing state
- animateProcessing(iconView)
- }
- isMenuExpanded -> {
- iconView?.setImageResource(android.R.drawable.ic_menu_close_clear_cancel)
- fab?.background = createFabBackground(
- getColor(android.R.color.holo_red_light),
- ELEVATION_PRESSED
- )
- }
- else -> {
- iconView?.setImageResource(android.R.drawable.ic_menu_camera)
- fab?.background = createFabBackground(
- getColor(android.R.color.holo_blue_bright),
- ELEVATION_RESTING
- )
- iconView?.clearAnimation()
- }
- }
- }
-
- private fun updateMenuItems() {
- // This could update menu item states based on current filter/debug settings
- // For now, just log that we received the update
- Log.d(TAG, "Menu items updated based on current settings")
- }
-
- private fun animateProcessing(view: View?) {
- view?.let {
- val rotation = ObjectAnimator.ofFloat(it, "rotation", 0f, 360f)
- rotation.duration = 1000L
- rotation.repeatCount = ValueAnimator.INFINITE
- rotation.interpolator = AccelerateDecelerateInterpolator()
- rotation.start()
- }
- }
-
- private fun provideFeedback(effect: Int) {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- vibrator?.vibrate(VibrationEffect.createPredefined(effect))
- } else {
- @Suppress("DEPRECATION")
- vibrator?.vibrate(50)
- }
- }
-
- // === Utility Methods ===
-
- private fun dpToPx(dp: Int): Int {
- return (dp * context.resources.displayMetrics.density).toInt()
- }
-
- private fun getColor(colorRes: Int): Int {
- return ContextCompat.getColor(context, colorRes)
- }
-
- private fun lightenColor(color: Int, factor: Float): Int {
- val red = Color.red(color)
- val green = Color.green(color)
- val blue = Color.blue(color)
-
- return Color.rgb(
- (red + (255 - red) * factor).toInt().coerceAtMost(255),
- (green + (255 - green) * factor).toInt().coerceAtMost(255),
- (blue + (255 - blue) * factor).toInt().coerceAtMost(255)
- )
- }
-
- private fun darkenColor(color: Int, factor: Float): Int {
- val red = Color.red(color)
- val green = Color.green(color)
- val blue = Color.blue(color)
-
- return Color.rgb(
- (red * (1 - factor)).toInt().coerceAtLeast(0),
- (green * (1 - factor)).toInt().coerceAtLeast(0),
- (blue * (1 - factor)).toInt().coerceAtLeast(0)
- )
- }
-
- /**
- * Data class for FAB menu items
- */
- private data class FabMenuItem(
- val icon: String,
- val label: String,
- val color: Int,
- val action: () -> Unit
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt
deleted file mode 100644
index 89ab602..0000000
--- a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt
+++ /dev/null
@@ -1,352 +0,0 @@
-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.WindowManager
-import androidx.compose.animation.*
-import androidx.compose.animation.core.*
-import androidx.compose.foundation.gestures.detectDragGestures
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.*
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.ComposeView
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme
-
-
-/**
- * True floating overlay using WindowManager + ComposeView.
- * This creates a proper system overlay that floats over all apps.
- */
-class FloatingComposeOverlay(
- private val context: Context,
- private val onDetectionRequested: () -> Unit,
- private val onClassFilterRequested: (String?) -> Unit,
- private val onDebugToggled: () -> Unit,
- private val onClose: () -> Unit
-) {
- companion object {
- private const val TAG = "FloatingComposeOverlay"
- }
-
- private var windowManager: WindowManager? = null
- private var overlayView: ComposeView? = null
- private var isShowing = false
-
- // Mutable state for Compose
- private var fabPosition = mutableStateOf(Offset.Zero)
-
- fun show() {
- if (isShowing) return
-
- try {
- windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
- createOverlayView()
- isShowing = true
- Log.d(TAG, "✅ Floating Compose overlay shown")
- } catch (e: Exception) {
- Log.e(TAG, "❌ Error showing floating overlay", e)
- }
- }
-
- fun hide() {
- if (!isShowing) return
-
- try {
- overlayView?.let { windowManager?.removeView(it) }
- overlayView = null
- windowManager = null
- isShowing = false
- Log.d(TAG, "🗑️ Floating Compose overlay hidden")
- } catch (e: Exception) {
- Log.e(TAG, "❌ Error hiding floating overlay", e)
- }
- }
-
- private fun createOverlayView() {
- overlayView = ComposeView(context).apply {
- setContent {
- PokeGoalsHelperTheme {
- FloatingFABContent(
- position = fabPosition.value,
- onPositionChanged = { newPosition ->
- fabPosition.value = newPosition
- updateWindowPosition(newPosition)
- },
- onDetectionRequested = onDetectionRequested,
- onClassFilterRequested = onClassFilterRequested,
- onDebugToggled = onDebugToggled,
- onClose = onClose
- )
- }
- }
- }
-
- // Window parameters for true floating behavior
- 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 or
- WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or
- WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
- WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS,
- PixelFormat.TRANSLUCENT
- ).apply {
- gravity = Gravity.TOP or Gravity.START
- x = 50 // Initial position
- y = 200
- }
-
- windowManager?.addView(overlayView, params)
- }
-
- private fun updateWindowPosition(position: Offset) {
- overlayView?.let { view ->
- val params = view.layoutParams as WindowManager.LayoutParams
- params.x = position.x.toInt()
- params.y = position.y.toInt()
- try {
- windowManager?.updateViewLayout(view, params)
- } catch (e: Exception) {
- Log.w(TAG, "Failed to update window position", e)
- }
- }
- }
-}
-
-@Composable
-fun FloatingFABContent(
- position: Offset,
- onPositionChanged: (Offset) -> Unit,
- onDetectionRequested: () -> Unit,
- onClassFilterRequested: (String?) -> Unit,
- onDebugToggled: () -> Unit,
- onClose: () -> Unit
-) {
- var isMenuExpanded by remember { mutableStateOf(false) }
- var isProcessing by remember { mutableStateOf(false) }
- val hapticFeedback = LocalHapticFeedback.current
-
- // Animation states
- val rotation by animateFloatAsState(
- targetValue = if (isMenuExpanded) 45f else 0f,
- animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
- label = "FAB rotation"
- )
-
- val processingRotation by animateFloatAsState(
- targetValue = if (isProcessing) 360f else 0f,
- animationSpec = infiniteRepeatable(
- animation = tween(1000, easing = LinearEasing),
- repeatMode = RepeatMode.Restart
- ),
- label = "Processing rotation"
- )
-
- Column(
- horizontalAlignment = Alignment.End,
- verticalArrangement = Arrangement.Bottom
- ) {
- // Expanded Menu Items
- AnimatedVisibility(
- visible = isMenuExpanded,
- enter = fadeIn(animationSpec = tween(300)) +
- slideInVertically(
- initialOffsetY = { it / 2 },
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessLow
- )
- ),
- exit = fadeOut(animationSpec = tween(200)) +
- slideOutVertically(
- targetOffsetY = { it / 2 },
- animationSpec = tween(200)
- )
- ) {
- Column(
- horizontalAlignment = Alignment.End,
- verticalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.padding(bottom = 16.dp)
- ) {
- // Menu FAB items
- OverlayMenuFABItem(
- icon = Icons.Default.Search,
- label = "DETECT",
- containerColor = MaterialTheme.colorScheme.primary,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- OverlayMenuFABItem(
- icon = Icons.Default.Star,
- label = "SHINY",
- containerColor = MaterialTheme.colorScheme.tertiary,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onClassFilterRequested("shiny_icon")
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- OverlayMenuFABItem(
- icon = Icons.Default.AccountCircle,
- label = "POKEBALL",
- containerColor = MaterialTheme.colorScheme.error,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onClassFilterRequested("ball_icon_cherishball")
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- OverlayMenuFABItem(
- icon = Icons.Default.List,
- label = "ALL",
- containerColor = MaterialTheme.colorScheme.secondary,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onClassFilterRequested(null)
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- OverlayMenuFABItem(
- icon = Icons.Default.Build,
- label = "DEBUG",
- containerColor = MaterialTheme.colorScheme.outline,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onDebugToggled()
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
- }
- }
-
- // Main FAB with drag support
- FloatingActionButton(
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- if (!isProcessing) {
- isMenuExpanded = !isMenuExpanded
- }
- },
- modifier = Modifier
- .size(56.dp)
- .rotate(if (isProcessing) processingRotation else rotation)
- .pointerInput(Unit) {
- detectDragGestures(
- onDragStart = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.TextHandleMove)
- },
- onDrag = { _, dragAmount ->
- val newPosition = Offset(
- position.x + dragAmount.x,
- position.y + dragAmount.y
- )
- onPositionChanged(newPosition)
- }
- )
- },
- containerColor = when {
- isProcessing -> MaterialTheme.colorScheme.tertiary
- isMenuExpanded -> MaterialTheme.colorScheme.error
- else -> MaterialTheme.colorScheme.primary
- },
- elevation = FloatingActionButtonDefaults.elevation(
- defaultElevation = 6.dp,
- pressedElevation = 12.dp
- )
- ) {
- Icon(
- imageVector = when {
- isProcessing -> Icons.Default.Refresh
- isMenuExpanded -> Icons.Default.Close
- else -> Icons.Default.Home
- },
- contentDescription = when {
- isProcessing -> "Processing..."
- isMenuExpanded -> "Close menu"
- else -> "Open detection menu"
- },
- tint = MaterialTheme.colorScheme.onPrimary
- )
- }
- }
-}
-
-@Composable
-fun OverlayMenuFABItem(
- icon: ImageVector,
- label: String,
- containerColor: Color,
- onClick: () -> Unit
-) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- // Label
- Surface(
- color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
- shape = MaterialTheme.shapes.small,
- shadowElevation = 2.dp
- ) {
- Text(
- text = label,
- modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = 12.sp
- )
- }
-
- // Mini FAB
- FloatingActionButton(
- onClick = onClick,
- modifier = Modifier.size(40.dp),
- containerColor = containerColor,
- elevation = FloatingActionButtonDefaults.elevation(
- defaultElevation = 4.dp,
- pressedElevation = 8.dp
- )
- ) {
- Icon(
- imageVector = icon,
- contentDescription = label,
- tint = Color.White,
- modifier = Modifier.size(20.dp)
- )
- }
- }
-}
\ 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
deleted file mode 100644
index feb1761..0000000
--- a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingOrbUI.kt
+++ /dev/null
@@ -1,270 +0,0 @@
-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 = 14f // Increased text size
- setBackgroundColor(option.color)
- setTextColor(0xFFFFFFFF.toInt())
- setPadding(8, 4, 8, 4) // Add padding for better text spacing
- 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/FloatingUIActivity.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt
deleted file mode 100644
index 35bb558..0000000
--- a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingUIActivity.kt
+++ /dev/null
@@ -1,444 +0,0 @@
-package com.quillstudios.pokegoalshelper.ui
-
-import android.app.Activity
-import android.content.*
-import android.os.Bundle
-import android.os.IBinder
-import android.util.Log
-import android.view.WindowManager
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.animation.*
-import androidx.compose.animation.core.*
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.*
-import androidx.compose.material3.*
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.foundation.gestures.detectDragGestures
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.unit.IntOffset
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
-import androidx.compose.ui.window.Dialog
-import androidx.compose.ui.window.DialogProperties
-import com.quillstudios.pokegoalshelper.ScreenCaptureService
-import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme
-
-/**
- * Transparent Activity that hosts the floating Material 3 FAB UI.
- * Communicates with ScreenCaptureService via Binder for detection functionality.
- *
- * This approach allows us to use true Jetpack Compose with Material 3 components
- * while keeping the detection logic in a background service.
- */
-class FloatingUIActivity : ComponentActivity() {
-
- companion object {
- private const val TAG = "FloatingUIActivity"
- const val ACTION_SHOW_FAB = "SHOW_FAB"
- const val ACTION_HIDE_FAB = "HIDE_FAB"
- }
-
- private var screenCaptureService: ScreenCaptureService? = null
- private var serviceBound = false
-
- // Service connection to communicate with ScreenCaptureService
- private val serviceConnection = object : ServiceConnection {
- override fun onServiceConnected(className: ComponentName, service: IBinder) {
- Log.d(TAG, "Connected to ScreenCaptureService")
- val binder = service as ScreenCaptureService.LocalBinder
- screenCaptureService = binder.getService()
- serviceBound = true
- }
-
- override fun onServiceDisconnected(arg0: ComponentName) {
- Log.d(TAG, "Disconnected from ScreenCaptureService")
- screenCaptureService = null
- serviceBound = false
- }
- }
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- // Bind to the ScreenCaptureService
- bindToScreenCaptureService()
-
- setContent {
- PokeGoalsHelperTheme {
- FloatingFABInterface(
- onDetectionRequested = { requestDetection() },
- onClassFilterRequested = { className -> requestClassFilter(className) },
- onDebugToggled = { toggleDebugMode() },
- onClose = { finish() }
- )
- }
- }
- }
-
- override fun onResume() {
- super.onResume()
- // Setup overlay after window is fully initialized
- setupTransparentOverlay()
- }
-
- override fun onDestroy() {
- super.onDestroy()
- if (serviceBound) {
- unbindService(serviceConnection)
- serviceBound = false
- }
- }
-
- private fun setupTransparentOverlay() {
- try {
- // Make the activity transparent and fullscreen
- window?.apply {
- // Hide system UI (navigation bar, status bar)
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.R) {
- setDecorFitsSystemWindows(false)
- insetsController?.let { controller ->
- controller.hide(android.view.WindowInsets.Type.systemBars())
- controller.systemBarsBehavior = android.view.WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- }
- } else {
- @Suppress("DEPRECATION")
- decorView.systemUiVisibility = (
- android.view.View.SYSTEM_UI_FLAG_LAYOUT_STABLE
- or android.view.View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
- or android.view.View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
- or android.view.View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
- or android.view.View.SYSTEM_UI_FLAG_FULLSCREEN
- or android.view.View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
- )
- }
-
- // Make it show over other apps
- setFlags(
- WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
- WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
- )
- setFlags(
- WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH,
- WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
- )
-
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY)
- } else {
- @Suppress("DEPRECATION")
- setType(WindowManager.LayoutParams.TYPE_PHONE)
- }
- }
- } catch (e: Exception) {
- Log.e(TAG, "Error setting up transparent overlay", e)
- // Continue without overlay setup if it fails
- }
- }
-
- private fun bindToScreenCaptureService() {
- val intent = Intent(this, ScreenCaptureService::class.java)
- bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
- }
-
- // === Communication with Service ===
-
- private fun requestDetection() {
- screenCaptureService?.triggerDetection()
- Log.d(TAG, "Requested detection from service")
- }
-
- private fun requestClassFilter(className: String?) {
- screenCaptureService?.setClassFilter(className)
- Log.d(TAG, "Set class filter to: $className")
- }
-
- private fun toggleDebugMode() {
- screenCaptureService?.toggleDebugMode()
- Log.d(TAG, "Toggled debug mode")
- }
-}
-
-@Composable
-fun FloatingFABInterface(
- onDetectionRequested: () -> Unit,
- onClassFilterRequested: (String?) -> Unit,
- onDebugToggled: () -> Unit,
- onClose: () -> Unit
-) {
- var isMenuExpanded by remember { mutableStateOf(false) }
- var isProcessing by remember { mutableStateOf(false) }
- val hapticFeedback = LocalHapticFeedback.current
-
- // Draggable position state
- var fabOffset by remember { mutableStateOf(Offset.Zero) }
- val configuration = LocalConfiguration.current
- val density = LocalDensity.current
-
- // Calculate screen bounds for FAB positioning
- val screenWidth = with(density) { configuration.screenWidthDp.dp.toPx() }
- val screenHeight = with(density) { configuration.screenHeightDp.dp.toPx() }
- val fabSize = with(density) { 56.dp.toPx() }
-
- Box(
- modifier = Modifier
- .fillMaxSize()
- .background(Color.Transparent)
- ) {
- // Main content area - transparent to allow touches through
- Spacer(modifier = Modifier.fillMaxSize())
-
- // FAB and menu area - positioned with offset
- Column(
- horizontalAlignment = Alignment.End,
- verticalArrangement = Arrangement.Bottom,
- modifier = Modifier
- .offset {
- IntOffset(
- fabOffset.x.toInt(),
- fabOffset.y.toInt()
- )
- }
- .padding(16.dp)
- ) {
-
- // Expanded Menu Items
- AnimatedVisibility(
- visible = isMenuExpanded,
- enter = fadeIn(animationSpec = tween(300)) +
- slideInVertically(
- initialOffsetY = { it / 2 },
- animationSpec = spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessLow
- )
- ),
- exit = fadeOut(animationSpec = tween(200)) +
- slideOutVertically(
- targetOffsetY = { it / 2 },
- animationSpec = tween(200)
- )
- ) {
- Column(
- horizontalAlignment = Alignment.End,
- verticalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier.padding(bottom = 16.dp)
- ) {
- // Menu FAB items
- MenuFABItem(
- icon = Icons.Default.Search,
- label = "DETECT",
- containerColor = MaterialTheme.colorScheme.primary,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- MenuFABItem(
- icon = Icons.Default.Star,
- label = "SHINY",
- containerColor = MaterialTheme.colorScheme.tertiary,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onClassFilterRequested("shiny_icon")
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- MenuFABItem(
- icon = Icons.Default.AccountCircle,
- label = "POKEBALL",
- containerColor = MaterialTheme.colorScheme.error,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onClassFilterRequested("ball_icon_cherishball")
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- MenuFABItem(
- icon = Icons.Default.List,
- label = "ALL",
- containerColor = MaterialTheme.colorScheme.secondary,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onClassFilterRequested(null)
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
-
- MenuFABItem(
- icon = Icons.Default.Build,
- label = "DEBUG",
- containerColor = MaterialTheme.colorScheme.outline,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onDebugToggled()
- onDetectionRequested()
- isMenuExpanded = false
- }
- )
- }
- }
-
- // Main FAB
- MainFloatingActionButton(
- isProcessing = isProcessing,
- isMenuExpanded = isMenuExpanded,
- onClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- if (!isProcessing) {
- isMenuExpanded = !isMenuExpanded
- }
- },
- onLongClick = {
- hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
- onClose()
- },
- onDrag = { dragAmount ->
- val newOffset = Offset(
- (fabOffset.x + dragAmount.x).coerceIn(
- -screenWidth + fabSize,
- 0f
- ),
- (fabOffset.y + dragAmount.y).coerceIn(
- -screenHeight + fabSize,
- 0f
- )
- )
- fabOffset = newOffset
- }
- )
- }
- }
-}
-
-@Composable
-fun MainFloatingActionButton(
- isProcessing: Boolean,
- isMenuExpanded: Boolean,
- onClick: () -> Unit,
- onLongClick: () -> Unit,
- onDrag: (Offset) -> Unit
-) {
- val rotation by animateFloatAsState(
- targetValue = if (isMenuExpanded) 45f else 0f,
- animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy),
- label = "FAB rotation"
- )
-
- val processingRotation by animateFloatAsState(
- targetValue = if (isProcessing) 360f else 0f,
- animationSpec = infiniteRepeatable(
- animation = tween(1000, easing = LinearEasing),
- repeatMode = RepeatMode.Restart
- ),
- label = "Processing rotation"
- )
-
- FloatingActionButton(
- onClick = onClick,
- modifier = Modifier
- .size(56.dp)
- .rotate(if (isProcessing) processingRotation else rotation)
- .pointerInput(Unit) {
- detectDragGestures(
- onDragStart = {
- // Provide haptic feedback when drag starts
- },
- onDragEnd = {
- // Optional: snap to screen edges
- },
- onDrag = { _, dragAmount ->
- onDrag(dragAmount)
- }
- )
- },
- containerColor = when {
- isProcessing -> MaterialTheme.colorScheme.tertiary
- isMenuExpanded -> MaterialTheme.colorScheme.error
- else -> MaterialTheme.colorScheme.primary
- },
- elevation = FloatingActionButtonDefaults.elevation(
- defaultElevation = 6.dp,
- pressedElevation = 12.dp
- )
- ) {
- Icon(
- imageVector = when {
- isProcessing -> Icons.Default.Refresh
- isMenuExpanded -> Icons.Default.Close
- else -> Icons.Default.Home
- },
- contentDescription = when {
- isProcessing -> "Processing..."
- isMenuExpanded -> "Close menu"
- else -> "Open detection menu"
- },
- tint = MaterialTheme.colorScheme.onPrimary
- )
- }
-}
-
-@Composable
-fun MenuFABItem(
- icon: ImageVector,
- label: String,
- containerColor: Color,
- onClick: () -> Unit
-) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- // Label
- Surface(
- color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f),
- shape = MaterialTheme.shapes.small,
- shadowElevation = 2.dp
- ) {
- Text(
- text = label,
- modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = 12.sp
- )
- }
-
- // Mini FAB
- FloatingActionButton(
- onClick = onClick,
- modifier = Modifier.size(40.dp),
- containerColor = containerColor,
- elevation = FloatingActionButtonDefaults.elevation(
- defaultElevation = 4.dp,
- pressedElevation = 8.dp
- )
- ) {
- Icon(
- imageVector = icon,
- contentDescription = label,
- tint = Color.White,
- modifier = Modifier.size(20.dp)
- )
- }
- }
-}
\ No newline at end of file