This commit is contained in:
callebtc 2026-07-27 03:59:51 +02:00
parent 32d5e6f1dc
commit c0b1af3d0f
2 changed files with 100 additions and 33 deletions

View File

@ -7,7 +7,12 @@ import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.outlined.*
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
@ -24,6 +29,9 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.res.stringResource
@ -96,30 +104,91 @@ private fun HeaderIconButton(
}
/**
* Smooth tint for header glyphs that should reflect Tor connection state.
* Tor health for location-channel header glyphs.
*
* When Tor is off or fully bootstrapped, [normal] is used. While connecting the tint goes
* orange; if Tor is enabled but not running it goes red. Cross-fades so Bootstrap percent
* flips do not flash the whole header.
* Status colours are heavily muted (blended into [normal]) so they read as a soft signal
* rather than an alarm. Connecting / not-yet-running also drives a slow glow pulse.
*/
internal data class TorConnectionVisual(
val tint: Color,
/** True while Tor is enabled but not fully bootstrapped — drives a pulse. */
val isProgress: Boolean,
)
@Composable
internal fun torConnectionTint(normal: Color): Color {
internal fun rememberTorConnectionVisual(normal: Color): TorConnectionVisual {
val palette = LocalBitchatPalette.current
val torStatus by remember { ArtiTorManager.getInstance() }.statusFlow.collectAsState()
// ~28% of the loud accent mixed into the base tint keeps the hue without intensity.
val mutedConnecting = lerp(normal, palette.accentOrange, 0.28f)
val mutedFailed = lerp(normal, palette.accentRed, 0.30f)
val target = when {
torStatus.mode == TorMode.OFF -> normal
torStatus.running && torStatus.bootstrapPercent >= 100 -> normal
torStatus.running -> palette.accentOrange
else -> palette.accentRed
torStatus.mode == TorMode.OFF -> TorConnectionVisual(normal, isProgress = false)
torStatus.running && torStatus.bootstrapPercent >= 100 ->
TorConnectionVisual(normal, isProgress = false)
torStatus.running -> TorConnectionVisual(mutedConnecting, isProgress = true)
else -> TorConnectionVisual(mutedFailed, isProgress = true)
}
val animated by animateColorAsState(
targetValue = target,
val animatedTint by animateColorAsState(
targetValue = target.tint,
animationSpec = tween(BitchatMotion.STANDARD_MS, easing = FastOutSlowInEasing),
label = "torConnectionTint"
)
return animated
return TorConnectionVisual(tint = animatedTint, isProgress = target.isProgress)
}
/**
* Soft, slow brightness pulse used while Tor is connecting. Keeps scale fixed so layout
* does not shift; only opacity / a faint halo breathe.
*/
@Composable
internal fun TorAwareHeaderIcon(
imageVector: ImageVector,
tint: Color,
isProgress: Boolean,
contentDescription: String?,
modifier: Modifier = Modifier,
) {
val pulse = if (isProgress) {
val transition = rememberInfiniteTransition(label = "torGlow")
transition.animateFloat(
initialValue = 0.42f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1800, easing = FastOutSlowInEasing),
repeatMode = RepeatMode.Reverse
),
label = "torGlowPulse"
).value
} else {
1f
}
Box(contentAlignment = Alignment.Center, modifier = modifier) {
if (isProgress) {
// Soft halo behind the glyph — opacity tracks the pulse, never solid.
Box(
modifier = Modifier
.size(HeaderIconSize + 10.dp)
.graphicsLayer { alpha = pulse * 0.28f }
.background(tint.copy(alpha = 1f), CircleShape)
)
}
Icon(
imageVector = imageVector,
contentDescription = contentDescription,
modifier = Modifier
.size(HeaderIconSize)
.graphicsLayer {
// Pulse icon opacity gently when in progress; settle to full when idle.
alpha = if (isProgress) 0.55f + pulse * 0.45f else 1f
},
tint = tint
)
}
}
@Composable
@ -530,8 +599,12 @@ private fun LocationChannelsButton(
else -> stringResource(R.string.mesh_label)
}
val channelColor = if (isLocation) palette.accentGreen else palette.accentBlue
// Icon alone carries Tor health so the geohash / "mesh" label stays channel-coloured.
val iconColor = torConnectionTint(normal = channelColor)
// Tor status only tints the globe (location channels). Mesh Bluetooth stays blue.
val torVisual = if (isLocation) {
rememberTorConnectionVisual(normal = channelColor)
} else {
TorConnectionVisual(tint = channelColor, isProgress = false)
}
val badgeIcon = if (isLocation) Icons.Outlined.Public else Icons.Filled.Bluetooth
Row(
@ -545,11 +618,11 @@ private fun LocationChannelsButton(
// padding so the gap to PeerCounter matches other cluster separations.
.padding(start = 0.dp, end = 6.dp)
) {
Icon(
TorAwareHeaderIcon(
imageVector = badgeIcon,
contentDescription = stringResource(R.string.cd_tor_status),
modifier = Modifier.size(HeaderIconSize),
tint = iconColor
tint = torVisual.tint,
isProgress = torVisual.isProgress,
contentDescription = stringResource(R.string.cd_tor_status)
)
Text(

View File

@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -25,10 +24,9 @@ import com.bitchat.android.nostr.LocationNotesManager
import com.bitchat.android.ui.theme.LocalBitchatPalette
/**
* Location Notes button component for MainHeader
* Shows in mesh mode when location permission granted AND services enabled.
* Base tint is primary when notes exist, secondary otherwise; Tor health may
* override that color (orange connecting, red failed) via [torConnectionTint].
* Location Notes button for MainHeader.
* Mesh-only with location authorized. Tor health tints the glyph with muted colours
* and a slow glow while connecting (via [rememberTorConnectionVisual]).
*/
@Composable
fun LocationNotesButton(
@ -40,17 +38,14 @@ fun LocationNotesButton(
val palette = LocalBitchatPalette.current
val context = LocalContext.current
// Get channel and permission state
val selectedLocationChannel by viewModel.selectedLocationChannel.collectAsStateWithLifecycle()
val locationManager = remember { LocationChannelManager.getInstance(context) }
val permissionState by locationManager.permissionState.collectAsStateWithLifecycle()
val locationServicesEnabled by locationManager.effectiveLocationEnabled.collectAsStateWithLifecycle(false)
// Check both permission AND location services enabled
val locationPermissionGranted = permissionState == LocationChannelManager.PermissionState.AUTHORIZED
val locationEnabled = locationPermissionGranted && locationServicesEnabled
// Get notes count from LocationNotesManager
val notesManager = remember { LocationNotesManager.getInstance() }
val notes by notesManager.notes.collectAsStateWithLifecycle()
val notesCount = notes.size
@ -60,9 +55,8 @@ fun LocationNotesButton(
val hasNotes = notesCount > 0
val contentDescription = stringResource(R.string.cd_location_notes)
val normalTint = if (hasNotes) colorScheme.primary else palette.textSecondary
val tint = torConnectionTint(normal = normalTint)
// Match other header icon buttons: 44.dp target, no Material IconButton min-size padding
// that pushed the notes glyph farther from the mesh badge than sibling gaps.
val torVisual = rememberTorConnectionVisual(normal = normalTint)
Box(
modifier = modifier
.size(44.dp)
@ -70,11 +64,11 @@ fun LocationNotesButton(
.clickable(onClickLabel = contentDescription) { onClick() },
contentAlignment = Alignment.Center
) {
Icon(
TorAwareHeaderIcon(
imageVector = Icons.Outlined.Description,
contentDescription = contentDescription,
modifier = Modifier.size(HeaderIconSize),
tint = tint
tint = torVisual.tint,
isProgress = torVisual.isProgress,
contentDescription = contentDescription
)
}
}