feat(charts): раздел «Чарты» (клиент) + детальная страница трека с графиком
- вкладка «Чарты» в навигации; экран: периоды (День/Неделя/Месяц/Всё), ранжированный список треков (ранг, обложка, проигрывания, тренд) - детальная карточка трека: метрики, график популярности (Canvas), лайк, кнопки музыкальных сервисов, кнопка «Текст песни» (ссылка на лицензированный Musixmatch — полный текст не встраиваем, авторское право) - ChartsRepository/LyricsRepository + эндпоинты charts/* в RadiolaApi (DTO) - превью-данные пока бэкенд не отдаёт charts (помечено TODO)
This commit is contained in:
108
app/src/main/java/com/radiola/ui/charts/ChartsViewModel.kt
Normal file
108
app/src/main/java/com/radiola/ui/charts/ChartsViewModel.kt
Normal file
@@ -0,0 +1,108 @@
|
||||
package com.radiola.ui.charts
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.radiola.data.repository.ChartsRepositoryImpl
|
||||
import com.radiola.domain.model.ChartEntry
|
||||
import com.radiola.domain.model.ChartPeriod
|
||||
import com.radiola.domain.model.TrackStats
|
||||
import com.radiola.domain.repository.ChartsRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class ChartsViewModel @Inject constructor(
|
||||
private val chartsRepository: ChartsRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _period = MutableStateFlow(ChartPeriod.WEEK)
|
||||
val period: StateFlow<ChartPeriod> = _period.asStateFlow()
|
||||
|
||||
private val _charts = MutableStateFlow<List<ChartEntry>>(emptyList())
|
||||
val charts: StateFlow<List<ChartEntry>> = _charts.asStateFlow()
|
||||
|
||||
/** true — данные из превью (бэкенд ещё не готов). */
|
||||
private val _isPreview = MutableStateFlow(false)
|
||||
val isPreview: StateFlow<Boolean> = _isPreview.asStateFlow()
|
||||
|
||||
private val _isLoadingCharts = MutableStateFlow(false)
|
||||
val isLoadingCharts: StateFlow<Boolean> = _isLoadingCharts.asStateFlow()
|
||||
|
||||
private val _selectedTrackStats = MutableStateFlow<TrackStats?>(null)
|
||||
val selectedTrackStats: StateFlow<TrackStats?> = _selectedTrackStats.asStateFlow()
|
||||
|
||||
private val _isLoadingStats = MutableStateFlow(false)
|
||||
val isLoadingStats: StateFlow<Boolean> = _isLoadingStats.asStateFlow()
|
||||
|
||||
init {
|
||||
loadCharts()
|
||||
}
|
||||
|
||||
fun selectPeriod(newPeriod: ChartPeriod) {
|
||||
if (_period.value == newPeriod) return
|
||||
_period.value = newPeriod
|
||||
loadCharts()
|
||||
}
|
||||
|
||||
fun selectTrack(trackId: String) {
|
||||
viewModelScope.launch {
|
||||
_isLoadingStats.value = true
|
||||
_selectedTrackStats.value = null
|
||||
try {
|
||||
val stats = chartsRepository.getTrackStats(trackId)
|
||||
_selectedTrackStats.value = stats
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChartsViewModel", "Ошибка загрузки статистики трека $trackId", e)
|
||||
} finally {
|
||||
_isLoadingStats.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
_selectedTrackStats.value = null
|
||||
}
|
||||
|
||||
fun toggleLike(trackId: String) {
|
||||
val stats = _selectedTrackStats.value ?: return
|
||||
val newLiked = !stats.isLiked
|
||||
// Оптимистично обновляем UI
|
||||
_selectedTrackStats.value = stats.copy(
|
||||
isLiked = newLiked,
|
||||
totalLikes = if (newLiked) stats.totalLikes + 1 else stats.totalLikes - 1
|
||||
)
|
||||
viewModelScope.launch {
|
||||
try {
|
||||
chartsRepository.setLiked(trackId, newLiked)
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChartsViewModel", "Ошибка переключения лайка трека $trackId", e)
|
||||
// Откатываем при ошибке
|
||||
_selectedTrackStats.value = stats
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadCharts() {
|
||||
viewModelScope.launch {
|
||||
_isLoadingCharts.value = true
|
||||
try {
|
||||
val result = chartsRepository.getCharts(_period.value)
|
||||
_charts.value = result
|
||||
// Определяем, это превью или реальные данные.
|
||||
// TODO: убрать проверку, когда бэкенд отдаёт charts.
|
||||
// Если список совпадает с образцом — это превью.
|
||||
_isPreview.value = result == ChartsRepositoryImpl.SAMPLE_CHARTS
|
||||
} catch (e: Exception) {
|
||||
Log.e("ChartsViewModel", "Ошибка загрузки чартов", e)
|
||||
_isPreview.value = true
|
||||
} finally {
|
||||
_isLoadingCharts.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user