Merge pull request #3420 from vitorpamplona/claude/browser-console-log-design-rq4x48

Console panel: add visibility toggle, elevation, and state sync
This commit is contained in:
Vitor Pamplona
2026-06-29 20:31:07 -04:00
committed by GitHub
4 changed files with 63 additions and 20 deletions
@@ -631,7 +631,7 @@ class NappletBrowserActivity : ComponentActivity() {
onInfo = null,
liveUrl = startUrl,
onNavigate = { loadAddress(it) },
onConsole = { consolePanel?.toggle() },
onConsole = { show -> consolePanel?.setShowing(show) },
isFavoriteInitially = intent.getBooleanExtra(EXTRA_IS_FAVORITE, false),
onFavoriteToggle = { url, _ -> sendFavoriteToggle(url) },
).also { controlSheet = it }
@@ -37,11 +37,17 @@ import androidx.core.content.ContextCompat
import com.vitorpamplona.amethyst.commons.R as CommonsR
/**
* The full-screen browser's **bottom pull-up sheet** for JavaScript console output. Collapsed it's
* a small grabber at the bottom edge, symmetric to [NappletControlSheet]'s top grabber. Pull it up
* (or tap) to reveal a scrollable log of [console.log / warn / error / debug] messages captured
* from the page via `WebChromeClient.onConsoleMessage`. Capped at [MAX_ENTRIES] entries (oldest
* dropped on overflow). Built in plain Views like [NappletControlSheet] — no Compose/Material.
* The full-screen browser's **bottom pull-up sheet** for JavaScript console output. The whole sheet is
* hidden until the user flips the Console **toggle** in [NappletControlSheet] ([setShowing]); turned on,
* it reveals a small grabber at the bottom edge (symmetric to that sheet's top grabber) already pulled
* up. Pull it down/up (or tap) to collapse/expand the scrollable log of
* [console.log / warn / error / debug] messages captured from the page via
* `WebChromeClient.onConsoleMessage`. Capped at [MAX_ENTRIES] entries (oldest dropped on overflow).
* Built in plain Views like [NappletControlSheet] — no Compose/Material.
*
* Its grabber + panel are elevated above [NappletControlSheet]'s panel so that, when both are open at
* once (e.g. in landscape), this bottom sheet draws on top of the top pull-down sheet — mirroring the
* Compose layer, where `BottomConsoleSheet` is composed after `TopControlSheet`.
*/
@SuppressLint("UseSwitchCompatOrMaterialCode")
class NappletConsolePanel(
@@ -52,6 +58,7 @@ class NappletConsolePanel(
private val surface = resolveThemeColor(android.R.attr.colorBackground)
private var expanded = false
private var showing = false
private val panel: LinearLayout
private lateinit var logContainer: LinearLayout
private lateinit var scrollView: ScrollView
@@ -59,6 +66,9 @@ class NappletConsolePanel(
init {
orientation = VERTICAL
gravity = Gravity.CENTER_HORIZONTAL
// Hidden until the Console toggle turns it on; matches the Compose `BottomConsoleSheet`, which is
// only composed while the toggle is on.
visibility = View.GONE
panel = buildPanel().also { addView(it) }
addView(buildGrabber())
@@ -68,7 +78,8 @@ class NappletConsolePanel(
LinearLayout(context).apply {
orientation = VERTICAL
visibility = View.GONE
elevation = dp(6).toFloat()
// Above NappletControlSheet's panel (6dp) so an open console draws over an open top sheet.
elevation = dp(8).toFloat()
background =
GradientDrawable().apply {
cornerRadii = floatArrayOf(dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), dp(16).toFloat(), 0f, 0f, 0f, 0f)
@@ -123,9 +134,21 @@ class NappletConsolePanel(
var entryCount: Int = 0
private set
/** Toggles the panel's expanded/collapsed state; used by the control sheet's Console row. */
fun toggle() {
if (expanded) collapse() else expand()
/**
* Shows or hides the entire sheet (grabber + log), driven by the control sheet's Console **toggle**:
* off hides everything, on reveals the sheet already pulled up — mirroring the Compose
* `BottomConsoleSheet`, which is only composed while the toggle is on and opens expanded.
*/
fun setShowing(show: Boolean) {
if (show == showing) return
showing = show
if (show) {
visibility = View.VISIBLE
expand()
} else {
collapse()
visibility = View.GONE
}
}
fun appendLog(
@@ -205,6 +228,8 @@ class NappletConsolePanel(
gravity = Gravity.CENTER_HORIZONTAL
}
setPadding(dp(16), dp(7), dp(16), dp(7))
// Above NappletControlSheet's panel (6dp) so the grabber stays on top of an open top sheet.
elevation = dp(8).toFloat()
background =
GradientDrawable().apply {
cornerRadii = floatArrayOf(dp(12).toFloat(), dp(12).toFloat(), dp(12).toFloat(), dp(12).toFloat(), 0f, 0f, 0f, 0f)
@@ -65,9 +65,9 @@ class NappletControlSheet(
// nsite/napplet), where it renders an editable address row; [onNavigate] loads what the user types.
liveUrl: String? = null,
private val onNavigate: ((String) -> Unit)? = null,
// When non-null, a "Console" row is added to the pull-down sheet. The callback toggles the
// browser's console log panel; the count label is updated via [updateConsoleCount].
private val onConsole: (() -> Unit)? = null,
// When non-null, a "Console" toggle row is added to the pull-down sheet. The callback is invoked with
// the new visibility each time the user flips it; the count label is updated via [updateConsoleCount].
private val onConsole: ((Boolean) -> Unit)? = null,
// When non-null, a favorite toggle row is shown; called with the current URL and new isFavorite state.
isFavoriteInitially: Boolean = false,
private val onFavoriteToggle: ((url: String, isFavorite: Boolean) -> Unit)? = null,
@@ -80,6 +80,7 @@ class NappletControlSheet(
private var torOn = torInitiallyOn
private var currentUrl = liveUrl
private var isFavorite = isFavoriteInitially
private var consoleShowing = false
private val panel: LinearLayout
private var torLabel: TextView? = null
@@ -87,6 +88,7 @@ class NappletControlSheet(
private var addressField: EditText? = null
private var securityGlyph: TextView? = null
private var consoleLabel: TextView? = null
private var consoleSwitch: Switch? = null
private var favoriteLabel: TextView? = null
init {
@@ -129,25 +131,32 @@ class NappletControlSheet(
},
)
}
onConsole?.let { console ->
onConsole?.let {
val label =
TextView(context).apply {
text = context.getString(CommonsR.string.browser_console_title_short)
setTextColor(onSurface)
textSize = 15f
setPadding(dp(8), 0, 0, 0)
// Weight 1 so the label fills and shoves the Switch to the end, like the Tor row.
layoutParams = LayoutParams(0, LayoutParams.WRAP_CONTENT, 1f)
}
consoleLabel = label
// Display-only switch (the whole row is the touch target), matching the Tor row + Compose twin.
val toggle =
Switch(context).apply {
isChecked = consoleShowing
isClickable = false
isFocusable = false
}
consoleSwitch = toggle
addView(
LinearLayout(context).apply {
orientation = HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
setPadding(dp(8), dp(10), dp(8), dp(10))
isClickable = true
setOnClickListener {
collapse()
console()
}
setOnClickListener { toggleConsole() }
addView(
TextView(context).apply {
text = ">"
@@ -155,10 +164,11 @@ class NappletControlSheet(
textSize = 18f
width = dp(28)
gravity = Gravity.CENTER
typeface = android.graphics.Typeface.MONOSPACE
typeface = Typeface.MONOSPACE
},
)
addView(label)
addView(toggle)
},
)
}
@@ -367,6 +377,14 @@ class NappletControlSheet(
onToggleTor(next)
}
private fun toggleConsole() {
consoleShowing = !consoleShowing
consoleSwitch?.isChecked = consoleShowing
// Collapse the top sheet on toggle, like the Compose twin, so the bottom console isn't hidden behind it.
collapse()
onConsole?.invoke(consoleShowing)
}
private fun actionRow(
glyph: String,
label: String,
@@ -838,7 +838,7 @@ class NappletHostActivity : ComponentActivity() {
torInitiallyOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null,
onNetworkTap = if (profile.exposesNetwork && proxyPort > 0) ({ setNetworkMode(!useTor) }) else null,
onInfo = { showAccessDialog() },
onConsole = { consolePanel?.toggle() },
onConsole = { show -> consolePanel?.setShowing(show) },
).also { controlSheet = it }
private fun buildConsolePanel(): View =