From ebfd1ea97eff0a77ae02985aa1f4da516e1693a0 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 00:44:42 +0530 Subject: [PATCH 01/20] UI: updated spinner and PullToRefresh anim, corner radius in systemd page, and minor tweaks --- .../app/ui/component/AccentColorPicker.kt | 5 +- .../app/ui/component/FilePickerDialog.kt | 4 +- .../app/ui/component/PullToRefreshWrapper.kt | 71 +- .../app/ui/component/RootfsRepoSheet.kt | 4 +- .../ui/navigation/DroidspacesNavigation.kt | 2 +- .../app/ui/screen/ContainerTerminalScreen.kt | 4 +- .../app/ui/screen/InitServiceScreen.kt | 2 +- .../app/ui/screen/RequirementsScreen.kt | 4 +- .../app/ui/util/LoadingIndicator.kt | 693 +++++++++++++++++- 9 files changed, 756 insertions(+), 33 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/AccentColorPicker.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/AccentColorPicker.kt index c5f30518..5abdde35 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/AccentColorPicker.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/AccentColorPicker.kt @@ -60,10 +60,11 @@ fun AccentColorPicker( LazyRow( modifier = Modifier .fillMaxWidth() - .padding(bottom = 8.dp), + .padding(bottom = 20.dp), contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + userScrollEnabled = false ) { items( items = ThemePalette.entries.toList(), diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/FilePickerDialog.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/FilePickerDialog.kt index fec89fcf..b22eab2c 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/FilePickerDialog.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/FilePickerDialog.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontWeight +import com.droidspaces.app.ui.util.LoadingIndicator +import com.droidspaces.app.ui.util.LoadingSize import com.droidspaces.app.ui.theme.JetBrainsMono import com.droidspaces.app.R import androidx.compose.ui.text.input.ImeAction @@ -236,7 +238,7 @@ fun FilePickerDialog( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + LoadingIndicator(size = LoadingSize.Medium) } } else { LazyColumn( diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/PullToRefreshWrapper.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/PullToRefreshWrapper.kt index 9de5be5e..fafab311 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/PullToRefreshWrapper.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/PullToRefreshWrapper.kt @@ -1,5 +1,8 @@ package com.droidspaces.app.ui.component +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.layout.* import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -8,8 +11,12 @@ import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.unit.dp +import com.droidspaces.app.ui.util.LoadingIndicator import kotlinx.coroutines.delay /** @@ -20,6 +27,8 @@ import kotlinx.coroutines.delay * - Hardware-accelerated indicator with graphicsLayer * - Material You theming integration * - No redundant state management (removed unused triggerRefresh) + * - Smooth spring animation on release — indicator slides to resting + * position instead of teleporting (fixes the jump-on-release bug) * * Performance characteristics: * - 0 allocations in hot path @@ -54,6 +63,23 @@ fun PullToRefreshWrapper( } } + // Smooth spring animation for the indicator's vertical position. + // + // M3's PullToRefreshContainer internally uses an Animatable for verticalOffset, + // but in some BOM versions the Animatable snaps (instead of animating) when + // isRefreshing flips to true — causing the visible "jump". We work around this + // by reading state.verticalOffset and re-applying it through animateFloatAsState + // with a spring so the transition from any pull distance to the resting position + // is always a smooth slide. + val animatedOffset by animateFloatAsState( + targetValue = pullToRefreshState.verticalOffset, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ), + label = "pullToRefreshOffset" + ) + Box( modifier = modifier .fillMaxSize() @@ -61,17 +87,56 @@ fun PullToRefreshWrapper( ) { content() - // Hardware-accelerated refresh indicator + // Hardware-accelerated refresh indicator with smooth release animation. + // We override the vertical position with our spring-animated offset so that + // releasing the pull always produces a smooth slide rather than a teleport. PullToRefreshContainer( state = pullToRefreshState, modifier = Modifier .align(Alignment.TopCenter) .graphicsLayer { - // Enable hardware layer for smooth 60fps animation + // Replace the container's own offset with our smooth animated value. + // The container positions itself at y=0 (top), so we shift it down + // by the animated offset to match where the finger dragged to, then + // let the spring bring it back to the resting position smoothly. + translationY = animatedOffset - pullToRefreshState.verticalOffset shadowElevation = 0f }, containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.primary + contentColor = MaterialTheme.colorScheme.primary, + indicator = { state -> + val progress = state.progress + val isRefreshing = state.isRefreshing + + Box( + modifier = Modifier.size(40.dp), + contentAlignment = Alignment.Center + ) { + if (isRefreshing) { + LoadingIndicator( + modifier = Modifier.size(24.dp), + color = MaterialTheme.colorScheme.primary + ) + } else { + LoadingIndicator( + progress = { progress }, + modifier = Modifier + .size(24.dp) + .drawWithContent { + if (progress > 1f) { + // Rotate the entire shape-morphing path as the pull continues past 1.0 + rotate(-(progress - 1) * 180) { + this@drawWithContent.drawContent() + } + } else { + drawContent() + } + }, + color = MaterialTheme.colorScheme.primary + ) + } + } + } ) } } diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt index 3b9f4d39..650520ea 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt @@ -36,6 +36,8 @@ import androidx.lifecycle.viewmodel.compose.viewModel import com.droidspaces.app.R import com.droidspaces.app.ui.util.ClearFocusOnClickOutside import com.droidspaces.app.ui.util.FocusUtils +import com.droidspaces.app.ui.util.LoadingIndicator +import com.droidspaces.app.ui.util.LoadingSize import com.droidspaces.app.ui.viewmodel.AssetDownloadState import com.droidspaces.app.ui.viewmodel.RepoUiState import com.droidspaces.app.ui.viewmodel.RootfsRepoViewModel @@ -217,7 +219,7 @@ private fun RepoLoadingContent() { .height(240.dp), contentAlignment = Alignment.Center ) { - CircularProgressIndicator() + LoadingIndicator(size = LoadingSize.Medium) } } diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt b/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt index 2c268434..595719fa 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/navigation/DroidspacesNavigation.kt @@ -498,7 +498,7 @@ fun DroidspacesNavigation( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - LoadingIndicator() + LoadingIndicator(size = LoadingSize.Medium) } } else { containerInfo?.let { container -> diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt index ff996bb1..939b022e 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt @@ -41,6 +41,8 @@ import com.droidspaces.app.ui.terminal.virtualkeys.VirtualKeysListener import com.droidspaces.app.ui.terminal.virtualkeys.VirtualKeysView import com.droidspaces.app.util.AnimationUtils import com.droidspaces.app.util.ContainerOSInfoManager +import com.droidspaces.app.ui.util.LoadingIndicator +import com.droidspaces.app.ui.util.LoadingSize import com.termux.terminal.TerminalSession import com.termux.view.TerminalView import java.lang.ref.WeakReference @@ -279,7 +281,7 @@ fun ContainerTerminalScreen( ) { if (binder == null || tabs.isEmpty()) { Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() + LoadingIndicator(size = LoadingSize.Medium) } } else { tabs.forEach { tab -> diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/InitServiceScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/InitServiceScreen.kt index 8ed199ee..a19ededa 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/InitServiceScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/InitServiceScreen.kt @@ -467,7 +467,7 @@ private fun InitServiceCard( Surface( modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(12.dp), + shape = RoundedCornerShape(20.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)) ) { Row(modifier = Modifier.fillMaxWidth().padding(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp)) { diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt index b3f427d6..bfd619c9 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/RequirementsScreen.kt @@ -49,6 +49,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import androidx.compose.runtime.rememberCoroutineScope import com.droidspaces.app.ui.util.showSuccess +import com.droidspaces.app.ui.util.LoadingIndicator @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -522,9 +523,8 @@ private fun CheckRequirementsButton( horizontalArrangement = Arrangement.Center ) { if (isRunning) { - CircularProgressIndicator( + LoadingIndicator( modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, color = MaterialTheme.colorScheme.onPrimary ) } else { diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/util/LoadingIndicator.kt b/Android/app/src/main/java/com/droidspaces/app/ui/util/LoadingIndicator.kt index c0a2bb16..70a2b126 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/util/LoadingIndicator.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/util/LoadingIndicator.kt @@ -1,15 +1,69 @@ package com.droidspaces.app.ui.util +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.AnimationEndReason +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.RepeatMode +import androidx.compose.animation.core.infiniteRepeatable +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.progressSemantics +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Matrix +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.platform.InfiniteAnimationPolicy +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.fastForEach +import androidx.compose.ui.util.fastMap +import androidx.graphics.shapes.CornerRounding +import androidx.graphics.shapes.Cubic +import androidx.graphics.shapes.Morph +import androidx.graphics.shapes.RoundedPolygon +import androidx.graphics.shapes.TransformResult +import androidx.graphics.shapes.circle +import androidx.graphics.shapes.rectangle +import androidx.graphics.shapes.star +import kotlin.math.PI +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.sin +import kotlin.math.max +import kotlin.math.min +import kotlinx.coroutines.async +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +// ── COMPATIBILITY & DROIDSPACES CONVENIENCE WRAAPERS ─────────────────────── /** * Standardized loading indicator sizes. @@ -21,32 +75,16 @@ enum class LoadingSize(val size: Dp, val strokeWidth: Dp) { } /** - * Standardized loading indicator component. + * Standardized loading indicator component (Convenience compatibility wrapper). */ @Composable fun LoadingIndicator( - size: LoadingSize = LoadingSize.Medium, + size: LoadingSize, modifier: Modifier = Modifier, - color: androidx.compose.ui.graphics.Color? = null + color: Color? = null ) { - CircularProgressIndicator( + LoadingIndicator( modifier = modifier.size(size.size), - strokeWidth = size.strokeWidth, - color = color ?: MaterialTheme.colorScheme.primary - ) -} - -/** - * Small loading indicator with custom modifier (for inline use). - */ -@Composable -fun LoadingIndicator( - modifier: Modifier, - color: androidx.compose.ui.graphics.Color? = null -) { - CircularProgressIndicator( - modifier = modifier, - strokeWidth = LoadingSize.Small.strokeWidth, color = color ?: MaterialTheme.colorScheme.primary ) } @@ -80,3 +118,616 @@ fun FullScreenLoading( } } +// ── OFFICIAL GOOGLE MATERIAL 3 EXPRESSIVE APIs ────────────────────────────── + +/** + * A Material Design loading indicator. + * + * This version of the loading indicator morphs between its [polygons] shapes by the value of its + * [progress]. + * + * @param progress the progress of this loading indicator, where 0.0 represents no progress and 1.0 + * represents full progress. Values outside of this range are coerced into the range. + * @param modifier the [Modifier] to be applied to this loading indicator + * @param color the loading indicator's color + * @param polygons a list of [RoundedPolygon]s for the sequence of shapes this loading indicator + * will morph between as it progresses from 0.0 to 1.0. + */ +@Composable +fun LoadingIndicator( + progress: () -> Float, + modifier: Modifier = Modifier, + color: Color = LoadingIndicatorDefaults.indicatorColor, + polygons: List = LoadingIndicatorDefaults.DeterminateIndicatorPolygons, +) { + LoadingIndicatorImpl( + progress = progress, + modifier = modifier, + containerColor = Color.Unspecified, + indicatorColor = color, + containerShape = LoadingIndicatorDefaults.containerShape, + indicatorPolygons = polygons, + ) +} + +/** + * A Material Design loading indicator. + * + * This version of the loading indicator animates and morphs between various shapes as long as the + * loading indicator is visible. + * + * @param modifier the [Modifier] to be applied to this loading indicator + * @param color the loading indicator's color + * @param polygons a list of [RoundedPolygon]s for the sequence of shapes this loading indicator + * will morph between. + */ +@Composable +fun LoadingIndicator( + modifier: Modifier = Modifier, + color: Color = LoadingIndicatorDefaults.indicatorColor, + polygons: List = LoadingIndicatorDefaults.IndeterminateIndicatorPolygons, +) { + LoadingIndicatorImpl( + modifier = modifier, + containerColor = Color.Unspecified, + indicatorColor = color, + containerShape = LoadingIndicatorDefaults.containerShape, + indicatorPolygons = polygons, + ) +} + +/** + * A Material Design contained loading indicator. + * + * This version of the loading indicator morphs between its [polygons] shapes by the value of its + * [progress]. The shapes in this variation are contained within a colored [containerShape]. + * + * @param progress the progress of this loading indicator, where 0.0 represents no progress and 1.0 + * represents full progress. Values outside of this range are coerced into the range. + * @param modifier the [Modifier] to be applied to this loading indicator + * @param containerColor the loading indicator's container color + * @param indicatorColor the loading indicator's color + * @param containerShape the loading indicator's container shape + * @param polygons a list of [RoundedPolygon]s for the sequence of shapes this loading indicator + * will morph between as it progresses from 0.0 to 1.0. + */ +@Composable +fun ContainedLoadingIndicator( + progress: () -> Float, + modifier: Modifier = Modifier, + containerColor: Color = LoadingIndicatorDefaults.containedContainerColor, + indicatorColor: Color = LoadingIndicatorDefaults.containedIndicatorColor, + containerShape: Shape = LoadingIndicatorDefaults.containerShape, + polygons: List = LoadingIndicatorDefaults.DeterminateIndicatorPolygons, +) { + LoadingIndicatorImpl( + progress = progress, + modifier = modifier, + containerColor = containerColor, + indicatorColor = indicatorColor, + containerShape = containerShape, + indicatorPolygons = polygons, + ) +} + +/** + * A Material Design contained loading indicator. + * + * This version of the loading indicator animates and morphs between various shapes as long as the + * loading indicator is visible. The shapes in this variation are contained within a colored + * [containerShape]. + * + * @param modifier the [Modifier] to be applied to this loading indicator + * @param containerColor the loading indicator's container color + * @param indicatorColor the loading indicator's color + * @param containerShape the loading indicator's container shape + * @param polygons a list of [RoundedPolygon]s for the sequence of shapes this loading indicator + * will morph between. + */ +@Composable +fun ContainedLoadingIndicator( + modifier: Modifier = Modifier, + containerColor: Color = LoadingIndicatorDefaults.containedContainerColor, + indicatorColor: Color = LoadingIndicatorDefaults.containedIndicatorColor, + containerShape: Shape = LoadingIndicatorDefaults.containerShape, + polygons: List = LoadingIndicatorDefaults.IndeterminateIndicatorPolygons, +) { + LoadingIndicatorImpl( + modifier = modifier, + containerColor = containerColor, + indicatorColor = indicatorColor, + containerShape = containerShape, + indicatorPolygons = polygons, + ) +} + +// ── INTERNAL IMPLEMENTATION DETAILS ───────────────────────────────────────── + +@Composable +private fun LoadingIndicatorImpl( + progress: () -> Float, + modifier: Modifier, + containerColor: Color, + indicatorColor: Color, + containerShape: Shape, + indicatorPolygons: List, +) { + require(indicatorPolygons.size > 1) { + "indicatorPolygons should have, at least, two RoundedPolygons" + } + val coercedProgress = { progress().coerceIn(0f, 1f) } + val path = remember { Path() } + val scaleMatrix = remember { Matrix() } + val morphSequence = remember(indicatorPolygons) { + morphSequence(polygons = indicatorPolygons, circularSequence = false) + } + val morphScaleFactor = remember(morphSequence) { + calculateScaleFactor(indicatorPolygons) * LoadingIndicatorDefaults.ActiveIndicatorScale + } + Box( + modifier = modifier + .semantics(mergeDescendants = true) { + progressBarRangeInfo = ProgressBarRangeInfo( + coercedProgress().takeUnless { it.isNaN() } ?: 0f, + 0f..1f, + ) + } + .size( + width = LoadingIndicatorDefaults.ContainerWidth, + height = LoadingIndicatorDefaults.ContainerHeight, + ) + .fillMaxSize() + .clip(containerShape) + .background(containerColor), + contentAlignment = Alignment.Center, + ) { + Spacer( + modifier = Modifier + .aspectRatio(ratio = 1f, matchHeightConstraintsFirst = true) + .drawWithContent { + val progressValue = coercedProgress() + val activeMorphIndex = (morphSequence.size * progressValue) + .toInt() + .coerceAtMost(morphSequence.size - 1) + val adjustedProgressValue = if (progressValue == 1f && activeMorphIndex == morphSequence.size - 1) { + 1f + } else { + (progressValue * morphSequence.size) % 1f + } + + val rotation = -progressValue * 180 + rotate(rotation) { + drawPath( + path = processPath( + path = morphSequence[activeMorphIndex].toPath( + progress = adjustedProgressValue, + path = path, + startAngle = 0, + ), + size = size, + scaleFactor = morphScaleFactor, + scaleMatrix = scaleMatrix, + ), + color = indicatorColor, + style = Fill, + ) + } + } + ) + } +} + +@Composable +private fun LoadingIndicatorImpl( + modifier: Modifier, + containerColor: Color, + indicatorColor: Color, + containerShape: Shape, + indicatorPolygons: List, +) { + require(indicatorPolygons.size > 1) { + "indicatorPolygons should have, at least, two RoundedPolygons" + } + val morphSequence = remember(indicatorPolygons) { + morphSequence(polygons = indicatorPolygons, circularSequence = true) + } + val shapesScaleFactor = remember(indicatorPolygons) { + calculateScaleFactor(indicatorPolygons) * LoadingIndicatorDefaults.ActiveIndicatorScale + } + val morphProgress = remember { Animatable(0f) } + var morphRotationTargetAngle by remember { mutableFloatStateOf(QuarterRotation) } + val globalRotation = remember { Animatable(0f) } + var currentMorphIndex by remember(indicatorPolygons) { mutableIntStateOf(0) } + + LaunchedEffect(indicatorPolygons) { + val morphAnimationBlock = { + launch { + val morphAnimationSpec = spring(dampingRatio = 0.6f, stiffness = 200f, visibilityThreshold = 0.1f) + while (true) { + val deferred = async { + val animationResult = morphProgress.animateTo( + targetValue = 1f, + animationSpec = morphAnimationSpec, + ) + if (animationResult.endReason == AnimationEndReason.Finished) { + currentMorphIndex = (currentMorphIndex + 1) % morphSequence.size + morphProgress.snapTo(0f) + morphRotationTargetAngle = (morphRotationTargetAngle + QuarterRotation) % FullRotation + } + } + delay(MorphIntervalMillis) + deferred.await() + } + } + } + + val rotationAnimationBlock = { + launch { + globalRotation.animateTo( + targetValue = FullRotation, + animationSpec = infiniteRepeatable( + tween(GlobalRotationDurationMillis, easing = LinearEasing), + repeatMode = RepeatMode.Restart, + ), + ) + } + } + + when (val policy = coroutineContext[InfiniteAnimationPolicy]) { + null -> { + morphAnimationBlock() + rotationAnimationBlock() + } + else -> policy.onInfiniteOperation { + morphAnimationBlock() + rotationAnimationBlock() + } + } + } + + val path = remember { Path() } + val scaleMatrix = remember { Matrix() } + Box( + modifier = modifier + .progressSemantics() + .size( + width = LoadingIndicatorDefaults.ContainerWidth, + height = LoadingIndicatorDefaults.ContainerHeight, + ) + .fillMaxSize() + .clip(containerShape) + .background(containerColor), + contentAlignment = Alignment.Center, + ) { + Spacer( + modifier = Modifier + .aspectRatio(1f, matchHeightConstraintsFirst = true) + .drawWithContent { + val progress = morphProgress.value + rotate(progress * 90 + morphRotationTargetAngle + globalRotation.value) { + drawPath( + path = processPath( + path = morphSequence[currentMorphIndex].toPath( + progress = progress, + path = path, + startAngle = 0, + ), + size = size, + scaleFactor = shapesScaleFactor, + scaleMatrix = scaleMatrix, + ), + color = indicatorColor, + style = Fill, + ) + } + } + ) + } +} + +// ── LOADING INDICATOR DEFAULTS ────────────────────────────────────────────── +object LoadingIndicatorDefaults { + val ContainerWidth: Dp = 48.dp + val ContainerHeight: Dp = 48.dp + val IndicatorSize: Dp = 40.dp + + val containerShape: Shape + @Composable get() = RoundedCornerShape(16.dp) + + val indicatorColor: Color + @Composable get() = MaterialTheme.colorScheme.primary + + val containedIndicatorColor: Color + @Composable get() = MaterialTheme.colorScheme.primary + + val containedContainerColor: Color + @Composable get() = MaterialTheme.colorScheme.surfaceContainerHigh + + val IndeterminateIndicatorPolygons: List = listOf( + MaterialShapes.SoftBurst, + MaterialShapes.Cookie9Sided, + MaterialShapes.Pentagon, + MaterialShapes.Pill, + MaterialShapes.Sunny, + MaterialShapes.Cookie4Sided, + MaterialShapes.Oval, + ) + + val DeterminateIndicatorPolygons: List = listOf( + MaterialShapes.Circle.transformed(Matrix().apply { rotateZ(360f / 20) }), + MaterialShapes.SoftBurst, + ) + + internal val ActiveIndicatorScale = + IndicatorSize.value / min(ContainerWidth.value, ContainerHeight.value) +} + +// ── SHAPE UTIL EXTENSIONS (TRANSFORM & PATH CONVERSION) ──────────────────── +internal fun RoundedPolygon.transformed(matrix: Matrix): RoundedPolygon = transformed { x, y -> + val transformedPoint = matrix.map(Offset(x, y)) + TransformResult(transformedPoint.x, transformedPoint.y) +} + +internal fun Morph.toPath( + progress: Float, + path: Path = Path(), + startAngle: Int = 270, + repeatPath: Boolean = false, + closePath: Boolean = true, + rotationPivotX: Float = 0f, + rotationPivotY: Float = 0f, +): Path { + pathFromCubics( + path = path, + startAngle = startAngle, + repeatPath = repeatPath, + closePath = closePath, + cubics = asCubics(progress), + rotationPivotX = rotationPivotX, + rotationPivotY = rotationPivotY, + ) + return path +} + +private fun pathFromCubics( + path: Path, + startAngle: Int, + repeatPath: Boolean, + closePath: Boolean, + cubics: List, + rotationPivotX: Float, + rotationPivotY: Float, +) { + var first = true + var firstCubic: Cubic? = null + path.rewind() + cubics.fastForEach { + if (first) { + path.moveTo(it.anchor0X, it.anchor0Y) + if (startAngle != 0) { + firstCubic = it + } + first = false + } + path.cubicTo( + it.control0X, + it.control0Y, + it.control1X, + it.control1Y, + it.anchor1X, + it.anchor1Y, + ) + } + if (repeatPath) { + var firstInRepeat = true + cubics.fastForEach { + if (firstInRepeat) { + path.lineTo(it.anchor0X, it.anchor0Y) + firstInRepeat = false + } + path.cubicTo( + it.control0X, + it.control0Y, + it.control1X, + it.control1Y, + it.anchor1X, + it.anchor1Y, + ) + } + } + + if (closePath) path.close() + + if (startAngle != 0 && firstCubic != null) { + val angleToFirstCubic = radiansToDegrees( + atan2( + y = cubics[0].anchor0Y - rotationPivotY, + x = cubics[0].anchor0X - rotationPivotX, + ) + ) + path.transform(Matrix().apply { rotateZ(-angleToFirstCubic + startAngle) }) + } +} + +private fun radiansToDegrees(radians: Float): Float { + return (radians * 180.0 / PI).toFloat() +} + +private fun morphSequence(polygons: List, circularSequence: Boolean): List { + return buildList { + for (i in polygons.indices) { + if (i + 1 < polygons.size) { + add(Morph(polygons[i].normalized(), polygons[i + 1].normalized())) + } else if (circularSequence) { + add(Morph(polygons[i].normalized(), polygons[0].normalized())) + } + } + } +} + +private fun calculateScaleFactor(indicatorPolygons: List): Float { + var scaleFactor = 1f + val bounds = FloatArray(size = 4) + val maxBounds = FloatArray(size = 4) + indicatorPolygons.fastForEach { polygon -> + polygon.calculateBounds(bounds) + polygon.calculateMaxBounds(maxBounds) + val scaleX = bounds.width() / maxBounds.width() + val scaleY = bounds.height() / maxBounds.height() + scaleFactor = min(scaleFactor, max(scaleX, scaleY)) + } + return scaleFactor +} + +private fun FloatArray.width(): Float = this[2] - this[0] +private fun FloatArray.height(): Float = this[3] - this[1] + +private fun processPath( + path: Path, + size: Size, + scaleFactor: Float, + scaleMatrix: Matrix = Matrix(), +): Path { + scaleMatrix.reset() + scaleMatrix.apply { scale(x = size.width * scaleFactor, y = size.height * scaleFactor) } + path.transform(scaleMatrix) + path.translate(size.center - path.getBounds().center) + return path +} + +// ── PREDEFINED MATERIAL SHAPES ────────────────────────────────────────────── +object MaterialShapes { + private val cornerRound15 = CornerRounding(radius = .15f) + private val cornerRound20 = CornerRounding(radius = .2f) + private val cornerRound30 = CornerRounding(radius = .3f) + private val cornerRound50 = CornerRounding(radius = .5f) + private val cornerRound100 = CornerRounding(radius = 1f) + + private val rotateNeg45 = Matrix().apply { rotateZ(-45f) } + private val rotateNeg90 = Matrix().apply { rotateZ(-90f) } + private val rotateNeg135 = Matrix().apply { rotateZ(-135f) } + + val Circle: RoundedPolygon = RoundedPolygon.circle(numVertices = 10).normalized() + val Square: RoundedPolygon = RoundedPolygon.rectangle(width = 1f, height = 1f, rounding = cornerRound30).normalized() + val Oval: RoundedPolygon = RoundedPolygon.circle().transformed(Matrix().apply { scale(1f, 0.64f) }).transformed(rotateNeg45).normalized() + + val Pill: RoundedPolygon = customPolygon( + listOf( + PointNRound(Offset(0.961f, 0.039f), CornerRounding(0.426f)), + PointNRound(Offset(1.001f, 0.428f)), + PointNRound(Offset(1.000f, 0.609f), CornerRounding(1.000f)), + ), + reps = 2, + mirroring = true, + ).normalized() + + val Pentagon: RoundedPolygon = customPolygon( + listOf( + PointNRound(Offset(0.500f, -0.009f), CornerRounding(0.172f)), + PointNRound(Offset(1.030f, 0.365f), CornerRounding(0.164f)), + PointNRound(Offset(0.828f, 0.970f), CornerRounding(0.169f)), + ), + reps = 1, + mirroring = true, + ).normalized() + + val Sunny: RoundedPolygon = RoundedPolygon.star( + numVerticesPerRadius = 8, + innerRadius = .8f, + rounding = cornerRound15, + ).normalized() + + val Cookie4Sided: RoundedPolygon = customPolygon( + listOf( + PointNRound(Offset(1.237f, 1.236f), CornerRounding(0.258f)), + PointNRound(Offset(0.500f, 0.918f), CornerRounding(0.233f)), + ), + 4, + ).normalized() + + val Cookie9Sided: RoundedPolygon = RoundedPolygon.star( + numVerticesPerRadius = 9, + innerRadius = .8f, + rounding = cornerRound50, + ).transformed(rotateNeg90).normalized() + + val SoftBurst: RoundedPolygon = customPolygon( + listOf( + PointNRound(Offset(0.193f, 0.277f), CornerRounding(0.053f)), + PointNRound(Offset(0.176f, 0.055f), CornerRounding(0.053f)), + ), + reps = 10, + ).normalized() + + private data class PointNRound( + val o: Offset, + val r: CornerRounding = CornerRounding.Unrounded, + ) + + private fun doRepeat( + points: List, + reps: Int, + center: Offset, + mirroring: Boolean, + ) = if (mirroring) { + buildList { + val angles = points.fastMap { (it.o - center).angleDegrees() } + val distances = points.fastMap { (it.o - center).getDistance() } + val actualReps = reps * 2 + val sectionAngle = 360f / actualReps + repeat(actualReps) { + points.indices.forEach { index -> + val i = if (it % 2 == 0) index else points.lastIndex - index + if (i > 0 || it % 2 == 0) { + val a = (sectionAngle * it + + if (it % 2 == 0) angles[i] + else sectionAngle - angles[i] + 2 * angles[0]) + .toRadians() + val finalPoint = Offset(cos(a), sin(a)) * distances[i] + center + add(PointNRound(finalPoint, points[i].r)) + } + } + } + } + } else { + points.size.let { np -> + (0 until np * reps).map { + val point = points[it % np].o.rotateDegrees((it / np) * 360f / reps, center) + PointNRound(point, points[it % np].r) + } + } + } + + private fun Offset.rotateDegrees(angle: Float, center: Offset = Offset.Zero) = + (angle.toRadians()).let { a -> + val off = this - center + Offset(off.x * cos(a) - off.y * sin(a), off.x * sin(a) + off.y * cos(a)) + center + } + + private fun Float.toRadians(): Float = this / 360f * 2 * PI.toFloat() + + private fun Offset.angleDegrees() = atan2(y, x) * 180f / PI.toFloat() + + private fun customPolygon( + pnr: List, + reps: Int, + center: Offset = Offset(0.5f, 0.5f), + mirroring: Boolean = false, + ): RoundedPolygon { + val actualPoints = doRepeat(pnr, reps, center, mirroring) + return RoundedPolygon( + vertices = FloatArray(actualPoints.size * 2) { ix -> + actualPoints[ix / 2].o.let { if (ix % 2 == 0) it.x else it.y } + }, + perVertexRounding = buildList { for (p in actualPoints) add(p.r) }, + centerX = center.x, + centerY = center.y, + ) + } +} + +private const val GlobalRotationDurationMillis = 4666 +private const val MorphIntervalMillis = 650L + +private const val FullRotation = 360f +private const val QuarterRotation = FullRotation / 4f From ca258654b238a1902b171f810fbe920702ef8edc Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 09:21:21 +0530 Subject: [PATCH 02/20] UI: restore graphics-shapes dependency to resolve compilation failure --- Android/app/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) diff --git a/Android/app/build.gradle.kts b/Android/app/build.gradle.kts index dd768519..dda9a4da 100644 --- a/Android/app/build.gradle.kts +++ b/Android/app/build.gradle.kts @@ -302,6 +302,7 @@ dependencies { implementation("androidx.compose.ui:ui-tooling-preview") implementation("androidx.compose.material3:material3") implementation("androidx.compose.material:material-icons-extended") + implementation("androidx.graphics:graphics-shapes:1.0.1") // Core Android implementation("androidx.core:core-ktx:1.12.0") From b9576357fbfbba393ba2cda9d53434a8bee4077f Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 09:21:50 +0530 Subject: [PATCH 03/20] docs: add changelog.md for ui branch --- changelog.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 changelog.md diff --git a/changelog.md b/changelog.md new file mode 100644 index 00000000..6cfab7b2 --- /dev/null +++ b/changelog.md @@ -0,0 +1,17 @@ +# Changelog + +## Commits on `ui` branch: +* UI: restore graphics-shapes dependency to resolve compilation failure (ca25865) +* UI: updated spinner and PullToRefresh anim, corner radius in systemd page, and minor tweaks (ebfd1ea) + +## Original commits (before squash): +* ci: skip Droidspaces CI on ui branch; drop --configuration-cache from android-fast (52ab8ee) +* fix(build): make generateModuleProp compatible with configuration cache (9088436) +* fix(ui): smooth pull-to-refresh release animation (5935fe8) +* added faster workflow (d81fa70) +* UI: migrate swipe down to refresh to shape-morphing LoadingIndicator (8ed20c8) +* UI: import LoadingSize in files using LoadingIndicator (e188639) +* UI: fix compilation errors (overload resolution ambiguity and layout imports) (881f354) +* UI: consolidate shape-morphing Google M3 LoadingIndicator APIs into LoadingIndicator.kt (f36f394) +* UI: replace circular progress indicators with shape-morphing Google MD3 LoadingIndicator (ac8d5fe) +* UI: match corner radius, disable color horizontal swipe, increase bottom padding (0624e65) From ba3e8f3ac092af6a62548fe7a97ed928acf53bf9 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 09:34:06 +0530 Subject: [PATCH 04/20] Delete changelog.md --- changelog.md | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 changelog.md diff --git a/changelog.md b/changelog.md deleted file mode 100644 index 6cfab7b2..00000000 --- a/changelog.md +++ /dev/null @@ -1,17 +0,0 @@ -# Changelog - -## Commits on `ui` branch: -* UI: restore graphics-shapes dependency to resolve compilation failure (ca25865) -* UI: updated spinner and PullToRefresh anim, corner radius in systemd page, and minor tweaks (ebfd1ea) - -## Original commits (before squash): -* ci: skip Droidspaces CI on ui branch; drop --configuration-cache from android-fast (52ab8ee) -* fix(build): make generateModuleProp compatible with configuration cache (9088436) -* fix(ui): smooth pull-to-refresh release animation (5935fe8) -* added faster workflow (d81fa70) -* UI: migrate swipe down to refresh to shape-morphing LoadingIndicator (8ed20c8) -* UI: import LoadingSize in files using LoadingIndicator (e188639) -* UI: fix compilation errors (overload resolution ambiguity and layout imports) (881f354) -* UI: consolidate shape-morphing Google M3 LoadingIndicator APIs into LoadingIndicator.kt (f36f394) -* UI: replace circular progress indicators with shape-morphing Google MD3 LoadingIndicator (ac8d5fe) -* UI: match corner radius, disable color horizontal swipe, increase bottom padding (0624e65) From a330e61492a77f53fdcfedf5476222cf048a81b4 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 14:35:12 +0530 Subject: [PATCH 05/20] refactor(ui): update storage bar progress indicator and terminal UI tab switching system --- .gitignore | 3 +- .../app/ui/component/RootfsRepoSheet.kt | 7 +- .../app/ui/screen/ContainerDetailsScreen.kt | 8 +- .../app/ui/screen/ContainerTerminalScreen.kt | 225 ++++++++++++++---- 4 files changed, 195 insertions(+), 48 deletions(-) diff --git a/.gitignore b/.gitignore index dbfe53da..39b98b17 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ output/ .claude Android/app/src/main/assets/binaries/ Android/app/src/main/assets/supported_locales.txt +md3/ # Keystores *.keystore @@ -25,4 +26,4 @@ compile_commands.json *.qcow2 # Android stuff -.gradle \ No newline at end of file +.gradle diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt index 650520ea..3079ce2e 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/RootfsRepoSheet.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -486,10 +487,10 @@ private fun RootfsAssetCard( progress = { state.percent / 100f }, modifier = Modifier .fillMaxWidth() - .height(4.dp) - .clip(RoundedCornerShape(2.dp)), + .height(4.dp), color = MaterialTheme.colorScheme.tertiary, // Match state pill - trackColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f) + trackColor = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f), + strokeCap = StrokeCap.Round ) Text( text = "${state.percent}%", diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt index 378046be..17691a14 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerDetailsScreen.kt @@ -17,6 +17,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -548,9 +549,10 @@ private fun SparseDiskUsageCard( progress = { animatedFraction }, modifier = Modifier .fillMaxWidth() - .height(8.dp) - .clip(RoundedCornerShape(4.dp)), - trackColor = MaterialTheme.colorScheme.surfaceVariant + .height(8.dp), + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.surfaceVariant, + strokeCap = StrokeCap.Round ) } } diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt index 939b022e..c6fecc18 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt @@ -10,6 +10,12 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.tween +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -21,6 +27,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material3.* +import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -57,7 +64,7 @@ private data class TerminalTab( val label: String, ) -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(ExperimentalMaterial3Api::class, androidx.compose.foundation.ExperimentalFoundationApi::class) @Composable fun ContainerTerminalScreen( containerName: String, @@ -96,6 +103,7 @@ fun ContainerTerminalScreen( val tabs = remember { mutableStateListOf() } var activeTabId by remember { mutableStateOf("") } var showUserPicker by remember { mutableStateOf(false) } + var tabToClose by remember { mutableStateOf(null) } // Resolve hostname reactively; picker is shown only after binder+hostname are both ready var hostname by remember(containerName) { @@ -190,10 +198,23 @@ fun ContainerTerminalScreen( ) } + if (tabToClose != null) { + CloseSessionDialog( + onConfirm = { + val tab = tabToClose!! + tabToClose = null + closeTab(tab) + }, + onDismiss = { + tabToClose = null + } + ) + } + Scaffold( topBar = { Column { - TopAppBar( + CenterAlignedTopAppBar( title = { Text( containerName, @@ -213,58 +234,59 @@ fun ContainerTerminalScreen( Icon(Icons.Default.Add, contentDescription = "New tab") } }, - colors = TopAppBarDefaults.topAppBarColors( + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( containerColor = MaterialTheme.colorScheme.surfaceContainerLow ) ) if (tabs.isNotEmpty()) { + val selectedTabIndex = tabs.indexOfFirst { it.id == activeTabId }.coerceAtLeast(0) ScrollableTabRow( - selectedTabIndex = tabs.indexOfFirst { it.id == activeTabId }.coerceAtLeast(0), + selectedTabIndex = selectedTabIndex, containerColor = MaterialTheme.colorScheme.surfaceContainerLow, contentColor = MaterialTheme.colorScheme.primary, edgePadding = 0.dp, divider = {}, - modifier = Modifier.fillMaxWidth() + indicator = { tabPositions -> + if (selectedTabIndex < tabPositions.size) { + val position = tabPositions[selectedTabIndex] + Box( + Modifier + .tabIndicatorOffset(position) + .fillMaxHeight() + .padding(horizontal = 4.dp, vertical = 6.dp) + .background( + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), + shape = RoundedCornerShape(12.dp) + ) + ) + } + }, + modifier = Modifier.fillMaxWidth().height(48.dp) ) { tabs.forEach { tab -> val isSelected = tab.id == activeTabId Tab( selected = isSelected, - onClick = { activeTabId = tab.id }, - modifier = Modifier.height(40.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.padding(horizontal = 8.dp) - ) { - Text( - tab.label, - style = MaterialTheme.typography.labelMedium, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.widthIn(max = 120.dp) + onClick = {}, + modifier = Modifier + .height(48.dp) + .clip(RoundedCornerShape(12.dp)) + .combinedClickable( + onClick = { activeTabId = tab.id }, + onLongClick = { tabToClose = tab } ) - Box( - Modifier.size(16.dp).clip(CircleShape), - contentAlignment = Alignment.Center - ) { - IconButton( - onClick = { closeTab(tab) }, - modifier = Modifier.size(16.dp) - ) { - Icon( - Icons.Default.Close, - contentDescription = "Close tab", - modifier = Modifier.size(12.dp), - tint = if (isSelected) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } + ) { + Text( + text = tab.label, + style = MaterialTheme.typography.labelMedium, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + color = if (isSelected) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) } } } @@ -284,13 +306,23 @@ fun ContainerTerminalScreen( LoadingIndicator(size = LoadingSize.Medium) } } else { - tabs.forEach { tab -> + val selectedTabIndex = tabs.indexOfFirst { it.id == activeTabId }.coerceAtLeast(0) + var previousActiveIndex by remember { mutableIntStateOf(0) } + LaunchedEffect(selectedTabIndex) { + previousActiveIndex = selectedTabIndex + } + val isMovingRight = selectedTabIndex > previousActiveIndex + val hasMoved = selectedTabIndex != previousActiveIndex + + tabs.forEachIndexed { index, tab -> key(tab.id) { TerminalTabView( tab = tab, binder = binder!!, containerName = containerName, isVisible = tab.id == activeTabId, + isMovingRight = isMovingRight, + hasMoved = hasMoved, activity = activity, onSessionFinished = { closeTab(tab) }, modifier = Modifier.fillMaxSize() @@ -308,6 +340,8 @@ private fun TerminalTabView( binder: TerminalSessionService.SessionBinder, containerName: String, isVisible: Boolean, + isMovingRight: Boolean, + hasMoved: Boolean, activity: Activity?, onSessionFinished: () -> Unit, modifier: Modifier = Modifier, @@ -320,10 +354,33 @@ private fun TerminalTabView( val context = androidx.compose.ui.platform.LocalContext.current val terminalTypeface = remember { ResourcesCompat.getFont(context, R.font.jetbrains_mono) } + val slideOffsetFraction = 0.08f + val enterTransition = if (hasMoved) { + slideInHorizontally( + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) + ) { width -> + if (isMovingRight) (width * slideOffsetFraction).toInt() + else -(width * slideOffsetFraction).toInt() + } + fadeIn(animationSpec = tween(250)) + } else { + fadeIn(animationSpec = AnimationUtils.fastSpec()) + } + + val exitTransition = if (hasMoved) { + slideOutHorizontally( + animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing) + ) { width -> + if (isMovingRight) -(width * slideOffsetFraction).toInt() + else (width * slideOffsetFraction).toInt() + } + fadeOut(animationSpec = tween(250)) + } else { + fadeOut(animationSpec = AnimationUtils.fastSpec()) + } + AnimatedVisibility( visible = isVisible, - enter = fadeIn(animationSpec = AnimationUtils.fastSpec()), - exit = fadeOut(animationSpec = AnimationUtils.fastSpec()), + enter = enterTransition, + exit = exitTransition, modifier = modifier ) { Column(modifier = Modifier.fillMaxSize()) { @@ -541,6 +598,92 @@ private fun UserPickerDialog( } } +@Composable +private fun CloseSessionDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + val context = LocalContext.current + val dialogShape = RoundedCornerShape(28.dp) + + androidx.compose.ui.window.Dialog( + onDismissRequest = onDismiss, + properties = androidx.compose.ui.window.DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp) + .wrapContentHeight(), + shape = dialogShape, + color = MaterialTheme.colorScheme.surfaceContainer, + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), + tonalElevation = 0.dp + ) { + Column( + modifier = Modifier.padding(24.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "Close this session?", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Row( + modifier = Modifier.fillMaxWidth().padding(top = 8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // NO button: styled like the restart button (secondary container) + Surface( + onClick = onDismiss, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f), + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f)) + ) { + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = context.getString(android.R.string.no), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } + } + + // YES button: styled like the stop button (error container) + Surface( + onClick = onConfirm, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.4f), + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.2f)) + ) { + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = context.getString(android.R.string.yes), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.error + ) + } + } + } + } + } + } +} + private val VIRTUAL_KEYS_LAYOUT = """ [ [ From 143022cc83e6bca38d7e445b2c6f237c6e7a242e Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 14:51:55 +0530 Subject: [PATCH 06/20] fix(ui): replace Tab with Box in terminal screen to fix click and long-press actions --- .../droidspaces/app/ui/screen/ContainerTerminalScreen.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt index c6fecc18..4491a834 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt @@ -266,16 +266,15 @@ fun ContainerTerminalScreen( ) { tabs.forEach { tab -> val isSelected = tab.id == activeTabId - Tab( - selected = isSelected, - onClick = {}, + Box( modifier = Modifier .height(48.dp) .clip(RoundedCornerShape(12.dp)) .combinedClickable( onClick = { activeTabId = tab.id }, onLongClick = { tabToClose = tab } - ) + ), + contentAlignment = Alignment.Center ) { Text( text = tab.label, From 3e3b4f1ce24088b31744e180713b4a29a0a68755 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 15:38:44 +0530 Subject: [PATCH 07/20] refactor(ui): apply concentric double-outline border to active tab and wrap dialog buttons in shared outline container --- .../app/ui/screen/ContainerTerminalScreen.kt | 99 +++++++++++-------- 1 file changed, 59 insertions(+), 40 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt index 4491a834..f2d12e2e 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt @@ -16,6 +16,7 @@ import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.border import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState @@ -255,6 +256,17 @@ fun ContainerTerminalScreen( .tabIndicatorOffset(position) .fillMaxHeight() .padding(horizontal = 4.dp, vertical = 6.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f), + shape = RoundedCornerShape(16.dp) + ) + .padding(4.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f), + shape = RoundedCornerShape(12.dp) + ) .background( color = MaterialTheme.colorScheme.primary.copy(alpha = 0.12f), shape = RoundedCornerShape(12.dp) @@ -269,7 +281,7 @@ fun ContainerTerminalScreen( Box( modifier = Modifier .height(48.dp) - .clip(RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(16.dp)) .combinedClickable( onClick = { activeTabId = tab.id }, onLongClick = { tabToClose = tab } @@ -630,51 +642,58 @@ private fun CloseSessionDialog( color = MaterialTheme.colorScheme.onSurface ) - Row( - modifier = Modifier.fillMaxWidth().padding(top = 8.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(20.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)) ) { - // NO button: styled like the restart button (secondary container) - Surface( - onClick = onDismiss, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f)) + Row( + modifier = Modifier.fillMaxWidth().padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + // NO button: styled like the restart button (secondary container) + Surface( + onClick = onDismiss, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f), + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f)) ) { - Text( - text = context.getString(android.R.string.no), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = context.getString(android.R.string.no), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSecondaryContainer + ) + } } - } - // YES button: styled like the stop button (error container) - Surface( - onClick = onConfirm, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.4f), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.2f)) - ) { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically + // YES button: styled like the stop button (error container) + Surface( + onClick = onConfirm, + modifier = Modifier.weight(1f).height(48.dp), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.4f), + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.2f)) ) { - Text( - text = context.getString(android.R.string.yes), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.error - ) + Row( + modifier = Modifier.fillMaxSize(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = context.getString(android.R.string.yes), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.error + ) + } } } } From e7b0cb8ad7e51bf747f79511a5ca93039802aa70 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 16:06:49 +0530 Subject: [PATCH 08/20] refactor(ui): smoothly animate bottom navigation items and background indicator on page swipe and selection --- .../app/ui/screen/MainTabScreen.kt | 47 ++++++++++--------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt index a33159b2..67ecdd61 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt @@ -40,6 +40,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import androidx.compose.ui.unit.sp import com.droidspaces.app.R +import kotlin.math.abs enum class TabItem(val titleResId: Int, val icon: androidx.compose.ui.graphics.vector.ImageVector) { Home(R.string.home_title, Icons.Default.Home), @@ -355,7 +356,8 @@ fun MainTabScreen( .onSizeChanged { bottomBarHeight = with(density) { it.height.toDp() } } ) { MainBottomBar( - selectedTab = selectedTab, + pagerState = pagerState, + tabs = tabs, onTabSelected = { tab -> scope.launch { pagerState.scrollToPage(tabs.indexOf(tab)) @@ -576,12 +578,12 @@ private fun ControlPanelTabContent( @Composable private fun MainBottomBar( - selectedTab: TabItem, + pagerState: androidx.compose.foundation.pager.PagerState, + tabs: Array, onTabSelected: (TabItem) -> Unit ) { val context = LocalContext.current - val tabs = TabItem.entries - val selectedIndex = tabs.indexOf(selectedTab) + val pagerPosition = pagerState.currentPage + pagerState.currentPageOffsetFraction Surface( modifier = Modifier.fillMaxWidth(), @@ -603,14 +605,7 @@ private fun MainBottomBar( // Background Indicator BoxWithConstraints(modifier = Modifier.fillMaxWidth()) { val tabWidth = maxWidth / tabs.size - val offset by androidx.compose.animation.core.animateDpAsState( - targetValue = tabWidth * selectedIndex, - animationSpec = androidx.compose.animation.core.spring( - dampingRatio = androidx.compose.animation.core.Spring.DampingRatioLowBouncy, - stiffness = androidx.compose.animation.core.Spring.StiffnessLow - ), - label = "IndicatorOffset" - ) + val offset = tabWidth * pagerPosition Surface( modifier = Modifier @@ -627,12 +622,17 @@ private fun MainBottomBar( horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { - tabs.forEach { tab -> - val isSelected = selectedTab == tab - val contentColor by androidx.compose.animation.animateColorAsState( - targetValue = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - label = "IconColor" - ) + tabs.forEachIndexed { index, tab -> + val distance = abs(pagerPosition - index) + val selectionFraction = (1.0f - distance).coerceIn(0.0f, 1.0f) + + val unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + val selectedColor = MaterialTheme.colorScheme.primary + val contentColor = androidx.compose.ui.graphics.lerp(unselectedColor, selectedColor, selectionFraction) + + val iconSize = androidx.compose.ui.unit.lerp(22.dp, 24.dp, selectionFraction) + val fontSize = (10f + 1f * selectionFraction).sp + val fontWeight = FontWeight((400f + 300f * selectionFraction).toInt()) Surface( onClick = { onTabSelected(tab) }, @@ -650,15 +650,16 @@ private fun MainBottomBar( Icon( imageVector = tab.icon, contentDescription = null, - modifier = Modifier.size(if (isSelected) 24.dp else 22.dp), + modifier = Modifier.size(iconSize), tint = contentColor ) Text( text = context.getString(tab.titleResId), - style = MaterialTheme.typography.labelSmall, - color = contentColor, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, - fontSize = if (isSelected) 11.sp else 10.sp + style = MaterialTheme.typography.labelSmall.copy( + fontSize = fontSize, + fontWeight = fontWeight + ), + color = contentColor ) } } From 1eb4428b000d86896869078daec8034b7bbb89ea Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 16:16:07 +0530 Subject: [PATCH 09/20] fix(ui): import lerp with alias and resolve experimental api usage in bottom bar navigation --- .../com/droidspaces/app/ui/screen/MainTabScreen.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt index 67ecdd61..2eecc186 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt @@ -40,7 +40,8 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import androidx.compose.ui.unit.sp import com.droidspaces.app.R -import kotlin.math.abs +import androidx.compose.ui.graphics.lerp as colorLerp +import androidx.compose.ui.unit.lerp as dpLerp enum class TabItem(val titleResId: Int, val icon: androidx.compose.ui.graphics.vector.ImageVector) { Home(R.string.home_title, Icons.Default.Home), @@ -576,6 +577,7 @@ private fun ControlPanelTabContent( } } +@OptIn(ExperimentalFoundationApi::class) @Composable private fun MainBottomBar( pagerState: androidx.compose.foundation.pager.PagerState, @@ -623,14 +625,14 @@ private fun MainBottomBar( verticalAlignment = Alignment.CenterVertically ) { tabs.forEachIndexed { index, tab -> - val distance = abs(pagerPosition - index) + val distance = if (pagerPosition > index) pagerPosition - index else index.toFloat() - pagerPosition val selectionFraction = (1.0f - distance).coerceIn(0.0f, 1.0f) val unselectedColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) val selectedColor = MaterialTheme.colorScheme.primary - val contentColor = androidx.compose.ui.graphics.lerp(unselectedColor, selectedColor, selectionFraction) + val contentColor = colorLerp(unselectedColor, selectedColor, selectionFraction) - val iconSize = androidx.compose.ui.unit.lerp(22.dp, 24.dp, selectionFraction) + val iconSize = dpLerp(22.dp, 24.dp, selectionFraction) val fontSize = (10f + 1f * selectionFraction).sp val fontWeight = FontWeight((400f + 300f * selectionFraction).toInt()) From 03edbd46faacf63bcb37bacfa8003a016b6ce346 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Mon, 3 Aug 2026 19:36:34 +0530 Subject: [PATCH 10/20] refactor(ui): update DialogFooterRow with shared concentric outline and fix bottom nav bar active font weight snap --- .../app/ui/component/DialogFooterRow.kt | 64 ++++++++++++------- .../app/ui/screen/MainTabScreen.kt | 16 +++-- 2 files changed, 51 insertions(+), 29 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt index 901564a9..28e1a72e 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt @@ -39,31 +39,49 @@ fun DialogFooterRow( confirmColor: Color = MaterialTheme.colorScheme.primary, confirmContentColor: Color = MaterialTheme.colorScheme.onPrimary, ) { - Row(modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable(onClick = onDismiss), - shape = RoundedCornerShape(14.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = cancelBorderAlpha)), - tonalElevation = 0.dp + Surface( + modifier = modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(20.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)) + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text(dismissLabel, style = MaterialTheme.typography.labelLarge, fontWeight = textFontWeight) + // Dismiss/Cancel Button + Surface( + onClick = onDismiss, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(16.dp), + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = cancelBorderAlpha)) + ) { + Box(modifier = Modifier.padding(vertical = 12.dp), contentAlignment = Alignment.Center) { + Text( + text = dismissLabel, + style = MaterialTheme.typography.labelLarge, + fontWeight = textFontWeight + ) + } } - } - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable(enabled = confirmEnabled, onClick = onConfirm), - shape = RoundedCornerShape(14.dp), - color = if (confirmEnabled) confirmColor else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - tonalElevation = 0.dp - ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text( - confirmLabel, - style = MaterialTheme.typography.labelLarge, - fontWeight = textFontWeight, - color = if (confirmEnabled) confirmContentColor else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) + + // Confirm/OK Button + Surface( + onClick = onConfirm, + enabled = confirmEnabled, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(16.dp), + color = if (confirmEnabled) confirmColor else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f) + ) { + Box(modifier = Modifier.padding(vertical = 12.dp), contentAlignment = Alignment.Center) { + Text( + text = confirmLabel, + style = MaterialTheme.typography.labelLarge, + fontWeight = textFontWeight, + color = if (confirmEnabled) confirmContentColor else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) + ) + } } } } diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt index 2eecc186..448ba47f 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/MainTabScreen.kt @@ -634,7 +634,12 @@ private fun MainBottomBar( val iconSize = dpLerp(22.dp, 24.dp, selectionFraction) val fontSize = (10f + 1f * selectionFraction).sp - val fontWeight = FontWeight((400f + 300f * selectionFraction).toInt()) + val isSelected = pagerState.currentPage == index + val fontWeight = if (isSelected) { + FontWeight.Bold + } else { + FontWeight((400f + 300f * selectionFraction).toInt().coerceIn(400, 700)) + } Surface( onClick = { onTabSelected(tab) }, @@ -657,11 +662,10 @@ private fun MainBottomBar( ) Text( text = context.getString(tab.titleResId), - style = MaterialTheme.typography.labelSmall.copy( - fontSize = fontSize, - fontWeight = fontWeight - ), - color = contentColor + style = MaterialTheme.typography.labelSmall, + color = contentColor, + fontSize = fontSize, + fontWeight = fontWeight ) } } From 8bb8aa00e9c79abecf7f4133195ba70808d196ef Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Wed, 5 Aug 2026 23:21:15 +0530 Subject: [PATCH 11/20] refactor(ui): add stop container confirmation dialog and resolve outline/padding inconsistencies in edit screen dialogs --- .../app/ui/component/ContainerConfigForm.kt | 48 ++---- .../app/ui/screen/ContainersScreen.kt | 162 ++++++++++-------- 2 files changed, 107 insertions(+), 103 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt index 07f0c6ad..7c06ad7c 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt @@ -168,43 +168,19 @@ fun ContainerConfigForm( Text(context.getString(R.string.read_only), style = MaterialTheme.typography.bodyMedium) Switch(checked = roEnabled, onCheckedChange = { roEnabled = it }) } - Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable(onClick = { clearFocus(); showDestDialog = false }), - shape = RoundedCornerShape(14.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), - tonalElevation = 0.dp - ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text(context.getString(R.string.cancel), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold) + DialogFooterRow( + dismissLabel = context.getString(R.string.cancel), + confirmLabel = context.getString(R.string.ok), + onDismiss = { clearFocus(); showDestDialog = false }, + onConfirm = { + clearFocus() + if (destPath.isNotBlank()) { + onStateChange(state.copy(bindMounts = state.bindMounts + BindMount(tempSrcPath, destPath, roEnabled))) + showDestDialog = false } - } - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable( - enabled = destPath.startsWith("/"), - onClick = { - clearFocus() - if (destPath.isNotBlank()) { - onStateChange(state.copy(bindMounts = state.bindMounts + BindMount(tempSrcPath, destPath, roEnabled))) - showDestDialog = false - } - } - ), - shape = RoundedCornerShape(14.dp), - color = if (destPath.startsWith("/")) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - tonalElevation = 0.dp - ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text( - context.getString(R.string.ok), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.SemiBold, - color = if (destPath.startsWith("/")) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - } - } - } + }, + confirmEnabled = destPath.startsWith("/") + ) } } } diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt index 3fdf6f2e..1f838429 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt @@ -45,6 +45,7 @@ import com.droidspaces.app.util.PreferencesManager import com.droidspaces.app.util.FilePickerUtils import com.droidspaces.app.ui.component.ContainerCard import com.droidspaces.app.ui.component.ContainerCardActions +import com.droidspaces.app.ui.component.DialogFooterRow import com.droidspaces.app.ui.component.TerminalDialog import com.droidspaces.app.ui.component.EmptyState import com.droidspaces.app.ui.component.ErrorState @@ -84,6 +85,7 @@ fun ContainersScreen( // UI-only state (dialog triggers / pending pickers). var showUninstallConfirmation by remember { mutableStateOf(null) } + var showStopConfirmationFor by remember { mutableStateOf(null) } var pendingSparseOperation by remember { mutableStateOf(null) } var pendingExportContainer by remember { mutableStateOf(null) } var showRepoSheet by remember { mutableStateOf(false) } @@ -198,15 +200,8 @@ fun ContainersScreen( } }, onStop = { - scope.launch { - opsViewModel.executeOperation( - container, "stop", - onRefresh = { containerViewModel.refresh() }, - onClearUsage = { systemStatsViewModel.clearContainerUsage(it) }, - onFailureSnackbar = { msg -> scope.launch { snackbarHostState.showSnackbar(msg, duration = SnackbarDuration.Long) } } - ) - } - }, + showStopConfirmationFor = container + }, onRestart = { scope.launch { opsViewModel.executeOperation( @@ -367,6 +362,27 @@ fun ContainersScreen( ) } + // Stop confirmation dialog + showStopConfirmationFor?.let { container -> + StopContainerConfirmationDialog( + containerName = container.name, + onConfirm = { + showStopConfirmationFor = null + scope.launch { + opsViewModel.executeOperation( + container, "stop", + onRefresh = { containerViewModel.refresh() }, + onClearUsage = { systemStatsViewModel.clearContainerUsage(it) }, + onFailureSnackbar = { msg -> scope.launch { snackbarHostState.showSnackbar(msg, duration = SnackbarDuration.Long) } } + ) + } + }, + onDismiss = { + showStopConfirmationFor = null + } + ) + } + // Uninstall progress dialog (opsViewModel.uninstallState as? UninstallState.InProgress)?.let { state -> ProgressDialog( @@ -462,37 +478,13 @@ private fun SparseSizeDialog( ) ) - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable(onClick = onDismiss), - shape = RoundedCornerShape(14.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), - tonalElevation = 0.dp - ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text(context.getString(R.string.cancel), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold) - } - } - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable(enabled = isValid, onClick = { size?.let { onConfirm(it) } }), - shape = RoundedCornerShape(14.dp), - color = if (isValid) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - tonalElevation = 0.dp - ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text( - context.getString(R.string.continue_button), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.SemiBold, - color = if (isValid) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - } - } - } + DialogFooterRow( + dismissLabel = context.getString(R.string.cancel), + confirmLabel = context.getString(R.string.continue_button), + onDismiss = onDismiss, + onConfirm = { size?.let { onConfirm(it) } }, + confirmEnabled = isValid + ) } } } @@ -570,37 +562,73 @@ private fun UninstallConfirmationDialog( ) ) } + DialogFooterRow( + dismissLabel = context.getString(R.string.cancel), + confirmLabel = context.getString(R.string.uninstall), + onDismiss = onDismiss, + onConfirm = onConfirm, + confirmEnabled = isConfirmed, + confirmColor = MaterialTheme.colorScheme.error, + confirmContentColor = MaterialTheme.colorScheme.onError + ) + } + } + } +} + +@Composable +private fun StopContainerConfirmationDialog( + containerName: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val dialogShape = RoundedCornerShape(24.dp) + + Dialog( + onDismissRequest = onDismiss, + properties = androidx.compose.ui.window.DialogProperties(usePlatformDefaultWidth = false) + ) { + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 24.dp), + shape = dialogShape, + color = MaterialTheme.colorScheme.surfaceContainer, + border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)), + tonalElevation = 0.dp + ) { + Column(modifier = Modifier.padding(24.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) { Row( - modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp) ) { - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable(onClick = onDismiss), - shape = RoundedCornerShape(14.dp), - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.4f)), - tonalElevation = 0.dp - ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text(context.getString(R.string.cancel), style = MaterialTheme.typography.labelLarge, fontWeight = FontWeight.SemiBold) - } - } - Surface( - modifier = Modifier.weight(1f).clip(RoundedCornerShape(14.dp)).clickable(enabled = isConfirmed, onClick = onConfirm), - shape = RoundedCornerShape(14.dp), - color = if (isConfirmed) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), - tonalElevation = 0.dp - ) { - Box(modifier = Modifier.padding(14.dp), contentAlignment = Alignment.Center) { - Text( - context.getString(R.string.uninstall), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = if (isConfirmed) MaterialTheme.colorScheme.onError else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f) - ) - } - } + Icon( + imageVector = androidx.compose.material.icons.Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(24.dp) + ) + Text( + text = "Stop Container?", + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold + ) } + Text( + text = "Are you sure you want to stop the container \"$containerName\"?", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + DialogFooterRow( + dismissLabel = context.getString(R.string.no), + confirmLabel = context.getString(R.string.yes), + onDismiss = onDismiss, + onConfirm = onConfirm, + confirmEnabled = true, + confirmColor = MaterialTheme.colorScheme.error, + confirmContentColor = MaterialTheme.colorScheme.onError + ) } } } From d0ffc3809884244176449042cfae0e3c8604feef Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Wed, 5 Aug 2026 23:25:58 +0530 Subject: [PATCH 12/20] refactor(ui): update CloseSessionDialog to use DialogFooterRow for design consistency --- .../app/ui/screen/ContainerTerminalScreen.kt | 65 +++---------------- 1 file changed, 10 insertions(+), 55 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt index f2d12e2e..1fcd38bd 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt @@ -51,6 +51,7 @@ import com.droidspaces.app.util.AnimationUtils import com.droidspaces.app.util.ContainerOSInfoManager import com.droidspaces.app.ui.util.LoadingIndicator import com.droidspaces.app.ui.util.LoadingSize +import com.droidspaces.app.ui.component.DialogFooterRow import com.termux.terminal.TerminalSession import com.termux.view.TerminalView import java.lang.ref.WeakReference @@ -642,61 +643,15 @@ private fun CloseSessionDialog( color = MaterialTheme.colorScheme.onSurface ) - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surfaceContainerHigh, - shape = RoundedCornerShape(20.dp), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)) - ) { - Row( - modifier = Modifier.fillMaxWidth().padding(4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - // NO button: styled like the restart button (secondary container) - Surface( - onClick = onDismiss, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.4f), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.secondary.copy(alpha = 0.2f)) - ) { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = context.getString(android.R.string.no), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSecondaryContainer - ) - } - } - - // YES button: styled like the stop button (error container) - Surface( - onClick = onConfirm, - modifier = Modifier.weight(1f).height(48.dp), - shape = RoundedCornerShape(16.dp), - color = MaterialTheme.colorScheme.errorContainer.copy(alpha = 0.4f), - border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.2f)) - ) { - Row( - modifier = Modifier.fillMaxSize(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = context.getString(android.R.string.yes), - style = MaterialTheme.typography.labelLarge, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.error - ) - } - } - } - } + DialogFooterRow( + dismissLabel = context.getString(android.R.string.no), + confirmLabel = context.getString(android.R.string.yes), + onDismiss = onDismiss, + onConfirm = onConfirm, + confirmEnabled = true, + confirmColor = MaterialTheme.colorScheme.error, + confirmContentColor = MaterialTheme.colorScheme.onError + ) } } } From 09695a239ceffa2a8d144f846db7b479ca9b04c3 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Wed, 5 Aug 2026 23:30:31 +0530 Subject: [PATCH 13/20] fix(ui): use android framework string resources for yes/no in stop container confirmation dialog --- .../java/com/droidspaces/app/ui/screen/ContainersScreen.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt index 1f838429..d81ad78a 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt @@ -621,8 +621,8 @@ private fun StopContainerConfirmationDialog( color = MaterialTheme.colorScheme.onSurfaceVariant ) DialogFooterRow( - dismissLabel = context.getString(R.string.no), - confirmLabel = context.getString(R.string.yes), + dismissLabel = context.getString(android.R.string.no), + confirmLabel = context.getString(android.R.string.yes), onDismiss = onDismiss, onConfirm = onConfirm, confirmEnabled = true, From 1506776a7a6da8bb244c6768b23b8c3443bf9cc2 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Thu, 6 Aug 2026 10:29:33 +0530 Subject: [PATCH 14/20] refactor(ui): unify width of option popups in Edit Container config page to 0.92f --- .../com/droidspaces/app/ui/component/ContainerConfigForm.kt | 3 +-- .../com/droidspaces/app/ui/component/HardwareAccessDialog.kt | 2 +- .../com/droidspaces/app/ui/component/PrivilegedModeDialog.kt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt index 7c06ad7c..cba1b8ad 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/ContainerConfigForm.kt @@ -141,8 +141,7 @@ fun ContainerConfigForm( ) { Surface( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) + .fillMaxWidth(0.92f) .imePadding(), shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceContainer, diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/HardwareAccessDialog.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/HardwareAccessDialog.kt index 74d0ce9a..9436a604 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/HardwareAccessDialog.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/HardwareAccessDialog.kt @@ -34,7 +34,7 @@ fun HardwareAccessDialog( ) { Surface( modifier = Modifier - .fillMaxWidth(0.95f) + .fillMaxWidth(0.92f) .wrapContentHeight(), shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceContainer, diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/PrivilegedModeDialog.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/PrivilegedModeDialog.kt index d40cc94b..dc100a8b 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/PrivilegedModeDialog.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/PrivilegedModeDialog.kt @@ -76,7 +76,7 @@ fun PrivilegedModeDialog( ) { Surface( modifier = Modifier - .fillMaxWidth(0.95f) + .fillMaxWidth(0.92f) .wrapContentHeight(), shape = RoundedCornerShape(24.dp), color = MaterialTheme.colorScheme.surfaceContainer, From 7f8fa640326a25ffb94313e917cf4f7d95c54813 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Thu, 6 Aug 2026 11:51:04 +0530 Subject: [PATCH 15/20] refactor(ui): apply default horizontal padding of 4.dp to DialogFooterRow for concentric styling --- .../java/com/droidspaces/app/ui/component/DialogFooterRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt index 28e1a72e..f57f5e1c 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt @@ -32,7 +32,7 @@ fun DialogFooterRow( confirmLabel: String, onDismiss: () -> Unit, onConfirm: () -> Unit, - modifier: Modifier = Modifier, + modifier: Modifier = Modifier.padding(horizontal = 4.dp), confirmEnabled: Boolean = true, cancelBorderAlpha: Float = 0.4f, textFontWeight: FontWeight = FontWeight.SemiBold, From 9770249a2697143b25b0c5c74853cdb2e69d4920 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Fri, 7 Aug 2026 10:16:53 +0530 Subject: [PATCH 16/20] refactor(ui): unify width of confirmation dialogs to 0.92f and wrap content height --- .../app/ui/screen/ContainerTerminalScreen.kt | 3 +-- .../droidspaces/app/ui/screen/ContainersScreen.kt | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt index 1fcd38bd..cb139a94 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainerTerminalScreen.kt @@ -624,8 +624,7 @@ private fun CloseSessionDialog( ) { Surface( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) + .fillMaxWidth(0.92f) .wrapContentHeight(), shape = dialogShape, color = MaterialTheme.colorScheme.surfaceContainer, diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt index d81ad78a..411ad450 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/screen/ContainersScreen.kt @@ -448,8 +448,8 @@ private fun SparseSizeDialog( ) { Surface( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp) + .fillMaxWidth(0.92f) + .wrapContentHeight() .imePadding(), shape = dialogShape, color = MaterialTheme.colorScheme.surfaceContainer, @@ -507,8 +507,8 @@ private fun UninstallConfirmationDialog( ) { Surface( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + .fillMaxWidth(0.92f) + .wrapContentHeight(), shape = dialogShape, color = MaterialTheme.colorScheme.surfaceContainer, border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.3f)), @@ -591,8 +591,8 @@ private fun StopContainerConfirmationDialog( ) { Surface( modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 24.dp), + .fillMaxWidth(0.92f) + .wrapContentHeight(), shape = dialogShape, color = MaterialTheme.colorScheme.surfaceContainer, border = androidx.compose.foundation.BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)), From dc180ee074cc96e311704951db47972dcff8be58 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Fri, 7 Aug 2026 11:16:15 +0530 Subject: [PATCH 17/20] refactor(ui): apply uniform 4.dp padding on all sides of DialogFooterRow --- .../java/com/droidspaces/app/ui/component/DialogFooterRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt index f57f5e1c..2cee8c8f 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt @@ -32,7 +32,7 @@ fun DialogFooterRow( confirmLabel: String, onDismiss: () -> Unit, onConfirm: () -> Unit, - modifier: Modifier = Modifier.padding(horizontal = 4.dp), + modifier: Modifier = Modifier.padding(4.dp), confirmEnabled: Boolean = true, cancelBorderAlpha: Float = 0.4f, textFontWeight: FontWeight = FontWeight.SemiBold, From c926890fc2fa32f2ec6a5dc46c269033a752e2d1 Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Fri, 7 Aug 2026 11:48:25 +0530 Subject: [PATCH 18/20] refactor(ui): update Row padding modifier order in DialogFooterRow --- .../java/com/droidspaces/app/ui/component/DialogFooterRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt index 2cee8c8f..fe841c83 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt @@ -46,7 +46,7 @@ fun DialogFooterRow( border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)) ) { Row( - modifier = Modifier.fillMaxWidth().padding(4.dp), + modifier = Modifier.padding(4.dp).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { // Dismiss/Cancel Button From 2a84a23578fcef7c37abb823340e9836886154ea Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Fri, 7 Aug 2026 14:46:24 +0530 Subject: [PATCH 19/20] fix(ui): remove horizontal padding from DialogFooterRow default modifier to align outline --- .../java/com/droidspaces/app/ui/component/DialogFooterRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt index fe841c83..606cd499 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt @@ -32,7 +32,7 @@ fun DialogFooterRow( confirmLabel: String, onDismiss: () -> Unit, onConfirm: () -> Unit, - modifier: Modifier = Modifier.padding(4.dp), + modifier: Modifier = Modifier.padding(vertical = 4.dp), confirmEnabled: Boolean = true, cancelBorderAlpha: Float = 0.4f, textFontWeight: FontWeight = FontWeight.SemiBold, From fde3cc093fa5ff72fe9a4d027a33b0f9f4972cab Mon Sep 17 00:00:00 2001 From: VizXtreme Date: Fri, 7 Aug 2026 15:11:51 +0530 Subject: [PATCH 20/20] fix(ui): increase horizontal padding inside DialogFooterRow outline to 8.dp --- .../java/com/droidspaces/app/ui/component/DialogFooterRow.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt index 606cd499..4f1144ce 100644 --- a/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt +++ b/Android/app/src/main/java/com/droidspaces/app/ui/component/DialogFooterRow.kt @@ -46,7 +46,7 @@ fun DialogFooterRow( border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f)) ) { Row( - modifier = Modifier.padding(4.dp).fillMaxWidth(), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp).fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp) ) { // Dismiss/Cancel Button