feat: auth screen with auto-redirect, sync favorites/history with backend
This commit is contained in:
@@ -2,13 +2,16 @@ package com.radiola.ui.stations
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
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.items
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.radiola.domain.model.Station
|
||||
import com.radiola.ui.components.*
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -24,54 +27,68 @@ fun StationsScreen(
|
||||
val selectedTag by viewModel.selectedTag.collectAsState()
|
||||
val isLoading by viewModel.isLoading.collectAsState()
|
||||
val error by viewModel.error.collectAsState()
|
||||
val favoriteIds by viewModel.favoriteIds.collectAsState()
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Радио") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
)
|
||||
Column(modifier = modifier.fillMaxSize()) {
|
||||
TopAppBar(
|
||||
title = { Text("Радио") },
|
||||
colors = TopAppBarDefaults.topAppBarColors(
|
||||
containerColor = MaterialTheme.colorScheme.background
|
||||
)
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
SearchBar(
|
||||
query = searchQuery,
|
||||
onQueryChange = viewModel::onSearchQueryChange,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
FilterChips(
|
||||
tags = tags,
|
||||
selectedTag = selectedTag,
|
||||
onTagSelected = viewModel::onTagSelected,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
)
|
||||
when {
|
||||
isLoading -> Box(modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
error != null -> EmptyState(message = error!!)
|
||||
stations.isEmpty() -> EmptyState(message = "Станции не найдены")
|
||||
else -> LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
)
|
||||
when {
|
||||
isLoading && stations.isEmpty() -> Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
stations.isEmpty() -> Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
items(stations, key = { it.id }) { station ->
|
||||
StationCard(
|
||||
station = station,
|
||||
onClick = { onStationClick(station) }
|
||||
)
|
||||
EmptyState(message = error ?: "Станции не найдены")
|
||||
if (selectedTag != null) {
|
||||
Button(onClick = { viewModel.onTagSelected(null) }) {
|
||||
Text("Показать все")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> LazyVerticalGrid(
|
||||
columns = GridCells.Fixed(2),
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(12.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 ->
|
||||
StationCard(
|
||||
station = station,
|
||||
isFavorite = favoriteIds.contains(station.id),
|
||||
onClick = { onStationClick(station) },
|
||||
onFavoriteClick = { viewModel.toggleFavorite(station) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,11 @@ package com.radiola.ui.stations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.radiola.domain.model.Station
|
||||
import com.radiola.domain.repository.FavoritesRepository
|
||||
import com.radiola.domain.repository.StationRepository
|
||||
import com.radiola.domain.usecase.GetStationsUseCase
|
||||
import com.radiola.domain.usecase.PlayStationUseCase
|
||||
import com.radiola.domain.usecase.RefreshStationsUseCase
|
||||
import com.radiola.domain.usecase.ToggleFavoriteUseCase
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.*
|
||||
@@ -14,8 +17,11 @@ import javax.inject.Inject
|
||||
@HiltViewModel
|
||||
class StationsViewModel @Inject constructor(
|
||||
private val getStationsUseCase: GetStationsUseCase,
|
||||
private val refreshStationsUseCase: RefreshStationsUseCase,
|
||||
private val playStationUseCase: PlayStationUseCase,
|
||||
private val toggleFavoriteUseCase: ToggleFavoriteUseCase
|
||||
private val toggleFavoriteUseCase: ToggleFavoriteUseCase,
|
||||
private val favoritesRepository: FavoritesRepository,
|
||||
private val stationRepository: StationRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _searchQuery = MutableStateFlow("")
|
||||
@@ -37,7 +43,9 @@ class StationsViewModel @Inject constructor(
|
||||
) { allStations, query, tag ->
|
||||
allStations
|
||||
.filter { station ->
|
||||
tag == null || station.tags.contains(tag) || station.genre.equals(tag, ignoreCase = true)
|
||||
tag == null ||
|
||||
station.genre.equals(tag, ignoreCase = true) ||
|
||||
station.tags.any { it.equals(tag, ignoreCase = true) }
|
||||
}
|
||||
.filter { station ->
|
||||
query.isBlank() ||
|
||||
@@ -46,10 +54,22 @@ class StationsViewModel @Inject constructor(
|
||||
}
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
val tags: StateFlow<List<String>> = getStationsUseCase()
|
||||
.map { stations -> stations.flatMap { it.tags }.distinct().sorted() }
|
||||
val tags: StateFlow<List<String>> = stationRepository.getTags()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
val favoriteIds: StateFlow<Set<Int>> = favoritesRepository.getFavorites()
|
||||
.map { list -> list.map { it.id }.toSet() }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptySet())
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
_isLoading.value = true
|
||||
refreshStationsUseCase()
|
||||
.onFailure { _error.value = it.localizedMessage ?: "Ошибка загрузки" }
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun onSearchQueryChange(query: String) {
|
||||
_searchQuery.value = query
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user