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

@@ -3,16 +3,21 @@ package com.radiola.ui.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.radiola.domain.model.DeeplinkService
import com.radiola.domain.model.StationTestStatus
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onNavigateToAuth: () -> Unit,
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = hiltViewModel()
) {
@@ -20,7 +25,14 @@ fun SettingsScreen(
val enabledServices by viewModel.enabledServices.collectAsState()
val equalizerPreset by viewModel.equalizerPreset.collectAsState()
val isRecordingEnabled by viewModel.isRecordingEnabled.collectAsState()
val isTesting by viewModel.isTesting.collectAsState()
val testProgress by viewModel.testProgress.collectAsState()
val testTotal by viewModel.testTotal.collectAsState()
val testResults by viewModel.testResults.collectAsState()
val isLoggedIn by viewModel.isLoggedIn.collectAsState()
val currentUser by viewModel.currentUser.collectAsState()
val presets = listOf("Flat", "Rock", "Pop", "Jazz", "Bass")
var showReport by remember { mutableStateOf(false) }
Scaffold(
topBar = {
@@ -39,6 +51,37 @@ fun SettingsScreen(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
item {
Text("Профиль", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
if (isLoggedIn && currentUser != null) {
Column {
Text(
text = currentUser?.email ?: "",
style = MaterialTheme.typography.bodyLarge
)
Spacer(modifier = Modifier.height(4.dp))
OutlinedButton(
onClick = { viewModel.logout() },
modifier = Modifier.fillMaxWidth()
) {
Text("Выйти")
}
}
} else {
Button(
onClick = onNavigateToAuth,
modifier = Modifier.fillMaxWidth()
) {
Text("Войти")
}
}
}
item {
Divider()
}
item {
Text("Таймер сна", style = MaterialTheme.typography.titleMedium)
Slider(
@@ -99,6 +142,94 @@ fun SettingsScreen(
)
}
}
item {
Divider()
}
item {
Text("Тестирование станций", style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(8.dp))
if (isTesting) {
Column {
LinearProgressIndicator(
progress = { if (testTotal > 0) testProgress.toFloat() / testTotal else 0f },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(4.dp))
Text("Проверено $testProgress из $testTotal")
}
} else if (testResults.isNotEmpty()) {
val ok = testResults.count { it.status == StationTestStatus.OK }
val okNoMeta = testResults.count { it.status == StationTestStatus.OK_NO_META }
val offline = testResults.count { it.status == StationTestStatus.OFFLINE }
val error = testResults.count { it.status == StationTestStatus.ERROR }
Column {
Text("Всего: ${testResults.size}")
Text("Работают + метаданные: $ok", color = Color(0xFF4CAF50))
Text("Работают без метаданных: $okNoMeta", color = Color(0xFFFF9800))
Text("Оффлайн: $offline", color = Color(0xFFFF5252))
Text("Ошибки: $error", color = Color(0xFFFF5252))
Spacer(modifier = Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(onClick = { showReport = true }) {
Text("Подробный отчёт")
}
OutlinedButton(onClick = { viewModel.clearTestResults() }) {
Text("Очистить")
}
}
}
} else {
Button(
onClick = { viewModel.startTesting() },
modifier = Modifier.fillMaxWidth()
) {
Text("Провести тестирование")
}
}
}
}
}
if (showReport) {
AlertDialog(
onDismissRequest = { showReport = false },
title = { Text("Результаты тестирования") },
text = {
LazyColumn(modifier = Modifier.heightIn(max = 400.dp)) {
items(testResults) { result ->
val color = when (result.status) {
StationTestStatus.OK -> Color(0xFF4CAF50)
StationTestStatus.OK_NO_META -> Color(0xFFFF9800)
StationTestStatus.OFFLINE -> Color(0xFFFF5252)
StationTestStatus.ERROR -> Color(0xFFFF5252)
}
Column(modifier = Modifier.padding(vertical = 4.dp)) {
Text(
text = result.stationName,
style = MaterialTheme.typography.bodyMedium,
color = color
)
Text(
text = buildString {
append("${result.status.name}")
result.httpCode?.let { append(" | HTTP $it") }
result.icyTitle?.let { append(" | Icy: $it") }
result.nowPlayingTrack?.let { append(" | NP: $it") }
result.errorMessage?.let { append(" | $it") }
},
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
},
confirmButton = {
TextButton(onClick = { showReport = false }) {
Text("Закрыть")
}
}
)
}
}

View File

@@ -3,7 +3,12 @@ package com.radiola.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.radiola.domain.model.DeeplinkService
import com.radiola.domain.model.StationTestResult
import com.radiola.domain.repository.SettingsRepository
import com.radiola.domain.usecase.TestStationsUseCase
import com.radiola.domain.usecase.auth.GetAuthStateUseCase
import com.radiola.domain.usecase.auth.GetCurrentUserUseCase
import com.radiola.domain.usecase.auth.LogoutUseCase
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
@@ -11,7 +16,11 @@ import javax.inject.Inject
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val settingsRepository: SettingsRepository
private val settingsRepository: SettingsRepository,
private val testStationsUseCase: TestStationsUseCase,
getAuthStateUseCase: GetAuthStateUseCase,
getCurrentUserUseCase: GetCurrentUserUseCase,
private val logoutUseCase: LogoutUseCase
) : ViewModel() {
val sleepTimerMinutes: StateFlow<Int> = settingsRepository.getSleepTimerMinutes()
@@ -26,6 +35,24 @@ class SettingsViewModel @Inject constructor(
val isRecordingEnabled: StateFlow<Boolean> = settingsRepository.isRecordingEnabled()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
val isLoggedIn: StateFlow<Boolean> = getAuthStateUseCase()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
val currentUser = getCurrentUserUseCase()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null)
private val _isTesting = MutableStateFlow(false)
val isTesting: StateFlow<Boolean> = _isTesting.asStateFlow()
private val _testProgress = MutableStateFlow(0)
val testProgress: StateFlow<Int> = _testProgress.asStateFlow()
private val _testTotal = MutableStateFlow(0)
val testTotal: StateFlow<Int> = _testTotal.asStateFlow()
private val _testResults = MutableStateFlow<List<StationTestResult>>(emptyList())
val testResults: StateFlow<List<StationTestResult>> = _testResults.asStateFlow()
fun setSleepTimer(minutes: Int) {
viewModelScope.launch { settingsRepository.setSleepTimerMinutes(minutes) }
}
@@ -45,4 +72,30 @@ class SettingsViewModel @Inject constructor(
fun setRecordingEnabled(enabled: Boolean) {
viewModelScope.launch { settingsRepository.setRecordingEnabled(enabled) }
}
fun startTesting() {
viewModelScope.launch {
_isTesting.value = true
_testProgress.value = 0
_testTotal.value = 0
_testResults.value = emptyList()
val results = mutableListOf<StationTestResult>()
testStationsUseCase().collect { progress ->
_testProgress.value = progress.current
_testTotal.value = progress.total
progress.result?.let { results.add(it) }
}
_testResults.value = results
_isTesting.value = false
}
}
fun clearTestResults() {
_testResults.value = emptyList()
}
fun logout() {
viewModelScope.launch { logoutUseCase() }
}
}