feat: auth screen with auto-redirect, sync favorites/history with backend

This commit is contained in:
nk
2026-06-02 19:12:07 +03:00
parent d4adb1e7be
commit a83672b455
2934 changed files with 97351 additions and 163 deletions

View File

@@ -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) }
)
}
}
}
}
}

View File

@@ -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
}