11package dev.c0redev.volter.ui.screens
22
3+ import android.graphics.Paint
4+ import android.graphics.RuntimeShader
5+ import android.os.Build
36import androidx.compose.animation.AnimatedVisibility
47import androidx.compose.animation.core.animateFloatAsState
8+ import androidx.compose.animation.core.LinearEasing
9+ import androidx.compose.animation.core.RepeatMode
10+ import androidx.compose.animation.core.animateFloat
11+ import androidx.compose.animation.core.infiniteRepeatable
12+ import androidx.compose.animation.core.rememberInfiniteTransition
513import androidx.compose.animation.core.tween
614import androidx.compose.animation.expandVertically
715import androidx.compose.animation.fadeIn
816import androidx.compose.animation.fadeOut
917import androidx.compose.animation.shrinkVertically
18+ import androidx.compose.foundation.background
1019import androidx.compose.foundation.clickable
20+ import androidx.compose.foundation.horizontalScroll
21+ import androidx.compose.foundation.layout.Box
1122import androidx.compose.foundation.layout.Arrangement
1223import androidx.compose.foundation.layout.Column
1324import androidx.compose.foundation.layout.PaddingValues
@@ -20,6 +31,7 @@ import androidx.compose.foundation.layout.padding
2031import androidx.compose.foundation.layout.size
2132import androidx.compose.foundation.lazy.LazyColumn
2233import androidx.compose.foundation.shape.RoundedCornerShape
34+ import androidx.compose.foundation.rememberScrollState
2335import androidx.compose.material.icons.Icons
2436import androidx.compose.material.icons.automirrored.outlined.ReceiptLong
2537import androidx.compose.material.icons.filled.CheckCircle
@@ -50,20 +62,53 @@ import androidx.compose.runtime.setValue
5062import androidx.compose.ui.Alignment
5163import androidx.compose.ui.Modifier
5264import androidx.compose.ui.draw.scale
65+ import androidx.compose.ui.draw.blur
66+ import androidx.compose.ui.draw.clip
67+ import androidx.compose.ui.graphics.Brush
68+ import androidx.compose.ui.graphics.Color
69+ import androidx.compose.ui.graphics.StrokeCap
70+ import androidx.compose.ui.graphics.nativeCanvas
71+ import androidx.compose.ui.graphics.toArgb
5372import androidx.compose.ui.graphics.vector.ImageVector
5473import androidx.compose.ui.hapticfeedback.HapticFeedbackType
74+ import androidx.compose.ui.platform.LocalDensity
5575import androidx.compose.ui.platform.LocalHapticFeedback
5676import androidx.compose.ui.res.stringArrayResource
5777import androidx.compose.ui.res.stringResource
5878import androidx.compose.ui.text.font.FontWeight
5979import androidx.compose.ui.text.style.TextAlign
80+ import androidx.compose.ui.geometry.Offset
81+ import androidx.compose.ui.graphics.drawscope.Stroke
82+ import androidx.compose.ui.graphics.drawscope.rotate
83+ import androidx.compose.ui.unit.Dp
6084import androidx.compose.ui.unit.dp
6185import dev.c0redev.volter.BuildConfig
6286import dev.c0redev.volter.R
6387import dev.c0redev.volter.theme.VolterSpacing
6488import dev.c0redev.volter.ui.ConnectionViewModel
6589import dev.c0redev.volter.ui.components.SectionCard
6690
91+ private const val ringShaderSrc = """
92+ uniform float2 resolution;
93+ uniform float time;
94+ uniform float4 colorA;
95+ uniform float4 colorB;
96+ uniform float4 colorC;
97+
98+ half4 main(float2 fragCoord) {
99+ float2 uv = fragCoord / max(resolution, float2(1.0));
100+ float2 p = uv - 0.5;
101+ float r = length(p);
102+ float angle = atan(p.y, p.x);
103+ float wave = sin(angle * 6.0 + time * 2.4) * 0.5 + 0.5;
104+ float wave2 = cos(angle * 10.0 - time * 1.8) * 0.5 + 0.5;
105+ half3 c = mix(colorA.rgb, colorB.rgb, wave);
106+ c = mix(c, colorC.rgb, wave2 * 0.45);
107+ float glow = smoothstep(0.48, 0.12, abs(r - 0.40));
108+ return half4(c * (0.65 + glow * 0.85), glow);
109+ }
110+ """
111+
67112private data class GuideEntry (
68113 val title : String ,
69114 val hint : String ,
@@ -171,16 +216,7 @@ fun HomeScreen(
171216 verticalAlignment = Alignment .CenterVertically ,
172217 horizontalArrangement = Arrangement .spacedBy(8 .dp),
173218 ) {
174- Icon (
175- imageVector = if (conn.connected) Icons .Default .CheckCircle else Icons .Default .Error ,
176- contentDescription = if (conn.connected) {
177- stringResource(R .string.home_status_connected_cd)
178- } else {
179- stringResource(R .string.home_status_disconnected_cd)
180- },
181- tint = if (conn.connected) MaterialTheme .colorScheme.secondary else MaterialTheme .colorScheme.onSurfaceVariant,
182- modifier = Modifier .size(24 .dp),
183- )
219+ ConnectionGlowIndicator (connected = conn.connected)
184220 Text (
185221 text = stringResource(R .string.home_status_title),
186222 style = MaterialTheme .typography.titleLarge,
@@ -283,6 +319,21 @@ fun HomeScreen(
283319 modifier = Modifier .padding(top = 4 .dp),
284320 )
285321 }
322+
323+ if (local.isNotEmpty()) {
324+ Spacer (modifier = Modifier .height(4 .dp))
325+ Text (
326+ text = " Серверы подключения" ,
327+ style = MaterialTheme .typography.labelLarge,
328+ color = MaterialTheme .colorScheme.onSurfaceVariant,
329+ )
330+ ServerPillRow (
331+ servers = local,
332+ activeName = activeProfile,
333+ connectingName = connectingName,
334+ onSelect = { name, cfg -> vm.connect(name, cfg) },
335+ )
336+ }
286337 }
287338 }
288339 }
@@ -331,6 +382,172 @@ fun HomeScreen(
331382 }
332383}
333384
385+ @Composable
386+ private fun ConnectionGlowIndicator (connected : Boolean , size : Dp = 26.dp) {
387+ val inf = rememberInfiniteTransition(label = " connGlow" )
388+ val angle by inf.animateFloat(
389+ initialValue = 0f ,
390+ targetValue = 360f ,
391+ animationSpec = infiniteRepeatable(animation = tween(3600 , easing = LinearEasing ), repeatMode = RepeatMode .Restart ),
392+ label = " connGlowAngle" ,
393+ )
394+ val base = if (connected) Color (0xFF8B5CF6 ) else MaterialTheme .colorScheme.onSurfaceVariant
395+ val glowAlpha = if (connected) 0.95f else 0.28f
396+ val ringWidth = with (LocalDensity .current) { 3 .dp.toPx() }
397+ Box (
398+ modifier = Modifier .size(size),
399+ contentAlignment = Alignment .Center ,
400+ ) {
401+ if (connected) {
402+ Box (
403+ modifier = Modifier
404+ .size(size + 6 .dp)
405+ .blur(8 .dp)
406+ .background(Color (0xFF8B5CF6 ).copy(alpha = 0.35f ), RoundedCornerShape (999 .dp)),
407+ )
408+ }
409+ if (connected && Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
410+ AgslRing (
411+ size = size,
412+ ringWidth = ringWidth,
413+ angle = angle,
414+ alpha = glowAlpha,
415+ )
416+ } else {
417+ androidx.compose.foundation.Canvas (modifier = Modifier .size(size)) {
418+ val sweep = Brush .sweepGradient(
419+ listOf (
420+ Color (0xFF8B5CF6 ),
421+ Color (0xFF6D28D9 ),
422+ Color (0xFFEC4899 ),
423+ Color (0xFF8B5CF6 ),
424+ ),
425+ center = center,
426+ )
427+ rotate(angle) {
428+ drawArc(
429+ brush = if (connected) sweep else Brush .linearGradient(listOf (base, base)),
430+ startAngle = 0f ,
431+ sweepAngle = 360f ,
432+ useCenter = false ,
433+ style = Stroke (width = ringWidth, cap = StrokeCap .Round ),
434+ topLeft = Offset .Zero ,
435+ size = this .size,
436+ alpha = glowAlpha,
437+ )
438+ }
439+ }
440+ }
441+ Box (
442+ modifier = Modifier
443+ .size(size - 10 .dp)
444+ .clip(RoundedCornerShape (999 .dp))
445+ .background(MaterialTheme .colorScheme.surface.copy(alpha = 0.9f )),
446+ contentAlignment = Alignment .Center ,
447+ ) {
448+ Icon (
449+ imageVector = if (connected) Icons .Default .CheckCircle else Icons .Default .Error ,
450+ contentDescription = null ,
451+ tint = if (connected) Color (0xFF8B5CF6 ) else MaterialTheme .colorScheme.onSurfaceVariant,
452+ modifier = Modifier .size(14 .dp),
453+ )
454+ }
455+ }
456+ }
457+
458+ @Composable
459+ private fun ServerPillRow (
460+ servers : List <dev.c0redev.volter.ui.ConfigItemState >,
461+ activeName : String? ,
462+ connectingName : String? ,
463+ onSelect : (String , dev.c0redev.volter.domain.model.Config ) -> Unit ,
464+ ) {
465+ val scroll = rememberScrollState()
466+ Row (
467+ modifier = Modifier
468+ .fillMaxWidth()
469+ .clip(RoundedCornerShape (999 .dp))
470+ .background(MaterialTheme .colorScheme.surfaceContainer.copy(alpha = 0.54f ))
471+ .horizontalScroll(scroll)
472+ .padding(horizontal = 8 .dp, vertical = 6 .dp),
473+ horizontalArrangement = Arrangement .spacedBy(8 .dp),
474+ ) {
475+ servers.forEach { item ->
476+ val selected = item.name == activeName || item.name == connectingName
477+ Box (
478+ modifier = Modifier
479+ .size(width = 122 .dp, height = 36 .dp)
480+ .clip(RoundedCornerShape (999 .dp))
481+ .background(
482+ if (selected) MaterialTheme .colorScheme.primaryContainer.copy(alpha = 0.9f )
483+ else MaterialTheme .colorScheme.surface.copy(alpha = 0.6f ),
484+ )
485+ .clickable { onSelect(item.name, item.config) }
486+ .padding(horizontal = 10 .dp, vertical = 8 .dp),
487+ contentAlignment = Alignment .Center ,
488+ ) {
489+ Text (
490+ text = item.name,
491+ style = MaterialTheme .typography.labelLarge,
492+ color = if (selected) MaterialTheme .colorScheme.onPrimaryContainer else MaterialTheme .colorScheme.onSurfaceVariant,
493+ fontWeight = if (selected) FontWeight .SemiBold else FontWeight .Medium ,
494+ )
495+ }
496+ }
497+ }
498+ }
499+
500+ @Composable
501+ private fun AgslRing (size : Dp , ringWidth : Float , angle : Float , alpha : Float ) {
502+ val c1 = MaterialTheme .colorScheme.primary
503+ val c2 = MaterialTheme .colorScheme.tertiary
504+ val c3 = MaterialTheme .colorScheme.secondary
505+ val shader = remember {
506+ runCatching { RuntimeShader (ringShaderSrc) }.getOrNull()
507+ }
508+ val a1 = c1.toArgb()
509+ val a2 = c2.toArgb()
510+ val a3 = c3.toArgb()
511+ if (shader == null ) {
512+ androidx.compose.foundation.Canvas (modifier = Modifier .size(size)) {
513+ val sweep = Brush .sweepGradient(listOf (c1, c2, c3, c1), center = center)
514+ rotate(angle) {
515+ drawArc(
516+ brush = sweep,
517+ startAngle = 0f ,
518+ sweepAngle = 360f ,
519+ useCenter = false ,
520+ style = Stroke (width = ringWidth, cap = StrokeCap .Round ),
521+ topLeft = Offset .Zero ,
522+ size = this .size,
523+ alpha = alpha,
524+ )
525+ }
526+ }
527+ return
528+ }
529+ androidx.compose.foundation.Canvas (modifier = Modifier .size(size)) {
530+ shader.setFloatUniform(" resolution" , this .size.width, this .size.height)
531+ shader.setFloatUniform(" time" , angle / 57.2958f )
532+ shader.setFloatUniform(" colorA" , ((a1 shr 16 ) and 0xFF ) / 255f , ((a1 shr 8 ) and 0xFF ) / 255f , (a1 and 0xFF ) / 255f , 1f )
533+ shader.setFloatUniform(" colorB" , ((a2 shr 16 ) and 0xFF ) / 255f , ((a2 shr 8 ) and 0xFF ) / 255f , (a2 and 0xFF ) / 255f , 1f )
534+ shader.setFloatUniform(" colorC" , ((a3 shr 16 ) and 0xFF ) / 255f , ((a3 shr 8 ) and 0xFF ) / 255f , (a3 and 0xFF ) / 255f , 1f )
535+ val p = Paint ().apply {
536+ isAntiAlias = true
537+ style = Paint .Style .STROKE
538+ strokeWidth = ringWidth
539+ strokeCap = Paint .Cap .ROUND
540+ this .alpha = (alpha * 255f ).toInt().coerceIn(0 , 255 )
541+ this .shader = shader
542+ }
543+ val native = drawContext.canvas.nativeCanvas
544+ native.save()
545+ native.rotate(angle, center.x, center.y)
546+ native.drawCircle(center.x, center.y, (this .size.minDimension * 0.5f ) - (ringWidth * 0.5f ), p)
547+ native.restore()
548+ }
549+ }
550+
334551@Composable
335552private fun StatusRow (label : String , value : String ) {
336553 Row (
0 commit comments