From 3ffe38d988f36799f0eff7ebd7529548c9395626 Mon Sep 17 00:00:00 2001
From: austinried <4966622+austinried@users.noreply.github.com>
Date: Wed, 21 Oct 2026 07:45:26 +0000
Subject: [PATCH] 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.
---
.../com/subtracks/ui/library/LibraryScreen.kt | 60 +++++--------------
app/src/main/res/values/strings.xml | 3 +-
.../com/subtracks/ui/LibraryScreenTest.kt | 9 ++-
3 files changed, 20 insertions(+), 52 deletions(-)
diff --git a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt
index f1cd8648..ba86d3db 100644
--- a/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt
+++ b/app/src/main/kotlin/com/subtracks/ui/library/LibraryScreen.kt
@@ -17,7 +17,6 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
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.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
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.clipRect
import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.graphics.lerp
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.boundsInParent
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.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.clearAndSetSemantics
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
@@ -593,9 +591,6 @@ fun LibraryScreen(
.onSizeChanged { tabBarHeightPx = it.height.toFloat() }
.padding(bottom = bottomInset),
) {
- if (offline) {
- OfflineStrip(onGoOnline = onExitOffline, headerColor = headerColor)
- }
LibraryTabs(
pagerState = pagerState,
onTabSelected = { tab ->
@@ -623,6 +618,20 @@ fun LibraryScreen(
}
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
FloatingActionButton(
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 TOUCH_TARGET = 48.dp
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)
@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)
@Composable
private fun TabButton(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index ed5b5a0d..af58820e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -175,8 +175,7 @@
Filters
Downloaded
Close search
- Offline mode
- Go online
+ Offline mode; tap to go online
Shuffle artist
Playing
diff --git a/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt b/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt
index 646d748f..a31bef1a 100644
--- a/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt
+++ b/app/src/test/kotlin/com/subtracks/ui/LibraryScreenTest.kt
@@ -510,7 +510,7 @@ class LibraryScreenTest {
@Test
@Config(qualifiers = "de-rDE-w320dp-h640dp-xhdpi")
- fun theOfflineStripExplainsTheModeAndGoesOnline() {
+ fun theOfflineIndicatorGoesOnline() {
var exited = false
composeRule.setContent {
SubtracksTheme {
@@ -533,11 +533,10 @@ class LibraryScreenTest {
}
composeRule.waitForIdle()
- composeRule.onNodeWithText(composeRule.activity.getString(R.string.offline_mode_banner)).performClick()
- assertTrue("the offline strip should leave offline mode", exited)
composeRule
- .onNodeWithContentDescription(composeRule.activity.getString(R.string.navigation_tabs_settings))
- .assertWidthIsEqualTo(40.dp)
+ .onNodeWithContentDescription(composeRule.activity.getString(R.string.offline_mode_status))
+ .performClick()
+ assertTrue("the offline indicator should leave offline mode", exited)
}
private fun tabBounds(tab: LibraryTab) =