fix(ui): единый скролл на экране станций + всегда видимый навбар
- StationsScreen: закреплённые заголовок/поиск/жанры, одна прокручиваемая сетка станций; поиск и фильтры больше не исчезают при пустом результате (+ кнопка «Сбросить фильтры») - таб-бар показывается без обязательного входа (скрыт только на экране входа) - старт сразу со «Станций» — авторизация необязательна, вход из Настроек
This commit is contained in:
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user