|
|
@@ -2,7 +2,9 @@ package io.legado.app.ui.book.audio
|
|
|
|
|
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
|
import androidx.compose.animation.core.tween
|
|
|
+import androidx.compose.foundation.clickable
|
|
|
import androidx.compose.foundation.gestures.Orientation
|
|
|
+import androidx.compose.foundation.gestures.animateScrollBy
|
|
|
import androidx.compose.foundation.gestures.detectTapGestures
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
|
import androidx.compose.foundation.layout.Box
|
|
|
@@ -18,6 +20,10 @@ import androidx.compose.foundation.layout.navigationBars
|
|
|
import androidx.compose.foundation.layout.padding
|
|
|
import androidx.compose.foundation.layout.statusBars
|
|
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
|
+import androidx.compose.foundation.lazy.LazyColumn
|
|
|
+import androidx.compose.foundation.lazy.itemsIndexed
|
|
|
+import androidx.compose.foundation.lazy.rememberLazyListState
|
|
|
+import androidx.compose.foundation.pager.HorizontalPager
|
|
|
import androidx.compose.foundation.pager.VerticalPager
|
|
|
import androidx.compose.foundation.pager.rememberPagerState
|
|
|
import androidx.compose.foundation.shape.CircleShape
|
|
|
@@ -49,12 +55,14 @@ import androidx.compose.material.icons.filled.SwapHoriz
|
|
|
import androidx.compose.material.icons.filled.Timer
|
|
|
import androidx.compose.material.icons.filled.WbTwilight
|
|
|
import androidx.compose.runtime.Composable
|
|
|
+import androidx.compose.runtime.LaunchedEffect
|
|
|
import androidx.compose.runtime.getValue
|
|
|
import androidx.compose.runtime.mutableFloatStateOf
|
|
|
import androidx.compose.runtime.mutableStateOf
|
|
|
import androidx.compose.runtime.remember
|
|
|
import androidx.compose.runtime.rememberCoroutineScope
|
|
|
import androidx.compose.runtime.setValue
|
|
|
+import androidx.compose.runtime.snapshotFlow
|
|
|
import androidx.compose.ui.Alignment
|
|
|
import androidx.compose.ui.Modifier
|
|
|
import androidx.compose.ui.draw.clip
|
|
|
@@ -100,9 +108,11 @@ import io.legado.app.ui.widget.components.player.PlayerProgressSlider
|
|
|
import io.legado.app.ui.widget.components.player.PlayerTocPage
|
|
|
import io.legado.app.ui.widget.components.player.playerBgModeLabel
|
|
|
import io.legado.app.ui.widget.components.text.AppText
|
|
|
+import kotlinx.coroutines.flow.first
|
|
|
import kotlinx.coroutines.launch
|
|
|
import top.yukonga.miuix.kmp.blur.layerBackdrop
|
|
|
import java.util.Locale
|
|
|
+import kotlin.math.abs
|
|
|
import kotlin.math.roundToInt
|
|
|
|
|
|
@OptIn(ExperimentalHazeMaterialsApi::class)
|
|
|
@@ -112,6 +122,10 @@ fun AudioPlayScreenContent(
|
|
|
onIntent: (AudioPlayIntent) -> Unit,
|
|
|
onBack: () -> Unit,
|
|
|
) {
|
|
|
+ val horizontalPagerState = rememberPagerState(
|
|
|
+ initialPage = 0,
|
|
|
+ pageCount = { if (state.lyricLines.isEmpty()) 1 else 2 },
|
|
|
+ )
|
|
|
val verticalPagerState = rememberPagerState(initialPage = 0, pageCount = { 2 })
|
|
|
val verticalPagerNestedScrollConnection = rememberPagerFlingPassThroughConnection(
|
|
|
state = verticalPagerState,
|
|
|
@@ -158,7 +172,7 @@ fun AudioPlayScreenContent(
|
|
|
Row(
|
|
|
modifier = Modifier
|
|
|
.fillMaxWidth()
|
|
|
- .padding(horizontal = 16.dp, vertical = 8.dp),
|
|
|
+ .padding(horizontal = 8.dp, vertical = 8.dp),
|
|
|
verticalAlignment = Alignment.CenterVertically,
|
|
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
|
) {
|
|
|
@@ -390,6 +404,15 @@ fun AudioPlayScreenContent(
|
|
|
),
|
|
|
onCheckedChange = {
|
|
|
coroutineScope.launch {
|
|
|
+ if (horizontalPagerState.currentPage != 0) {
|
|
|
+ horizontalPagerState.animateScrollToPage(
|
|
|
+ page = 0,
|
|
|
+ animationSpec = tween(
|
|
|
+ durationMillis = 520,
|
|
|
+ easing = FastOutSlowInEasing,
|
|
|
+ ),
|
|
|
+ )
|
|
|
+ }
|
|
|
verticalPagerState.animateScrollToPage(
|
|
|
page = if (verticalPagerState.currentPage == 0) 1 else 0,
|
|
|
animationSpec = tween(
|
|
|
@@ -457,21 +480,35 @@ fun AudioPlayScreenContent(
|
|
|
Modifier
|
|
|
},
|
|
|
)
|
|
|
- VerticalPager(
|
|
|
- state = verticalPagerState,
|
|
|
+ HorizontalPager(
|
|
|
+ state = horizontalPagerState,
|
|
|
modifier = Modifier.fillMaxSize(),
|
|
|
- horizontalAlignment = Alignment.CenterHorizontally,
|
|
|
- pageNestedScrollConnection = verticalPagerNestedScrollConnection,
|
|
|
+ verticalAlignment = Alignment.CenterVertically,
|
|
|
) { page ->
|
|
|
if (page == 0) {
|
|
|
- AudioCoverPage(state, pageContentPadding)
|
|
|
+ VerticalPager(
|
|
|
+ state = verticalPagerState,
|
|
|
+ modifier = Modifier.fillMaxSize(),
|
|
|
+ horizontalAlignment = Alignment.CenterHorizontally,
|
|
|
+ pageNestedScrollConnection = verticalPagerNestedScrollConnection,
|
|
|
+ ) { verticalPage ->
|
|
|
+ if (verticalPage == 0) {
|
|
|
+ AudioCoverPage(state, pageContentPadding)
|
|
|
+ } else {
|
|
|
+ PlayerTocPage(
|
|
|
+ chapters = state.chapters,
|
|
|
+ currentIndex = state.chapterIndex,
|
|
|
+ isPaused = !state.isPlaying,
|
|
|
+ onSelect = { onIntent(AudioPlayIntent.SelectChapter(it)) },
|
|
|
+ contentPadding = pageContentPadding,
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
} else {
|
|
|
- PlayerTocPage(
|
|
|
- chapters = state.chapters,
|
|
|
- currentIndex = state.chapterIndex,
|
|
|
- isPaused = !state.isPlaying,
|
|
|
- onSelect = { onIntent(AudioPlayIntent.SelectChapter(it)) },
|
|
|
+ AudioLyricPage(
|
|
|
+ state = state,
|
|
|
contentPadding = pageContentPadding,
|
|
|
+ onIntent = onIntent,
|
|
|
)
|
|
|
}
|
|
|
}
|
|
|
@@ -517,6 +554,77 @@ private const val AUDIO_GAIN_MIN = -6000
|
|
|
private const val AUDIO_GAIN_MAX = 6000
|
|
|
private const val CREDITS_MAX_SECONDS = 180
|
|
|
|
|
|
+@Composable
|
|
|
+private fun AudioLyricPage(
|
|
|
+ state: AudioPlayUiState,
|
|
|
+ contentPadding: PaddingValues,
|
|
|
+ onIntent: (AudioPlayIntent) -> Unit,
|
|
|
+) {
|
|
|
+ val listState = rememberLazyListState()
|
|
|
+ val activeLine = state.lyricLines.indexOfLast { it.timestampMs <= state.position }
|
|
|
+
|
|
|
+ LaunchedEffect(state.lyricLines, activeLine) {
|
|
|
+ if (activeLine !in state.lyricLines.indices) return@LaunchedEffect
|
|
|
+
|
|
|
+ snapshotFlow { listState.layoutInfo.viewportSize.height }.first { it > 0 }
|
|
|
+ val layoutInfo = listState.layoutInfo
|
|
|
+ val viewportHeight = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset
|
|
|
+ val targetDistance = viewportHeight * 0.32f
|
|
|
+ val targetOffset = layoutInfo.viewportStartOffset + targetDistance
|
|
|
+ val visibleItem = layoutInfo.visibleItemsInfo.firstOrNull { it.index == activeLine }
|
|
|
+ val scrollDistance = if (visibleItem != null) {
|
|
|
+ visibleItem.offset - targetOffset
|
|
|
+ } else {
|
|
|
+ val approachDistance = (viewportHeight * 0.08f).coerceAtLeast(1f)
|
|
|
+ listState.scrollToItem(
|
|
|
+ index = activeLine,
|
|
|
+ scrollOffset = -(targetDistance + approachDistance).roundToInt(),
|
|
|
+ )
|
|
|
+ approachDistance
|
|
|
+ }
|
|
|
+ if (abs(scrollDistance) > 1f) {
|
|
|
+ listState.animateScrollBy(
|
|
|
+ value = scrollDistance,
|
|
|
+ animationSpec = tween(durationMillis = 520, easing = FastOutSlowInEasing),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ LazyColumn(
|
|
|
+ state = listState,
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxSize()
|
|
|
+ .padding(horizontal = 16.dp),
|
|
|
+ contentPadding = PaddingValues(
|
|
|
+ top = contentPadding.calculateTopPadding(),
|
|
|
+ bottom = contentPadding.calculateBottomPadding(),
|
|
|
+ ),
|
|
|
+ verticalArrangement = Arrangement.spacedBy(8.dp),
|
|
|
+ ) {
|
|
|
+ itemsIndexed(
|
|
|
+ items = state.lyricLines,
|
|
|
+ key = { index, line -> "${line.timestampMs}:$index" },
|
|
|
+ contentType = { _, _ -> "audio_lyric_line" },
|
|
|
+ ) { index, line ->
|
|
|
+ val active = index == activeLine
|
|
|
+ AppText(
|
|
|
+ text = line.text,
|
|
|
+ modifier = Modifier
|
|
|
+ .fillMaxWidth()
|
|
|
+ .clip(RoundedCornerShape(16.dp))
|
|
|
+ .clickable { onIntent(AudioPlayIntent.SeekTo(line.timestampMs)) }
|
|
|
+ .padding(horizontal = 12.dp, vertical = 12.dp),
|
|
|
+ style = LegadoTheme.typography.titleLargeEmphasized,
|
|
|
+ color = if (active) {
|
|
|
+ LegadoTheme.colorScheme.onSurface
|
|
|
+ } else {
|
|
|
+ LegadoTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f)
|
|
|
+ },
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
@Composable
|
|
|
private fun AudioSkipCreditsSheet(
|
|
|
show: Boolean,
|