feat: будильник с радиостанцией + выбор битрейта по умолчанию

Будильник (Settings → Будильник): несколько будильников, время, станция, дни недели,
fade-in пробуждения. AlarmManager.setAlarmClock (вне doze) + фолбэк, BootReceiver
перепланирует после перезагрузки, AlarmReceiver→PlayerService (foreground) →
PlayerController.startAlarmPlayback (нарастание громкости). Room: AlarmEntity/Dao, БД v7.
Выбор битрейта по умолчанию в Settings (Авто/Эконом/Стандарт/Высокое) → preferredBitrate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-06 15:25:42 +03:00
parent 4411d53a6c
commit 861b0e2b8f
17 changed files with 1014 additions and 3 deletions

View File

@@ -24,6 +24,8 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import com.composables.icons.lucide.AlarmClock
import com.composables.icons.lucide.ChevronRight
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.User
import com.radiola.domain.model.DeeplinkService
@@ -35,6 +37,7 @@ import com.radiola.ui.theme.RadiolaTheme
@Composable
fun SettingsScreen(
onNavigateToAuth: () -> Unit,
onNavigateToAlarms: () -> Unit = {},
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = hiltViewModel()
) {
@@ -43,6 +46,7 @@ fun SettingsScreen(
val equalizerPreset by viewModel.equalizerPreset.collectAsState()
val visualizerStyle by viewModel.visualizerStyle.collectAsState()
val isRecordingEnabled by viewModel.isRecordingEnabled.collectAsState()
val preferredBitrate by viewModel.preferredBitrate.collectAsState()
val isTesting by viewModel.isTesting.collectAsState()
val testProgress by viewModel.testProgress.collectAsState()
val testTotal by viewModel.testTotal.collectAsState()
@@ -144,6 +148,48 @@ fun SettingsScreen(
}
}
// --- Будильник ---
item {
SectionLabel("БУДИЛЬНИК")
Spacer(Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(colors.surface)
.border(1.dp, colors.border, RoundedCornerShape(16.dp))
.clickable { onNavigateToAlarms() }
.padding(horizontal = 16.dp, vertical = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp)
) {
Icon(
Lucide.AlarmClock,
contentDescription = null,
tint = colors.accent,
modifier = Modifier.size(22.dp)
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Будильники",
style = MaterialTheme.typography.titleMedium,
color = colors.textPrimary
)
Text(
text = "Просыпайтесь под любимое радио",
style = MaterialTheme.typography.labelMedium,
color = colors.textSecondary
)
}
Icon(
Lucide.ChevronRight,
contentDescription = null,
tint = colors.textMuted,
modifier = Modifier.size(18.dp)
)
}
}
// --- Таймер сна ---
item {
SectionLabel("ТАЙМЕР СНА")
@@ -187,6 +233,58 @@ fun SettingsScreen(
}
}
// --- Качество звука по умолчанию ---
item {
SectionLabel("КАЧЕСТВО ЗВУКА")
Spacer(Modifier.height(8.dp))
val options = listOf(0 to "Авто", 64 to "Эконом", 128 to "Стандарт", 320 to "Высокое")
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(16.dp))
.background(colors.surface)
.border(1.dp, colors.border, RoundedCornerShape(16.dp))
.padding(12.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp)
) {
options.forEach { (bitrate, label) ->
val selected = preferredBitrate == bitrate
val bgColor by animateColorAsState(
targetValue = if (selected) colors.accent else colors.surface2,
animationSpec = tween(Motion.Medium),
label = "qSegment"
)
val textColor by animateColorAsState(
targetValue = if (selected) colors.bgBase else colors.textSecondary,
animationSpec = tween(Motion.Medium),
label = "qText"
)
Box(
modifier = Modifier
.weight(1f)
.clip(RoundedCornerShape(10.dp))
.background(bgColor)
.clickable { viewModel.setPreferredBitrate(bitrate) }
.padding(vertical = 10.dp),
contentAlignment = Alignment.Center
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
color = textColor,
fontWeight = FontWeight.Medium
)
}
}
}
Text(
text = "Применяется к станциям с несколькими потоками. «Авто» — выбор станции.",
style = MaterialTheme.typography.bodySmall,
color = colors.textMuted,
modifier = Modifier.padding(top = 8.dp)
)
}
// --- Эквалайзер ---
item {
SectionLabel("ЭКВАЛАЙЗЕР")

View File

@@ -38,6 +38,10 @@ class SettingsViewModel @Inject constructor(
val isRecordingEnabled: StateFlow<Boolean> = settingsRepository.isRecordingEnabled()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
// Предпочитаемый битрейт по умолчанию (0 = авто/станция сама выбирает).
val preferredBitrate: StateFlow<Int> = settingsRepository.getPreferredBitrate()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0)
val isLoggedIn: StateFlow<Boolean> = getAuthStateUseCase()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
@@ -60,6 +64,10 @@ class SettingsViewModel @Inject constructor(
viewModelScope.launch { settingsRepository.setSleepTimerMinutes(minutes) }
}
fun setPreferredBitrate(bitrate: Int) {
viewModelScope.launch { settingsRepository.setPreferredBitrate(bitrate) }
}
fun toggleService(serviceId: String, enabled: Boolean) {
viewModelScope.launch {
val current = enabledServices.value.toMutableSet()