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