Dialog show/hide lyrics

This commit is contained in:
Damontecres 2026-03-03 21:28:30 -05:00
parent 59a0bee0a7
commit 18e3268638
No known key found for this signature in database
6 changed files with 102 additions and 6 deletions

View file

@ -68,6 +68,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.serializer.toUUID
import org.jellyfin.sdk.model.serializer.toUUIDOrNull import org.jellyfin.sdk.model.serializer.toUUIDOrNull
import timber.log.Timber import timber.log.Timber
import javax.inject.Inject import javax.inject.Inject
@ -213,7 +214,12 @@ class MainActivity : AppCompatActivity() {
) { ) {
val requestedDestination = val requestedDestination =
remember(intent) { remember(intent) {
intent?.let(::extractDestination) ?: Destination.Home() intent?.let(::extractDestination) ?: // Destination.Home()
// TODO
Destination.MediaItem(
itemId = "011ef0c7ca45684f2cd9dd3b020ca5f6".toUUID(),
type = BaseItemKind.MUSIC_ALBUM,
)
} }
MainContent( MainContent(
backStack = setupNavigationManager.backStack, backStack = setupNavigationManager.backStack,

View file

@ -1,6 +1,7 @@
package com.github.damontecres.wholphin.ui.detail.music package com.github.damontecres.wholphin.ui.detail.music
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
@ -10,6 +11,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
@ -19,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -31,6 +34,7 @@ import androidx.media3.ui.compose.state.rememberPlayPauseButtonState
import androidx.media3.ui.compose.state.rememberPreviousButtonState import androidx.media3.ui.compose.state.rememberPreviousButtonState
import androidx.media3.ui.compose.state.rememberRepeatButtonState import androidx.media3.ui.compose.state.rememberRepeatButtonState
import androidx.media3.ui.compose.state.rememberShuffleButtonState import androidx.media3.ui.compose.state.rememberShuffleButtonState
import androidx.tv.material3.Border
import androidx.tv.material3.ClickableSurfaceDefaults import androidx.tv.material3.ClickableSurfaceDefaults
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
@ -41,7 +45,9 @@ import com.github.damontecres.wholphin.ui.PreviewTvSpec
import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.components.Button
import com.github.damontecres.wholphin.ui.playback.ControllerViewState import com.github.damontecres.wholphin.ui.playback.ControllerViewState
import com.github.damontecres.wholphin.ui.playback.PlaybackAction import com.github.damontecres.wholphin.ui.playback.PlaybackAction
import com.github.damontecres.wholphin.ui.playback.PlaybackButton
import com.github.damontecres.wholphin.ui.playback.PlaybackButtons import com.github.damontecres.wholphin.ui.playback.PlaybackButtons
import com.github.damontecres.wholphin.ui.playback.PlaybackDialogType
import com.github.damontecres.wholphin.ui.playback.buttonSpacing import com.github.damontecres.wholphin.ui.playback.buttonSpacing
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
import com.github.damontecres.wholphin.ui.theme.WholphinTheme import com.github.damontecres.wholphin.ui.theme.WholphinTheme
@ -53,6 +59,7 @@ fun NowPlayingButtons(
player: Player, player: Player,
controllerViewState: ControllerViewState, controllerViewState: ControllerViewState,
initialFocusRequester: FocusRequester, initialFocusRequester: FocusRequester,
onClickMore: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val playPauseState = rememberPlayPauseButtonState(player) val playPauseState = rememberPlayPauseButtonState(player)
@ -60,13 +67,30 @@ fun NowPlayingButtons(
val nextState = rememberNextButtonState(player) val nextState = rememberNextButtonState(player)
val shuffleState = rememberShuffleButtonState(player) val shuffleState = rememberShuffleButtonState(player)
val repeatState = rememberRepeatButtonState(player) val repeatState = rememberRepeatButtonState(player)
val onControllerInteraction = remember { { controllerViewState.pulseControls() } }
Box( Box(
modifier = modifier, modifier = modifier,
) { ) {
Row(
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
modifier = Modifier.align(Alignment.CenterStart),
) {
PlaybackButton(
iconRes = R.drawable.baseline_more_vert_96,
onClick = {
onControllerInteraction.invoke()
onClickMore.invoke()
},
enabled = true,
onControllerInteraction = onControllerInteraction,
modifier = Modifier,
)
}
PlaybackButtons( PlaybackButtons(
player = player, player = player,
initialFocusRequester = initialFocusRequester, initialFocusRequester = initialFocusRequester,
onControllerInteraction = { controllerViewState.pulseControls() }, onControllerInteraction = onControllerInteraction,
onPlaybackActionClick = { onPlaybackActionClick = {
when (it) { when (it) {
PlaybackAction.Next -> { PlaybackAction.Next -> {
@ -102,7 +126,7 @@ fun NowPlayingButtons(
onClick = { onClick = {
shuffleState.onClick() shuffleState.onClick()
}, },
onControllerInteraction = { controllerViewState.pulseControls() }, onControllerInteraction = onControllerInteraction,
) )
RepeatButton( RepeatButton(
repeatMode = repeatState.repeatModeState, repeatMode = repeatState.repeatModeState,
@ -110,7 +134,7 @@ fun NowPlayingButtons(
onClick = { onClick = {
repeatState.onClick() repeatState.onClick()
}, },
onControllerInteraction = { controllerViewState.pulseControls() }, onControllerInteraction = onControllerInteraction,
) )
} }
} }

View file

@ -59,6 +59,7 @@ fun NowPlayingOverlay(
current: AudioItem?, current: AudioItem?,
queue: List<AudioItem>, queue: List<AudioItem>,
controllerViewState: ControllerViewState, controllerViewState: ControllerViewState,
onClickMore: () -> Unit,
onMoveQueue: (Int, MoveDirection) -> Unit, onMoveQueue: (Int, MoveDirection) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -122,6 +123,7 @@ fun NowPlayingOverlay(
player = player, player = player,
controllerViewState = controllerViewState, controllerViewState = controllerViewState,
initialFocusRequester = focusRequester, initialFocusRequester = focusRequester,
onClickMore = onClickMore,
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()

View file

@ -1,5 +1,6 @@
package com.github.damontecres.wholphin.ui.detail.music package com.github.damontecres.wholphin.ui.detail.music
import android.view.Gravity
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.annotation.OptIn import androidx.annotation.OptIn
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@ -21,23 +22,29 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.tv.material3.MaterialTheme import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.github.damontecres.wholphin.R
import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.AppPreferences
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.rememberQueue import com.github.damontecres.wholphin.services.rememberQueue
import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.AppColors
import com.github.damontecres.wholphin.ui.components.LoadingPage import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.playback.BottomDialog
import com.github.damontecres.wholphin.ui.playback.BottomDialogItem
import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler
import com.github.damontecres.wholphin.ui.tryRequestFocus import com.github.damontecres.wholphin.ui.tryRequestFocus
import com.github.damontecres.wholphin.util.LoadingState import com.github.damontecres.wholphin.util.LoadingState
@ -90,6 +97,10 @@ fun NowPlayingPage(
) )
} }
var showMoreDialog by remember { mutableStateOf(false) }
var lyricsActive by remember { mutableStateOf(true) }
var showDebugInfo by remember { mutableStateOf(true) }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } LaunchedEffect(Unit) { focusRequester.tryRequestFocus() }
Box(modifier) { Box(modifier) {
@ -106,7 +117,7 @@ fun NowPlayingPage(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
AnimatedVisibility( AnimatedVisibility(
visible = state.lyrics != null && state.lyrics?.lyrics?.isNotEmpty() == true, visible = lyricsActive && state.hasLyrics,
enter = expandHorizontally(expandFrom = Alignment.Start), enter = expandHorizontally(expandFrom = Alignment.Start),
exit = shrinkHorizontally(shrinkTowards = Alignment.Start), exit = shrinkHorizontally(shrinkTowards = Alignment.Start),
modifier = Modifier, modifier = Modifier,
@ -177,6 +188,7 @@ fun NowPlayingPage(
queue = queue, queue = queue,
controllerViewState = controllerViewState, controllerViewState = controllerViewState,
onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) }, onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) },
onClickMore = { showMoreDialog = true },
modifier = modifier =
Modifier Modifier
.background(AppColors.TransparentBlack50) .background(AppColors.TransparentBlack50)
@ -187,4 +199,50 @@ fun NowPlayingPage(
LoadingPage(focusEnabled = false) LoadingPage(focusEnabled = false)
} }
} }
if (showMoreDialog) {
NowPlayingBottomDialog(
showDebugInfo = showDebugInfo,
lyricsActive = lyricsActive,
songHasLyrics = state.hasLyrics,
onDismissRequest = { showMoreDialog = false },
onClickShowDebug = { showDebugInfo = !showDebugInfo },
onClickLyrics = { lyricsActive = !lyricsActive },
)
}
}
@Composable
fun NowPlayingBottomDialog(
showDebugInfo: Boolean,
lyricsActive: Boolean,
songHasLyrics: Boolean,
onDismissRequest: () -> Unit,
onClickShowDebug: () -> Unit,
onClickLyrics: () -> Unit,
) {
val choices =
mapOf(
BottomDialogItem(
data = 0,
headline = stringResource(if (showDebugInfo) R.string.hide_debug_info else R.string.show_debug_info),
supporting = null,
) to onClickShowDebug,
BottomDialogItem(
data = 0,
headline = stringResource(if (lyricsActive) R.string.hide_lyrics else R.string.show_lyrics),
supporting = if (songHasLyrics) stringResource(R.string.song_has_lyrics) else null,
) to onClickLyrics,
)
BottomDialog(
choices = choices.keys.toList(),
onDismissRequest = {
onDismissRequest.invoke()
},
onSelectChoice = { _, choice ->
choices[choice]?.invoke()
onDismissRequest.invoke()
},
gravity = Gravity.START,
)
} }

View file

@ -9,4 +9,6 @@ data class NowPlayingState(
val musicServiceState: MusicServiceState, val musicServiceState: MusicServiceState,
val lyrics: LyricDto? = null, val lyrics: LyricDto? = null,
val currentLyricIndex: Int? = null, val currentLyricIndex: Int? = null,
) ) {
val hasLyrics: Boolean get() = lyrics != null && lyrics.lyrics.isNotEmpty()
}

View file

@ -733,6 +733,10 @@
<string name="popular_songs">Popular songs</string> <string name="popular_songs">Popular songs</string>
<string name="play_next">Play next</string> <string name="play_next">Play next</string>
<string name="music_videos">Music videos</string> <string name="music_videos">Music videos</string>
<string name="lyrics">Lyrics</string>
<string name="hide_lyrics">Hide lyrics</string>
<string name="show_lyrics">Show lyrics</string>
<string name="song_has_lyrics">Song has lyrics</string>
</resources> </resources>