Radio ROKS и Kiss FM (TavR Media, хосты radioroks.ua / kissfm.ua) недоступны с российских IP без VPN. Теперь для пользователей из РФ они полностью скрыты — и сами станции (везде, где используется список), и их чипы-категории. Страна определяется по IP (api.country.is → ipapi.co; при VPN вернёт страну выходного узла, тогда станции доступны и НЕ скрываются), с фолбэком на страну SIM/сети/локали устройства, если IP-сервис недоступен (в РФ часто заблокирован). Код страны кэшируется (DataStore). Фильтр в GetStationsUseCase (combine со страной) + чипы в StationsViewModel. id 741 «Радио РОКС» (stream.roks.com) — российская, под правило не попадает. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
131 lines
5.2 KiB
Kotlin
131 lines
5.2 KiB
Kotlin
package com.radiola.ui.stations
|
||
|
||
import androidx.lifecycle.ViewModel
|
||
import androidx.lifecycle.viewModelScope
|
||
import com.radiola.domain.geo.GeoBlock
|
||
import com.radiola.domain.model.Station
|
||
import com.radiola.domain.model.Track
|
||
import com.radiola.domain.repository.FavoritesRepository
|
||
import com.radiola.domain.repository.NowPlayingRepository
|
||
import com.radiola.domain.repository.RegionRepository
|
||
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 com.radiola.service.PlayerController
|
||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||
import kotlinx.coroutines.delay
|
||
import kotlinx.coroutines.flow.*
|
||
import kotlinx.coroutines.launch
|
||
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 favoritesRepository: FavoritesRepository,
|
||
private val stationRepository: StationRepository,
|
||
private val nowPlayingRepository: NowPlayingRepository,
|
||
private val regionRepository: RegionRepository,
|
||
private val playerController: PlayerController
|
||
) : ViewModel() {
|
||
|
||
// Активная (играющая) станция — для подсветки карточки в списке.
|
||
val playingStationId: StateFlow<Int?> = playerController.currentStationId
|
||
val isPlaying: StateFlow<Boolean> = playerController.isPlaying
|
||
|
||
private val _searchQuery = MutableStateFlow("")
|
||
val searchQuery: StateFlow<String> = _searchQuery.asStateFlow()
|
||
|
||
private val _selectedTag = MutableStateFlow<String?>(null)
|
||
val selectedTag: StateFlow<String?> = _selectedTag.asStateFlow()
|
||
|
||
private val _isLoading = MutableStateFlow(false)
|
||
val isLoading: StateFlow<Boolean> = _isLoading.asStateFlow()
|
||
|
||
private val _error = MutableStateFlow<String?>(null)
|
||
val error: StateFlow<String?> = _error.asStateFlow()
|
||
|
||
val stations: StateFlow<List<Station>> = combine(
|
||
getStationsUseCase(),
|
||
_searchQuery,
|
||
_selectedTag
|
||
) { allStations, query, tag ->
|
||
allStations
|
||
.filter { station ->
|
||
tag == null ||
|
||
station.genre.equals(tag, ignoreCase = true) ||
|
||
station.tags.any { it.equals(tag, ignoreCase = true) }
|
||
}
|
||
.filter { station ->
|
||
query.isBlank() ||
|
||
station.name.contains(query, ignoreCase = true) ||
|
||
station.genre.contains(query, ignoreCase = true)
|
||
}
|
||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||
|
||
// Чипы-жанры. Для пользователей из РФ убираем жанры украинских станций
|
||
// (Radio ROKS, Kiss FM) — их чипы не показываем вовсе.
|
||
val tags: StateFlow<List<String>> = combine(
|
||
stationRepository.getTags(),
|
||
stationRepository.getStations(),
|
||
regionRepository.countryCode()
|
||
) { tags, allStations, country ->
|
||
if (GeoBlock.shouldHideUa(country)) {
|
||
val uaGenres = allStations.filter { GeoBlock.isUaStation(it) }.map { it.genre }.toSet()
|
||
tags.filterNot { it in uaGenres }
|
||
} else {
|
||
tags
|
||
}
|
||
}.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())
|
||
|
||
// Текущие треки по id станции (каталожный == station.id) — без коллизий по имени.
|
||
val nowPlaying: StateFlow<Map<Int, Track>> = nowPlayingRepository.getAllNowPlaying()
|
||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||
|
||
init {
|
||
// Определяем страну пользователя по IP (для гео-фильтрации станций).
|
||
viewModelScope.launch { regionRepository.refresh() }
|
||
viewModelScope.launch {
|
||
_isLoading.value = true
|
||
refreshStationsUseCase()
|
||
.onFailure { _error.value = it.localizedMessage ?: "Ошибка загрузки" }
|
||
_isLoading.value = false
|
||
}
|
||
// Периодическое обновление now-playing каждые 20 секунд.
|
||
viewModelScope.launch {
|
||
while (true) {
|
||
nowPlayingRepository.refreshNowPlaying()
|
||
delay(20_000)
|
||
}
|
||
}
|
||
}
|
||
|
||
fun onSearchQueryChange(query: String) {
|
||
_searchQuery.value = query
|
||
}
|
||
|
||
fun onTagSelected(tag: String?) {
|
||
_selectedTag.value = tag
|
||
}
|
||
|
||
fun playStation(station: Station) {
|
||
viewModelScope.launch {
|
||
playStationUseCase(station)
|
||
}
|
||
}
|
||
|
||
fun toggleFavorite(station: Station) {
|
||
viewModelScope.launch {
|
||
toggleFavoriteUseCase(station)
|
||
}
|
||
}
|
||
}
|