Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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 @@ -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
@@ -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 @@ -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
Expand Down Expand Up @@ -217,7 +219,7 @@ private fun RepoLoadingContent() {
.height(240.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
LoadingIndicator(size = LoadingSize.Medium)
}
}

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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
Loading
Loading