mirror of
https://github.com/permissionlesstech/bitchat-android.git
synced 2026-08-08 06:46:11 +00:00
button
This commit is contained in:
parent
32d5e6f1dc
commit
c0b1af3d0f
@ -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(
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user