fix(ui): единый скролл на экране станций + всегда видимый навбар

- StationsScreen: закреплённые заголовок/поиск/жанры, одна прокручиваемая
  сетка станций; поиск и фильтры больше не исчезают при пустом результате
  (+ кнопка «Сбросить фильтры»)
- таб-бар показывается без обязательного входа (скрыт только на экране входа)
- старт сразу со «Станций» — авторизация необязательна, вход из Настроек
This commit is contained in:
nk
2026-06-02 21:58:11 +03:00
parent 220d1d6fa1
commit af13272852
2 changed files with 87 additions and 74 deletions

View File

@@ -9,9 +9,11 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.radiola.data.local.TokenDataStore import com.radiola.data.local.TokenDataStore
@@ -65,12 +67,17 @@ class MainActivity : ComponentActivity() {
val isRecording by playerViewModel.isRecording.collectAsState() val isRecording by playerViewModel.isRecording.collectAsState()
val isLoggedIn by tokenDataStore.isLoggedIn.collectAsState(initial = false) val isLoggedIn by tokenDataStore.isLoggedIn.collectAsState(initial = false)
val startDestination = remember(isLoggedIn) { // Авторизация необязательна — всегда стартуем со станций.
if (isLoggedIn) NavDestinations.Stations.route else NavDestinations.Auth.route // Вход доступен из Настроек.
} val startDestination = NavDestinations.Stations.route
val currentRoute = navController
.currentBackStackEntryAsState().value?.destination?.route
val showChrome = currentRoute != NavDestinations.Auth.route
Scaffold( Scaffold(
bottomBar = { bottomBar = {
if (showChrome) {
Column { Column {
if (currentStation != null) { if (currentStation != null) {
MiniPlayer( MiniPlayer(
@@ -80,8 +87,10 @@ class MainActivity : ComponentActivity() {
onClick = { showPlayer = true }, onClick = { showPlayer = true },
onPlayPause = { playerViewModel.togglePlayPause() } onPlayPause = { playerViewModel.togglePlayPause() }
) )
Spacer(Modifier.height(8.dp))
} }
if (isLoggedIn) { // Навигация доступна и без входа — приложением можно
// пользоваться анонимно.
BottomNavBar(navController) BottomNavBar(navController)
} }
} }

View File

@@ -1,12 +1,8 @@
package com.radiola.ui.stations package com.radiola.ui.stations
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.slideInVertically
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -18,6 +14,8 @@ import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Radio
import com.radiola.domain.model.Station import com.radiola.domain.model.Station
import com.radiola.ui.components.* import com.radiola.ui.components.*
import com.radiola.ui.theme.RadiolaTheme import com.radiola.ui.theme.RadiolaTheme
@@ -38,11 +36,7 @@ fun StationsScreen(
val favoriteIds by viewModel.favoriteIds.collectAsState() val favoriteIds by viewModel.favoriteIds.collectAsState()
val colors = RadiolaTheme.colors val colors = RadiolaTheme.colors
Column( Column(modifier = modifier.fillMaxSize()) {
modifier = modifier
.fillMaxSize()
.padding(horizontal = 20.dp)
) {
// Двухцветный заголовок экрана // Двухцветный заголовок экрана
Text( Text(
text = buildAnnotatedString { text = buildAnnotatedString {
@@ -50,38 +44,61 @@ fun StationsScreen(
withStyle(SpanStyle(color = colors.accent)) { append("радио") } withStyle(SpanStyle(color = colors.accent)) { append("радио") }
}, },
style = MaterialTheme.typography.headlineLarge, style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(top = 20.dp, bottom = 16.dp) modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 16.dp)
) )
// Поиск — всегда виден (в т.ч. когда результатов нет)
SearchBar(
query = searchQuery,
onQueryChange = viewModel::onSearchQueryChange,
modifier = Modifier.padding(horizontal = 20.dp)
)
Spacer(Modifier.height(12.dp))
// Жанры — всегда видны
if (tags.isNotEmpty()) {
FilterChips(
tags = tags,
selectedTag = selectedTag,
onTagSelected = viewModel::onTagSelected
)
Spacer(Modifier.height(8.dp))
}
// Область результатов — единственная прокручиваемая зона
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
when { when {
isLoading && stations.isEmpty() -> Box( isLoading && stations.isEmpty() -> {
modifier = Modifier.fillMaxSize(), CircularProgressIndicator(
contentAlignment = Alignment.Center color = colors.accent,
) { modifier = Modifier.align(Alignment.Center)
CircularProgressIndicator(color = colors.accent) )
} }
stations.isEmpty() -> { stations.isEmpty() -> {
AnimatedVisibility(
visible = true,
enter = fadeIn() + slideInVertically()
) {
Column( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.Center
) { ) {
EmptyState(message = error ?: "Станции не найдены") EmptyState(
if (selectedTag != null) { message = error
?: if (searchQuery.isNotBlank() || selectedTag != null)
"Ничего не найдено" else "Станции не найдены",
icon = Lucide.Radio,
modifier = Modifier.wrapContentSize()
)
if (searchQuery.isNotBlank() || selectedTag != null) {
Spacer(Modifier.height(12.dp))
OutlinedButton( OutlinedButton(
onClick = { viewModel.onTagSelected(null) }, onClick = {
colors = ButtonDefaults.outlinedButtonColors( viewModel.onSearchQueryChange("")
contentColor = colors.accent viewModel.onTagSelected(null)
), },
colors = ButtonDefaults.outlinedButtonColors(contentColor = colors.accent),
border = androidx.compose.foundation.BorderStroke(1.dp, colors.accent) border = androidx.compose.foundation.BorderStroke(1.dp, colors.accent)
) { ) {
Text("Показать все") Text("Сбросить фильтры")
}
} }
} }
} }
@@ -90,24 +107,10 @@ fun StationsScreen(
else -> LazyVerticalGrid( else -> LazyVerticalGrid(
columns = GridCells.Fixed(2), columns = GridCells.Fixed(2),
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = 16.dp), contentPadding = PaddingValues(start = 20.dp, end = 20.dp, top = 4.dp, bottom = 20.dp),
horizontalArrangement = Arrangement.spacedBy(14.dp), horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalArrangement = Arrangement.spacedBy(14.dp) verticalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
item(span = { GridItemSpan(maxLineSpan) }) {
SearchBar(
query = searchQuery,
onQueryChange = viewModel::onSearchQueryChange,
)
}
item(span = { GridItemSpan(maxLineSpan) }) {
FilterChips(
tags = tags,
selectedTag = selectedTag,
onTagSelected = viewModel::onTagSelected,
modifier = Modifier.padding(vertical = 8.dp)
)
}
items(stations, key = { it.id }) { station -> items(stations, key = { it.id }) { station ->
StationCard( StationCard(
station = station, station = station,
@@ -121,3 +124,4 @@ fun StationsScreen(
} }
} }
} }
}