ui: float the offline indicator opposite the library FAB

Drop the strip above the tab bar, which kept the bottom of the library
busy, and go back to a floating tonal cloud-off button at the bottom
start, level with the FAB on the other side. Tapping it still leaves
offline mode, and the tab bar goes back to just tabs and the gear.
This commit is contained in:
austinried
2026-10-21 10:14:10 +00:00
parent b76328fc26
commit 3ffe38d988
3 changed files with 20 additions and 52 deletions
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -46,6 +45,7 @@ import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButton
@@ -90,7 +90,6 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.clipPath import androidx.compose.ui.graphics.drawscope.clipPath
import androidx.compose.ui.graphics.drawscope.clipRect import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.boundsInParent import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
@@ -100,7 +99,6 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.semantics
@@ -593,9 +591,6 @@ fun LibraryScreen(
.onSizeChanged { tabBarHeightPx = it.height.toFloat() } .onSizeChanged { tabBarHeightPx = it.height.toFloat() }
.padding(bottom = bottomInset), .padding(bottom = bottomInset),
) { ) {
if (offline) {
OfflineStrip(onGoOnline = onExitOffline, headerColor = headerColor)
}
LibraryTabs( LibraryTabs(
pagerState = pagerState, pagerState = pagerState,
onTabSelected = { tab -> onTabSelected = { tab ->
@@ -623,6 +618,20 @@ fun LibraryScreen(
} }
if (!searchActive) { if (!searchActive) {
if (offline) {
FilledTonalIconButton(
onClick = onExitOffline,
modifier =
Modifier
.align(Alignment.BottomStart)
.padding(start = 16.dp, bottom = tabBarHeight + 16.dp),
) {
Icon(
imageVector = Icons.Rounded.CloudOff,
contentDescription = stringResource(R.string.offline_mode_status),
)
}
}
val searchFab = selectedTab == LibraryTab.Home val searchFab = selectedTab == LibraryTab.Home
FloatingActionButton( FloatingActionButton(
onClick = { if (searchFab) onOpenSearch() else showOptions = true }, onClick = { if (searchFab) onOpenSearch() else showOptions = true },
@@ -837,7 +846,6 @@ private val TAB_ICON_SIZE = 24.dp
private val TAB_VERTICAL_PADDING = 6.dp private val TAB_VERTICAL_PADDING = 6.dp
private val TOUCH_TARGET = 48.dp private val TOUCH_TARGET = 48.dp
private const val LIST_OPTIONS_WEIGHT = 0.11f private const val LIST_OPTIONS_WEIGHT = 0.11f
private const val OFFLINE_STRIP_TINT = 0.10f
private val LIST_OPTIONS_BAR_WIDTHS = listOf(1f, 0.62f, 0.34f) private val LIST_OPTIONS_BAR_WIDTHS = listOf(1f, 0.62f, 0.34f)
@Composable @Composable
@@ -1015,44 +1023,6 @@ internal fun LibraryTabs(
} }
} }
@Composable
private fun OfflineStrip(
onGoOnline: () -> Unit,
headerColor: Color,
modifier: Modifier = Modifier,
) {
Row(
modifier =
modifier
.fillMaxWidth()
.background(lerp(headerColor, MaterialTheme.colorScheme.primary, OFFLINE_STRIP_TINT))
.heightIn(min = 48.dp)
.clickable(role = Role.Button, onClick = onGoOnline)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
imageVector = Icons.Rounded.CloudOff,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(18.dp),
)
Text(
text = stringResource(R.string.offline_mode_banner),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
maxLines = 2,
modifier = Modifier.weight(1f),
)
Text(
text = stringResource(R.string.offline_go_online),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
}
}
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun TabButton( private fun TabButton(
+1 -2
View File
@@ -175,8 +175,7 @@
<string name="filters">Filters</string> <string name="filters">Filters</string>
<string name="filter_downloaded">Downloaded</string> <string name="filter_downloaded">Downloaded</string>
<string name="close_search">Close search</string> <string name="close_search">Close search</string>
<string name="offline_mode_banner">Offline mode</string> <string name="offline_mode_status">Offline mode; tap to go online</string>
<string name="offline_go_online">Go online</string>
<string name="shuffle_artist">Shuffle artist</string> <string name="shuffle_artist">Shuffle artist</string>
<string name="status_playing">Playing</string> <string name="status_playing">Playing</string>
@@ -510,7 +510,7 @@ class LibraryScreenTest {
@Test @Test
@Config(qualifiers = "de-rDE-w320dp-h640dp-xhdpi") @Config(qualifiers = "de-rDE-w320dp-h640dp-xhdpi")
fun theOfflineStripExplainsTheModeAndGoesOnline() { fun theOfflineIndicatorGoesOnline() {
var exited = false var exited = false
composeRule.setContent { composeRule.setContent {
SubtracksTheme { SubtracksTheme {
@@ -533,11 +533,10 @@ class LibraryScreenTest {
} }
composeRule.waitForIdle() composeRule.waitForIdle()
composeRule.onNodeWithText(composeRule.activity.getString(R.string.offline_mode_banner)).performClick()
assertTrue("the offline strip should leave offline mode", exited)
composeRule composeRule
.onNodeWithContentDescription(composeRule.activity.getString(R.string.navigation_tabs_settings)) .onNodeWithContentDescription(composeRule.activity.getString(R.string.offline_mode_status))
.assertWidthIsEqualTo(40.dp) .performClick()
assertTrue("the offline indicator should leave offline mode", exited)
} }
private fun tabBounds(tab: LibraryTab) = private fun tabBounds(tab: LibraryTab) =