diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt index 67c5285..f7132f5 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/MainActivity.kt @@ -50,7 +50,7 @@ class MainActivity : ComponentActivity() { if (data != null) { try { startScreenCaptureService(data) - startFloatingUI() + // FloatingUI now handled by service overlay isCapturing = true Log.d(TAG, "Screen capture service started successfully") } catch (e: Exception) { diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt index 0e642f6..1851da9 100644 --- a/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/ScreenCaptureService.kt @@ -23,7 +23,7 @@ import android.widget.Button import android.widget.LinearLayout import androidx.core.app.NotificationCompat import com.quillstudios.pokegoalshelper.controllers.DetectionController -// FloatingActionButtonUI no longer used - UI handled by FloatingUIActivity +import com.quillstudios.pokegoalshelper.ui.FloatingComposeOverlay import org.opencv.android.Utils import org.opencv.core.* import org.opencv.imgproc.Imgproc @@ -114,6 +114,7 @@ class ScreenCaptureService : Service() { // MVC Components private lateinit var detectionController: DetectionController + private var floatingOverlay: FloatingComposeOverlay? = null private val handler = Handler(Looper.getMainLooper()) private var captureInterval = 2000L // Capture every 2 seconds @@ -166,6 +167,15 @@ class ScreenCaptureService : Service() { detectionController = DetectionController(yoloDetector!!) detectionController.setDetectionRequestCallback { triggerManualDetection() } + // Initialize floating overlay + floatingOverlay = FloatingComposeOverlay( + context = this, + onDetectionRequested = { triggerDetection() }, + onClassFilterRequested = { className -> setClassFilter(className) }, + onDebugToggled = { toggleDebugMode() }, + onClose = { stopSelf() } + ) + Log.d(TAG, "✅ MVC architecture initialized") } @@ -311,7 +321,8 @@ class ScreenCaptureService : Service() { } Log.d(TAG, "Screen capture setup complete") - // UI will be shown by FloatingUIActivity + // Show floating overlay + floatingOverlay?.show() } catch (e: Exception) { Log.e(TAG, "Error starting screen capture", e) @@ -324,7 +335,7 @@ class ScreenCaptureService : Service() { handler.removeCallbacks(captureRunnable) hideDetectionOverlay() - // UI is handled by FloatingUIActivity + floatingOverlay?.hide() latestImage?.close() latestImage = null virtualDisplay?.release() @@ -1220,7 +1231,7 @@ class ScreenCaptureService : Service() { override fun onDestroy() { super.onDestroy() hideDetectionOverlay() - // UI is handled by FloatingUIActivity + floatingOverlay?.hide() detectionController.clearUICallbacks() yoloDetector?.release() ocrExecutor.shutdown() diff --git a/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt new file mode 100644 index 0000000..555e7b5 --- /dev/null +++ b/app/src/main/java/com/quillstudios/pokegoalshelper/ui/FloatingComposeOverlay.kt @@ -0,0 +1,351 @@ +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 + 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 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 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