feat(app): кнопка «Распознать трек» (Shazam) + история распознанных
- кнопка распознавания в плеере: видна только на музыкальных станциях без
метаданных эфира (track == null), показывает спиннер и результат через Toast
- распознанный трек отображается в плеере и пишется в ОТДЕЛЬНУЮ историю
распознанных (не дублируется в историю эфирных треков — гейт по ключу)
- экран Истории: переключатель «Треки эфира | Распознанные», два списка
- Room: таблица recognized_track (миграция 7→8), DAO/репозиторий
- ShazamRepository → POST /shazam/recognize/{stationId}, маппинг 503/400 в текст
- MusicGenres.isMusicStation — клиентский гейт (синхронизирован с бэкендом)
- bump backend submodule (модуль shazam)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -79,8 +79,15 @@ fun PlayerBottomSheet(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val enabledServices by viewModel.enabledServices.collectAsState()
|
||||
val recognizing by viewModel.recognizing.collectAsState()
|
||||
val colors = RadiolaTheme.colors
|
||||
val haptics = LocalHapticFeedback.current
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.recognizeEvent.collect { msg ->
|
||||
android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
var showLyrics by remember { mutableStateOf(false) }
|
||||
var showQuality by remember { mutableStateOf(false) }
|
||||
var showSleep by remember { mutableStateOf(false) }
|
||||
@@ -186,6 +193,54 @@ fun PlayerBottomSheet(
|
||||
}
|
||||
}
|
||||
|
||||
// Кнопка распознавания (Shazam) — только для музыкальных станций без метаданных эфира.
|
||||
val recognizeSection: @Composable () -> Unit = {
|
||||
val show = station != null &&
|
||||
track == null &&
|
||||
com.radiola.domain.model.MusicGenres.isMusicStation(station.genre)
|
||||
if (show) {
|
||||
val interaction = remember { MutableInteractionSource() }
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(colors.accent.copy(alpha = 0.15f))
|
||||
.pressScale(interactionSource = interaction)
|
||||
.clickable(
|
||||
interactionSource = interaction,
|
||||
indication = null,
|
||||
enabled = !recognizing
|
||||
) {
|
||||
haptics.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
viewModel.recognizeCurrentTrack()
|
||||
}
|
||||
.padding(horizontal = 18.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
if (recognizing) {
|
||||
CircularProgressIndicator(
|
||||
color = colors.accent,
|
||||
strokeWidth = 2.dp,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Lucide.Mic,
|
||||
contentDescription = null,
|
||||
tint = colors.accent,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = if (recognizing) "Распознаём…" else "Распознать трек",
|
||||
color = colors.accent,
|
||||
fontSize = 15.sp,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val visualizerSection: @Composable () -> Unit = {
|
||||
// Живой эквалайзер. Спектр (45/с) собирается ВНУТРИ VisualizerHost —
|
||||
// чтобы 45/с рекомпозиции не задевали весь плеер, только этот leaf.
|
||||
@@ -408,6 +463,7 @@ fun PlayerBottomSheet(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
trackInfoSection()
|
||||
recognizeSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
visualizerSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
@@ -441,6 +497,7 @@ fun PlayerBottomSheet(
|
||||
coverSection(190.dp)
|
||||
Spacer(Modifier.height(14.dp))
|
||||
trackInfoSection()
|
||||
recognizeSection()
|
||||
Spacer(Modifier.height(20.dp))
|
||||
visualizerSection()
|
||||
Spacer(Modifier.height(16.dp))
|
||||
|
||||
Reference in New Issue
Block a user