feat(recordings): перемотка записей + тайм-коды треков

1) Перемотка: записи эфира — сырой ADTS-AAC/MP3 без индексов, ExoPlayer
   считал их неперематываемыми (старт всегда с нуля). Включён CBR-seeking
   (DefaultExtractorsFactory.setConstantBitrateSeekingEnabled) — seek работает.

2) Тайм-коды треков: при записи фиксируются смены now-playing с offset от
   начала (модель TrackMarker, колонка markers в recordings, миграция v6,
   захват через NowPlayingRepository — свой поллинг, не зависит от экрана).
   В плеере записи — список «Треки в записи»: тайм-код + название, тап
   переходит к моменту, текущий трек подсвечен.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nk
2026-06-04 13:18:23 +03:00
parent 777f5d5082
commit fc63814f97
8 changed files with 172 additions and 6 deletions

View File

@@ -2,9 +2,14 @@ package com.radiola.ui.recordings
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -54,6 +59,7 @@ fun RecordingPlayerSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
.padding(bottom = 40.dp),
horizontalAlignment = Alignment.CenterHorizontally
@@ -202,6 +208,78 @@ fun RecordingPlayerSheet(
)
}
}
// Список треков записи с переходом по тайм-коду
if (recording.markers.isNotEmpty()) {
Spacer(modifier = Modifier.height(28.dp))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Треки в записи",
style = MaterialTheme.typography.titleSmall,
color = colors.textPrimary,
fontWeight = FontWeight.SemiBold
)
Text(
text = "${recording.markers.size}",
style = MaterialTheme.typography.labelMedium,
color = colors.textMuted
)
}
Spacer(modifier = Modifier.height(8.dp))
// Индекс текущего трека: последняя метка, до которой уже дошло время
val activeIndex = recording.markers.indexOfLast { positionMs >= it.offsetMs }
recording.markers.forEachIndexed { index, marker ->
MarkerRow(
timecode = formatMs(marker.offsetMs),
title = marker.title,
active = index == activeIndex,
onClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
viewModel.seekTo(marker.offsetMs)
}
)
}
}
}
}
/** Строка трека в записи: тайм-код + название, тап → переход. */
@Composable
private fun MarkerRow(
timecode: String,
title: String,
active: Boolean,
onClick: () -> Unit
) {
val colors = RadiolaTheme.colors
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(if (active) colors.surface2 else androidx.compose.ui.graphics.Color.Transparent)
.clickable(onClick = onClick)
.padding(horizontal = 12.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = timecode,
style = MaterialTheme.typography.labelMedium,
color = if (active) colors.accent else colors.textMuted,
fontWeight = FontWeight.Medium
)
Text(
text = title.ifBlank { "" },
style = MaterialTheme.typography.bodyMedium,
color = if (active) colors.accent else colors.textPrimary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
)
}
}