Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ebfd1ea
UI: updated spinner and PullToRefresh anim, corner radius in systemd …
VizXtreme Aug 2, 2026
ca25865
UI: restore graphics-shapes dependency to resolve compilation failure
VizXtreme Aug 3, 2026
b957635
docs: add changelog.md for ui branch
VizXtreme Aug 3, 2026
ba3e8f3
Delete changelog.md
VizXtreme Aug 3, 2026
a330e61
refactor(ui): update storage bar progress indicator and terminal UI t…
VizXtreme Aug 3, 2026
143022c
fix(ui): replace Tab with Box in terminal screen to fix click and lon…
VizXtreme Aug 3, 2026
3e3b4f1
refactor(ui): apply concentric double-outline border to active tab an…
VizXtreme Aug 3, 2026
e7b0cb8
refactor(ui): smoothly animate bottom navigation items and background…
VizXtreme Aug 3, 2026
1eb4428
fix(ui): import lerp with alias and resolve experimental api usage in…
VizXtreme Aug 3, 2026
03edbd4
refactor(ui): update DialogFooterRow with shared concentric outline a…
VizXtreme Aug 3, 2026
8bb8aa0
refactor(ui): add stop container confirmation dialog and resolve outl…
VizXtreme Aug 5, 2026
d0ffc38
refactor(ui): update CloseSessionDialog to use DialogFooterRow for de…
VizXtreme Aug 5, 2026
09695a2
fix(ui): use android framework string resources for yes/no in stop co…
VizXtreme Aug 5, 2026
1506776
refactor(ui): unify width of option popups in Edit Container config p…
VizXtreme Aug 6, 2026
7f8fa64
refactor(ui): apply default horizontal padding of 4.dp to DialogFoote…
VizXtreme Aug 6, 2026
9770249
refactor(ui): unify width of confirmation dialogs to 0.92f and wrap c…
VizXtreme Aug 7, 2026
dc180ee
refactor(ui): apply uniform 4.dp padding on all sides of DialogFooterRow
VizXtreme Aug 7, 2026
c926890
refactor(ui): update Row padding modifier order in DialogFooterRow
VizXtreme Aug 7, 2026
2a84a23
fix(ui): remove horizontal padding from DialogFooterRow default modif…
VizXtreme Aug 7, 2026
fde3cc0
fix(ui): increase horizontal padding inside DialogFooterRow outline t…
VizXtreme Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ output/
.claude
Android/app/src/main/assets/binaries/
Android/app/src/main/assets/supported_locales.txt
md3/

# Keystores
*.keystore
Expand All @@ -25,4 +26,4 @@ compile_commands.json
*.qcow2

# Android stuff
.gradle
.gradle
1 change: 1 addition & 0 deletions Android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -168,43 +167,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("/")
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,38 +32,56 @@ fun DialogFooterRow(
confirmLabel: String,
onDismiss: () -> Unit,
onConfirm: () -> Unit,
modifier: Modifier = Modifier,
modifier: Modifier = Modifier.padding(vertical = 4.dp),
confirmEnabled: Boolean = true,
cancelBorderAlpha: Float = 0.4f,
textFontWeight: FontWeight = FontWeight.SemiBold,
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.padding(horizontal = 8.dp, vertical = 4.dp).fillMaxWidth(),
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)
)
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -236,7 +238,7 @@ fun FilePickerDialog(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
LoadingIndicator(size = LoadingSize.Medium)
}
} else {
LazyColumn(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fun HardwareAccessDialog(
) {
Surface(
modifier = Modifier
.fillMaxWidth(0.95f)
.fillMaxWidth(0.92f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surfaceContainer,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ fun PrivilegedModeDialog(
) {
Surface(
modifier = Modifier
.fillMaxWidth(0.95f)
.fillMaxWidth(0.92f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
color = MaterialTheme.colorScheme.surfaceContainer,
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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

/**
Expand All @@ -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
Expand Down Expand Up @@ -54,24 +63,80 @@ 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()
.nestedScroll(pullToRefreshState.nestedScrollConnection)
) {
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
)
}
}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -36,6 +37,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
Expand Down Expand Up @@ -217,7 +220,7 @@ private fun RepoLoadingContent() {
.height(240.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
LoadingIndicator(size = LoadingSize.Medium)
}
}

Expand Down Expand Up @@ -484,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}%",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,7 @@ fun DroidspacesNavigation(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
LoadingIndicator()
LoadingIndicator(size = LoadingSize.Medium)
}
} else {
containerInfo?.let { container ->
Expand Down
Loading