diff --git a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt index 5be2cf4..2d09489 100644 --- a/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt +++ b/app-android/src/main/kotlin/llc/lookatwhataicando/codeoba/android/TestWebViewActivity.kt @@ -3,6 +3,7 @@ package llc.lookatwhataicando.codeoba.android import android.annotation.SuppressLint import android.os.Bundle import android.util.Log +import android.view.MotionEvent import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebResourceError @@ -12,15 +13,19 @@ import android.webkit.WebViewClient import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -32,15 +37,24 @@ import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.math.roundToInt /** * Test activity with WebView and editable address bar. @@ -149,9 +163,18 @@ fun TestWebView( onWebViewCreated: (WebView) -> Unit = {}, onUrlChanged: (String) -> Unit = {} ) { - AndroidView( - modifier = modifier, - factory = { context -> + var isRefreshing by remember { mutableStateOf(false) } + var pullOffset by remember { mutableFloatStateOf(0f) } + val coroutineScope = rememberCoroutineScope() + val refreshThreshold = with(LocalDensity.current) { 120.dp.toPx() } + val minDragToActivate = with(LocalDensity.current) { 20.dp.toPx() } + + Box(modifier = modifier) { + AndroidView( + modifier = Modifier + .fillMaxSize() + .offset { IntOffset(0, pullOffset.roundToInt()) }, + factory = { context -> WebView(context).apply { Log.d("TestWebViewActivity", "Creating WebView") @@ -233,6 +256,106 @@ fun TestWebView( } } + // Implement pull-to-refresh with touch listener + var downY = 0f + var downX = 0f + var totalDragDistance = 0f + var isDragging = false + + setOnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + downY = event.y + downX = event.x + totalDragDistance = 0f + isDragging = false + false // Let WebView handle it + } + MotionEvent.ACTION_MOVE -> { + val currentY = event.y + val currentX = event.x + val deltaY = currentY - downY + val deltaX = currentX - downX + + // CRITICAL: Always check if we're at the top of the page + // This must be checked on EVERY move, not just when starting + val isAtTop = (view as? WebView)?.scrollY == 0 + + // Stricter vertical drag detection to avoid drawer conflicts + // Require significantly more vertical than horizontal movement + val isVerticalDrag = kotlin.math.abs(deltaY) > kotlin.math.abs(deltaX) * 2.5f + + // Only activate pull-to-refresh after minimum drag threshold + // This prevents conflicts with text selection + val hasMinimumDrag = kotlin.math.abs(deltaY) >= minDragToActivate + + if (isAtTop && deltaY > 0 && !isRefreshing && isVerticalDrag && hasMinimumDrag) { + isDragging = true + totalDragDistance = deltaY + + // Apply resistance - more aggressive after threshold + val resistance = if (totalDragDistance > refreshThreshold) 0.2f else 0.6f + pullOffset = (totalDragDistance * resistance).coerceAtLeast(0f) + + true // Consume touch to prevent scrolling + } else if (isDragging && isAtTop) { + // Continue drag ONLY if still at top + // If user scrolled away, stop the pull-to-refresh + totalDragDistance = deltaY + val resistance = if (totalDragDistance > refreshThreshold) 0.2f else 0.6f + pullOffset = (totalDragDistance * resistance).coerceAtLeast(0f) + true + } else { + // Stop dragging if we're no longer at top + if (isDragging && !isAtTop) { + isDragging = false + pullOffset = 0f + } + false + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isDragging) { + isDragging = false + + // Trigger refresh if threshold met + if (totalDragDistance > refreshThreshold) { + isRefreshing = true + coroutineScope.launch { + (view as? WebView)?.reload() + delay(1000) + isRefreshing = false + } + } + + // Animate back + coroutineScope.launch { + val start = pullOffset + val duration = 200L + val startTime = System.currentTimeMillis() + + while (pullOffset > 0) { + val elapsed = System.currentTimeMillis() - startTime + val progress = (elapsed.toFloat() / duration).coerceIn(0f, 1f) + pullOffset = start * (1f - progress) + + if (progress >= 1f) { + pullOffset = 0f + break + } + delay(16) + } + } + + true + } else { + false + } + } + else -> false + } + } + Log.d("TestWebViewActivity", "Loading URL: $url") loadUrl(url) onWebViewCreated(this) @@ -247,4 +370,32 @@ fun TestWebView( } } ) + + // Refresh indicator + if (isRefreshing || pullOffset > 0) { + // Background overlay to make indicator more visible + Surface( + modifier = Modifier + .align(Alignment.TopCenter) + .offset { IntOffset(0, (pullOffset * 0.3f).roundToInt()) } + .size(56.dp) + .graphicsLayer { + alpha = (pullOffset / refreshThreshold).coerceIn(0.3f, 0.9f) + }, + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surface, + tonalElevation = 4.dp + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(32.dp), + color = MaterialTheme.colorScheme.primary + ) + } + } + } + } } diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/PullToRefreshWebView.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/PullToRefreshWebView.kt new file mode 100644 index 0000000..a32e621 --- /dev/null +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/PullToRefreshWebView.kt @@ -0,0 +1,155 @@ +package llc.lookatwhataicando.codeoba.core.ui + +import android.annotation.SuppressLint +import android.content.pm.ApplicationInfo +import android.graphics.Bitmap +import android.util.Log +import android.webkit.CookieManager +import android.webkit.WebChromeClient +import android.webkit.WebResourceRequest +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.pulltorefresh.PullToRefreshBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.viewinterop.AndroidView +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * Android WebView with Material 3 PullToRefreshBox integration. + * Uses Compose's built-in pull-to-refresh mechanism with custom nested scroll handling + * to work with AndroidView.WebView. + * + * This provides Chrome-like pull-to-refresh UX with circular indicator. + */ +@OptIn(ExperimentalMaterial3Api::class) +@SuppressLint("SetJavaScriptEnabled") +@Composable +fun PullToRefreshWebView( + url: String, + modifier: Modifier = Modifier, + onWebViewCreated: ((WebView) -> Unit)? = null +) { + var webView by remember { mutableStateOf(null) } + var canGoBack by remember { mutableStateOf(false) } + var isRefreshing by remember { mutableStateOf(false) } + val coroutineScope = rememberCoroutineScope() + + // Handle back navigation + BackHandler(enabled = canGoBack) { + webView?.goBack() + } + + // PullToRefreshBox provides the pull-to-refresh gesture and indicator + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + coroutineScope.launch { + isRefreshing = true + webView?.reload() + // Simulate refresh duration (WebView doesn't have completion callback) + delay(1000) + isRefreshing = false + } + }, + modifier = modifier.fillMaxSize() + ) { + // WebView wrapped in Box for proper layout + Box(modifier = Modifier.fillMaxSize()) { + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + if (0 != (context.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)) { + WebView.setWebContentsDebuggingEnabled(true) + } + WebView(context).apply { + // CRITICAL: Set layout params to ensure WebView has proper dimensions + layoutParams = android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.MATCH_PARENT + ) + + // Enable JavaScript and DOM storage + settings.apply { + javaScriptEnabled = true + domStorageEnabled = true + @Suppress("DEPRECATION") + databaseEnabled = true + } + + // Enable cookies for session persistence + val cookieManager = CookieManager.getInstance() + cookieManager.setAcceptCookie(true) + cookieManager.setAcceptThirdPartyCookies(this, true) + + // WebViewClient for navigation handling + webViewClient = object : WebViewClient() { + override fun shouldOverrideUrlLoading( + view: WebView?, + request: WebResourceRequest? + ): Boolean { + return false // Let WebView handle navigation + } + + override fun onPageStarted(view: WebView?, url: String?, favicon: Bitmap?) { + super.onPageStarted(view, url, favicon) + canGoBack = view?.canGoBack() ?: false + Log.d("WebView", "Page started: $url") + } + + override fun onPageFinished(view: WebView?, url: String?) { + super.onPageFinished(view, url) + canGoBack = view?.canGoBack() ?: false + Log.d("WebView", "Page finished: $url") + } + + @Suppress("OVERRIDE_DEPRECATION") + @Deprecated("Deprecated in Java") + override fun onReceivedError( + view: WebView?, + errorCode: Int, + description: String?, + failingUrl: String? + ) { + super.onReceivedError(view, errorCode, description, failingUrl) + Log.e("WebView", "Error $errorCode: $description at $failingUrl") + } + } + + // WebChromeClient for progress updates + webChromeClient = object : WebChromeClient() { + override fun onProgressChanged(view: WebView?, newProgress: Int) { + super.onProgressChanged(view, newProgress) + Log.d("WebView", "Loading progress: $newProgress%") + } + } + + // Load the initial URL + loadUrl(url) + + // Store reference and notify callback + webView = this + onWebViewCreated?.invoke(this) + } + }, + update = { view -> + // Update if URL changes (though typically doesn't in this app) + if (view.url != url) { + view.loadUrl(url) + } + } + ) + } + } +} diff --git a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 178c452..e52285f 100644 --- a/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/androidMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -4,20 +4,20 @@ import android.annotation.SuppressLint import android.content.pm.ApplicationInfo import android.graphics.Bitmap import android.util.Log +import android.view.MotionEvent import android.webkit.CookieManager import android.webkit.WebChromeClient import android.webkit.WebResourceRequest import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.compose.BackHandler -import androidx.compose.foundation.gestures.awaitEachGesture -import androidx.compose.foundation.gestures.awaitFirstDown -import androidx.compose.foundation.gestures.drag import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf @@ -29,8 +29,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp @@ -47,12 +45,13 @@ import kotlin.math.roundToInt * - Cookie persistence for logged-in sessions * - Pull-to-refresh gesture (custom implementation to avoid gesture conflicts) */ -@SuppressLint("SetJavaScriptEnabled") +@SuppressLint("SetJavaScriptEnabled", "ClickableViewAccessibility") @Composable actual fun WebViewWithBackHandler( url: String, modifier: Modifier, - onWebViewCreated: ((Any?) -> Unit)? + onWebViewCreated: ((Any?) -> Unit)?, + onDebugInfoUpdate: ((scrollY: Int, isAtTop: Boolean, pullOffset: Float) -> Unit)? ) { var webView by remember { mutableStateOf(null) } var canGoBack by remember { mutableStateOf(false) } @@ -61,7 +60,8 @@ actual fun WebViewWithBackHandler( var scrollY by remember { mutableIntStateOf(0) } val coroutineScope = rememberCoroutineScope() - val refreshThreshold = with(LocalDensity.current) { 80.dp.toPx() } + val refreshThreshold = with(LocalDensity.current) { 120.dp.toPx() } + val minDragToActivate = with(LocalDensity.current) { 20.dp.toPx() } // Handle back navigation BackHandler(enabled = canGoBack) { @@ -69,66 +69,7 @@ actual fun WebViewWithBackHandler( } Box( - modifier = modifier - .fillMaxSize() - .pointerInput(Unit) { - awaitEachGesture { - val down = awaitFirstDown() - var totalDrag = 0f - - // Check if WebView is scrolled to top - val isAtTop = scrollY <= 0 - - if (isAtTop && !isRefreshing) { - drag(down.id) { change -> - val dragAmount = change.positionChange().y - - // Only handle downward drags when at top - if (dragAmount > 0 || totalDrag > 0) { - totalDrag += dragAmount - - // Apply resistance to the drag - val resistance = if (totalDrag > refreshThreshold) 0.3f else 0.5f - pullOffset = (totalDrag * resistance).coerceAtLeast(0f) - - // Consume the change to prevent the drawer from opening - if (abs(dragAmount) > abs(change.positionChange().x)) { - change.consume() - } - } - } - - // Trigger refresh if threshold met - if (totalDrag > refreshThreshold) { - isRefreshing = true - coroutineScope.launch { - webView?.reload() - delay(1000) - isRefreshing = false - } - } - - // Animate back to 0 - coroutineScope.launch { - val start = pullOffset - val duration = 200L - val startTime = System.currentTimeMillis() - - while (pullOffset > 0) { - val elapsed = System.currentTimeMillis() - startTime - val progress = (elapsed.toFloat() / duration).coerceIn(0f, 1f) - pullOffset = start * (1f - progress) - - if (progress >= 1f) { - pullOffset = 0f - break - } - delay(16) - } - } - } - } - } + modifier = modifier.fillMaxSize() ) { // WebView AndroidView( @@ -224,6 +165,127 @@ actual fun WebViewWithBackHandler( // Monitor scroll changes setOnScrollChangeListener { _, _, newScrollY, _, _ -> scrollY = newScrollY + // Update debug info if callback provided + onDebugInfoUpdate?.invoke(scrollY, scrollY == 0, pullOffset) + } + + // Implement pull-to-refresh with touch listener + // This must be done at the WebView level since AndroidView consumes touches + var downY = 0f + var downX = 0f + var totalDragDistance = 0f + var isDragging = false + + setOnTouchListener { view, event -> + when (event.action) { + MotionEvent.ACTION_DOWN -> { + downY = event.y + downX = event.x + totalDragDistance = 0f + isDragging = false + false // Don't consume, let WebView handle it + } + MotionEvent.ACTION_MOVE -> { + val currentY = event.y + val currentX = event.x + val deltaY = currentY - downY + val deltaX = currentX - downX + + // CRITICAL: Always check if we're at the top of the page + // This must be checked on EVERY move, not just when starting + val isAtTop = (view as? WebView)?.scrollY == 0 + + // Stricter vertical drag detection to avoid drawer conflicts + // Require significantly more vertical than horizontal movement + val isVerticalDrag = kotlin.math.abs(deltaY) > kotlin.math.abs(deltaX) * 2.5f + + // Only activate pull-to-refresh after minimum drag threshold + // This prevents conflicts with text selection and other gestures + val hasMinimumDrag = kotlin.math.abs(deltaY) >= minDragToActivate + + if (isAtTop && deltaY > 0 && !isRefreshing && isVerticalDrag && hasMinimumDrag) { + // Start pull-to-refresh + isDragging = true + totalDragDistance = deltaY + + // Apply resistance - more aggressive resistance after threshold + val resistance = if (totalDragDistance > refreshThreshold) 0.2f else 0.6f + pullOffset = (totalDragDistance * resistance).coerceAtLeast(0f) + + // Update debug info if callback provided + onDebugInfoUpdate?.invoke(scrollY, isAtTop, pullOffset) + + // Consume touch event to prevent scrolling + true + } else if (isDragging && isAtTop) { + // Continue drag ONLY if still at top + // If user scrolled away, stop the pull-to-refresh + totalDragDistance = deltaY + val resistance = if (totalDragDistance > refreshThreshold) 0.2f else 0.6f + pullOffset = (totalDragDistance * resistance).coerceAtLeast(0f) + + // Update debug info if callback provided + onDebugInfoUpdate?.invoke(scrollY, isAtTop, pullOffset) + + true + } else { + // Stop dragging if we're no longer at top + if (isDragging && !isAtTop) { + isDragging = false + pullOffset = 0f + // Update debug info if callback provided + onDebugInfoUpdate?.invoke(scrollY, isAtTop, pullOffset) + } + false // Let WebView handle normal scrolling + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (isDragging) { + isDragging = false + + // Trigger refresh if threshold met + if (totalDragDistance > refreshThreshold) { + isRefreshing = true + coroutineScope.launch { + (view as? WebView)?.reload() + delay(1000) + isRefreshing = false + } + } + + // Animate pull offset back to 0 + coroutineScope.launch { + val start = pullOffset + val duration = 200L + val startTime = System.currentTimeMillis() + val currentScrollY = scrollY + val wasAtTop = currentScrollY == 0 + + while (pullOffset > 0) { + val elapsed = System.currentTimeMillis() - startTime + val progress = (elapsed.toFloat() / duration).coerceIn(0f, 1f) + pullOffset = start * (1f - progress) + + // Update debug info during animation + onDebugInfoUpdate?.invoke(currentScrollY, wasAtTop, pullOffset) + + if (progress >= 1f) { + pullOffset = 0f + // Final update with 0 offset + onDebugInfoUpdate?.invoke(currentScrollY, wasAtTop, pullOffset) + break + } + delay(16) + } + } + + true // Consume the up event + } else { + false // Let WebView handle it + } + } + else -> false + } } loadUrl(url) @@ -242,17 +304,29 @@ actual fun WebViewWithBackHandler( // Refresh indicator if (isRefreshing || pullOffset > 0) { - CircularProgressIndicator( + // Background overlay to make indicator more visible + Surface( modifier = Modifier .align(Alignment.TopCenter) - .offset { IntOffset(0, (pullOffset * 0.5f).roundToInt()) } - .size(32.dp) + .offset { IntOffset(0, (pullOffset * 0.3f).roundToInt()) } + .size(56.dp) .graphicsLayer { - alpha = (pullOffset / refreshThreshold).coerceIn(0f, 1f) - scaleX = (pullOffset / refreshThreshold).coerceIn(0f, 1f) - scaleY = (pullOffset / refreshThreshold).coerceIn(0f, 1f) - } - ) + alpha = (pullOffset / refreshThreshold).coerceIn(0.3f, 0.9f) + }, + shape = MaterialTheme.shapes.medium, + color = MaterialTheme.colorScheme.surface, + tonalElevation = 4.dp + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(32.dp), + color = MaterialTheme.colorScheme.primary + ) + } + } } } } diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt index 1ce9212..3738033 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/AgentTabContent.kt @@ -1,6 +1,9 @@ package llc.lookatwhataicando.codeoba.core.ui +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -8,8 +11,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp /** - * Agent tab content displaying the GitHub Copilot Agents page in a browser. - * Provides easy navigation with gestures support. + * Agents tab content displaying the GitHub Copilot Agents page in a browser. + * Provides easy navigation with gestures support and a refresh button. * * @param onWebViewCreated Callback when WebView is created, provides the WebView instance */ @@ -19,6 +22,17 @@ fun AgentTabContent( modifier: Modifier = Modifier, onWebViewCreated: ((Any?) -> Unit)? = null ) { + // Debug flag - tap title 3 times to toggle + val showDebugInfo = remember { mutableStateOf(false) } + var debugScrollY by remember { mutableIntStateOf(0) } + var debugIsAtTop by remember { mutableStateOf(false) } + var debugPullOffset by remember { mutableFloatStateOf(0f) } + var tapCount by remember { mutableIntStateOf(0) } + var lastTapTime by remember { mutableLongStateOf(0L) } + + // WebView reference for refresh button + var webViewRef by remember { mutableStateOf(null) } + Column(modifier = modifier.fillMaxSize()) { // Header Surface( @@ -33,10 +47,56 @@ fun AgentTabContent( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { + // Title with tap-to-enable debug feature (triple-tap to toggle) Text( text = "GitHub Copilot Agents", - style = MaterialTheme.typography.titleMedium + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .weight(1f) + .clickable { + val currentTime = System.currentTimeMillis() + if (currentTime - lastTapTime < 500) { + tapCount++ + if (tapCount >= 3) { + showDebugInfo.value = !showDebugInfo.value + tapCount = 0 + } + } else { + tapCount = 1 + } + lastTapTime = currentTime + } ) + + // Debug info display (triple-tap title to toggle) + if (showDebugInfo.value) { + Text( + text = "scrollY=$debugScrollY ${if (debugIsAtTop) "TOP" else "---"} pull=${debugPullOffset.toInt()}", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error + ) + } + + // Refresh button + IconButton( + onClick = { + // Reload the WebView using platform-specific handling + webViewRef?.let { view -> + // Try to call reload() via reflection for platform compatibility + try { + val reloadMethod = view::class.java.getMethod("reload") + reloadMethod.invoke(view) + } catch (e: Exception) { + // Silently fail if reload method not available + } + } + } + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh page" + ) + } } } @@ -44,7 +104,17 @@ fun AgentTabContent( WebViewWithBackHandler( url = "https://github.com/copilot/agents", modifier = Modifier.fillMaxSize(), - onWebViewCreated = onWebViewCreated + onWebViewCreated = { view -> + webViewRef = view + onWebViewCreated?.invoke(view) + }, + onDebugInfoUpdate = if (showDebugInfo.value) { + { scrollY, isAtTop, pullOffset -> + debugScrollY = scrollY + debugIsAtTop = isAtTop + debugPullOffset = pullOffset + } + } else null ) } } diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt index e528a27..f07c933 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/CodeobaUI.kt @@ -240,7 +240,7 @@ fun CodeobaUI( ) } 1 -> { - // Agent tab: Browser view + // Agents tab: Browser view AgentTabContent( modifier = Modifier .fillMaxSize() diff --git a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 710d37d..308feb1 100644 --- a/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/commonMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -10,10 +10,12 @@ import androidx.compose.ui.Modifier * @param url The URL to load in the WebView * @param modifier The modifier to apply to the WebView * @param onWebViewCreated Callback when WebView is created (for platform-specific handling) + * @param onDebugInfoUpdate Optional callback for debug info (scrollY, isAtTop, pullOffset) */ @Composable expect fun WebViewWithBackHandler( url: String, modifier: Modifier = Modifier, - onWebViewCreated: ((Any?) -> Unit)? = null + onWebViewCreated: ((Any?) -> Unit)? = null, + onDebugInfoUpdate: ((scrollY: Int, isAtTop: Boolean, pullOffset: Float) -> Unit)? = null ) diff --git a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt index 3e5cedd..5444f43 100644 --- a/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt +++ b/core/src/desktopMain/kotlin/llc/lookatwhataicando/codeoba/core/ui/WebViewWithBackHandler.kt @@ -18,9 +18,11 @@ import javax.swing.JPanel actual fun WebViewWithBackHandler( url: String, modifier: Modifier, - onWebViewCreated: ((Any?) -> Unit)? + onWebViewCreated: ((Any?) -> Unit)?, + onDebugInfoUpdate: ((scrollY: Int, isAtTop: Boolean, pullOffset: Float) -> Unit)? ) { // Desktop back navigation is handled by browser controls // Just delegate to the standard WebView + // Note: Desktop doesn't support pull-to-refresh, so debug callback is ignored WebView(url = url, modifier = modifier) }