Files
radiola-android/app/src/main/java/com/radiola/ui/stations/StationsViewModel.kt
nk 8d2c53c441 feat(stations): скрывать украинские станции (ROKS, Kiss FM) для РФ
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>
2026-06-04 14:46:42 +03:00

131 lines
5.2 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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