diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt index dfc1a9b5..f066467b 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/AlbumDetailsPage.kt @@ -207,21 +207,8 @@ class AlbumViewModel fun updateBackDrop() { state.value.album?.let { album -> viewModelScope.launchDefault { - if (album.data.imageTags?.contains(ImageType.BACKDROP) == true) { - backdropService.submit(album) - } else { - val artistIds = - album.data.albumArtists - ?.shuffled() - ?.take(50) - ?.map { it.id } - api.itemsApi - .getItems( - ids = artistIds, - imageTypes = listOf(ImageType.BACKDROP), - ).content.items - .firstOrNull() - ?.let { backdropService.submit(BaseItem(it, false)) } + getBackdropItemForAlbum(api, album)?.let { + backdropService.submit(it) } } } @@ -249,6 +236,28 @@ class AlbumViewModel } } +suspend fun getBackdropItemForAlbum( + api: ApiClient, + album: BaseItem, +): BaseItem? { + if (album.data.backdropImageTags?.isNotEmpty() == true) { + return album + } else { + val artistIds = + album.data.albumArtists + ?.shuffled() + ?.take(50) + ?.map { it.id } + return api.itemsApi + .getItems( + ids = artistIds, + imageTypes = listOf(ImageType.BACKDROP), + ).content.items + .firstOrNull() + ?.let { BaseItem(it, false) } + } +} + data class AlbumState( val album: BaseItem?, val isVariousArtists: Boolean, diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicPreference.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicPreference.kt index d7cf5260..1a519998 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicPreference.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicPreference.kt @@ -8,11 +8,35 @@ import com.github.damontecres.wholphin.preferences.updateMusicPreferences fun getMusicPreferences() = listOf( AppSwitchPreference( - title = R.string.bold_font, + title = R.string.show_album_cover, defaultValue = false, getter = { it.musicPreferences.showAlbumArt }, setter = { prefs, value -> prefs.updateMusicPreferences { showAlbumArt = value } }, ), + AppSwitchPreference( + title = R.string.show_visualizer, + defaultValue = false, + getter = { it.musicPreferences.showVisualizer }, + setter = { prefs, value -> + prefs.updateMusicPreferences { showVisualizer = value } + }, + ), + AppSwitchPreference( + title = R.string.show_backdrop, + defaultValue = false, + getter = { it.musicPreferences.showBackdrop }, + setter = { prefs, value -> + prefs.updateMusicPreferences { showBackdrop = value } + }, + ), + AppSwitchPreference( + title = R.string.show_lyrics, + defaultValue = false, + getter = { it.musicPreferences.showLyrics }, + setter = { prefs, value -> + prefs.updateMusicPreferences { showLyrics = value } + }, + ), ) diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicViewOptionsDialog.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicViewOptionsDialog.kt new file mode 100644 index 00000000..1b99fa9f --- /dev/null +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/MusicViewOptionsDialog.kt @@ -0,0 +1,97 @@ +package com.github.damontecres.wholphin.ui.detail.music + +import android.view.Gravity +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.DialogWindowProvider +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.Text +import androidx.tv.material3.surfaceColorAtElevation +import com.github.damontecres.wholphin.R +import com.github.damontecres.wholphin.preferences.AppPreference +import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.ui.preferences.ComposablePreference +import com.github.damontecres.wholphin.ui.tryRequestFocus + +@Composable +fun MusicViewOptionsDialog( + appPreferences: AppPreferences, + onDismissRequest: () -> Unit, + onViewOptionsChange: (AppPreferences) -> Unit, +) { + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { focusRequester.tryRequestFocus() } + val columnState = rememberLazyListState() + val items = remember { getMusicPreferences() } + Dialog( + onDismissRequest = onDismissRequest, + properties = + DialogProperties( + usePlatformDefaultWidth = false, + ), + ) { + val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider + dialogWindowProvider?.window?.let { window -> + window.setGravity(Gravity.END) + window.setDimAmount(0f) + } + Column { + Text( + text = stringResource(R.string.view_options), + style = MaterialTheme.typography.titleMedium, + ) + } + LazyColumn( + state = columnState, + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(0.dp), + modifier = + Modifier + .width(256.dp) + .heightIn(max = 380.dp) + .focusRequester(focusRequester) + .background( + MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp), + shape = RoundedCornerShape(8.dp), + ), + ) { + items(items) { pref -> + pref as AppPreference + val interactionSource = remember { MutableInteractionSource() } + val value = pref.getter.invoke(appPreferences) + ComposablePreference( + preference = pref, + value = value, + onNavigate = {}, + onValueChange = { newValue -> + onViewOptionsChange.invoke(pref.setter(appPreferences, newValue)) + }, + interactionSource = interactionSource, + modifier = Modifier, + onClickPreference = { pref -> + }, + ) + } + } + } +} diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt index 5d410e29..943d3334 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingPage.kt @@ -4,6 +4,7 @@ import android.view.Gravity import androidx.activity.compose.BackHandler import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.expandHorizontally import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.slideInVertically @@ -12,10 +13,10 @@ import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable @@ -85,6 +86,7 @@ fun NowPlayingPage( AppPreferences.getDefaultInstance(), ), ).value.appPreferences + val musicPrefs = preferences.musicPreferences val keyHandler = remember(preferences) { @@ -120,9 +122,7 @@ fun NowPlayingPage( ) } - var showMoreDialog by remember { mutableStateOf(false) } - var lyricsActive by remember { mutableStateOf(true) } - var showDebugInfo by remember { mutableStateOf(true) } + var showViewOptionsDialog by remember { mutableStateOf(false) } var itemMoreDialog by remember { mutableStateOf(null) } val focusRequester = remember { FocusRequester() } @@ -136,55 +136,25 @@ fun NowPlayingPage( .focusRequester(focusRequester) .focusable(), ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxSize(), + AnimatedVisibility( + musicPrefs.showAlbumArt, + modifier = + Modifier + .padding(32.dp) + .fillMaxWidth(.5f) + .align(Alignment.CenterStart), ) { - AnimatedVisibility( - visible = lyricsActive && state.hasLyrics, - enter = expandHorizontally(expandFrom = Alignment.Start), - exit = shrinkHorizontally(shrinkTowards = Alignment.Start), - modifier = Modifier, - ) { - LyricsContent( - synced = true, - lyrics = state.lyrics, - currentLyricPosition = state.currentLyricIndex, - onClick = {}, - modifier = - Modifier - .padding(vertical = 120.dp, horizontal = 32.dp) - .fillMaxHeight() - .width(320.dp), - ) - } Column( horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = - Modifier - .padding(40.dp) - .fillMaxSize() - .weight(1f), + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Box( - contentAlignment = Alignment.Center, + AsyncImage( + contentDescription = null, + model = current?.imageUrl, modifier = Modifier - .fillMaxWidth() - .fillMaxHeight(.75f), - ) { - AsyncImage( - contentDescription = null, - model = current?.imageUrl, - modifier = Modifier.fillMaxSize(), - ) - - BarVisualizer( - data = viz, - modifier = Modifier.fillMaxSize(), - ) - } + .height(320.dp), + ) current?.title?.let { Text( text = it, @@ -205,6 +175,50 @@ fun NowPlayingPage( } } } + AnimatedVisibility( + musicPrefs.showVisualizer, + modifier = + Modifier + .padding(horizontal = 32.dp) + .fillMaxHeight(.75f) + .align(Alignment.CenterStart), + ) { + val visualizerWidth by animateFloatAsState(if (musicPrefs.showLyrics && state.hasLyrics) .5f else 1f) + BarVisualizer( + data = viz, + modifier = + Modifier + .fillMaxHeight() + .fillMaxWidth(visualizerWidth) + .align(Alignment.CenterStart), + ) + } + AnimatedVisibility( + visible = musicPrefs.showLyrics && state.hasLyrics, + enter = expandHorizontally(expandFrom = Alignment.End), + exit = shrinkHorizontally(shrinkTowards = Alignment.End), + modifier = Modifier.align(Alignment.CenterEnd), + ) { + Box( + contentAlignment = Alignment.Center, + modifier = + Modifier + .fillMaxWidth(.5f) + .fillMaxHeight(), + ) { + LyricsContent( + synced = true, + lyrics = state.lyrics, + currentLyricPosition = state.currentLyricIndex, + onClick = {}, + modifier = + Modifier + .padding(vertical = 120.dp) + .fillMaxHeight() + .width(320.dp), + ) + } + } } val showContextForItem = remember { @@ -243,7 +257,7 @@ fun NowPlayingPage( queue = queue, controllerViewState = controllerViewState, onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) }, - onClickMore = { showMoreDialog = true }, + onClickMore = { showViewOptionsDialog = true }, onClickSong = { index, _ -> viewModel.play(index) }, onClickMoreItem = { index, song -> showContextForItem.invoke(false, index, song) }, onLongClickSong = { index, song -> showContextForItem.invoke(true, index, song) }, @@ -277,14 +291,11 @@ fun NowPlayingPage( waitToLoad = params.fromLongClick, ) } - if (showMoreDialog) { - NowPlayingBottomDialog( - showDebugInfo = showDebugInfo, - lyricsActive = lyricsActive, - songHasLyrics = state.hasLyrics, - onDismissRequest = { showMoreDialog = false }, - onClickShowDebug = { showDebugInfo = !showDebugInfo }, - onClickLyrics = { lyricsActive = !lyricsActive }, + if (showViewOptionsDialog) { + MusicViewOptionsDialog( + appPreferences = preferences, + onDismissRequest = { showViewOptionsDialog = false }, + onViewOptionsChange = { viewModel.updatePreferences(it) }, ) } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt index 0335e21f..8517cce1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/detail/music/NowPlayingViewModel.kt @@ -2,12 +2,18 @@ package com.github.damontecres.wholphin.ui.detail.music import android.content.Context import android.media.audiofx.Visualizer +import androidx.datastore.core.DataStore import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi import com.github.damontecres.wholphin.data.model.AudioItem +import com.github.damontecres.wholphin.data.model.BaseItem import com.github.damontecres.wholphin.preferences.AppPreference +import com.github.damontecres.wholphin.preferences.AppPreferences +import com.github.damontecres.wholphin.services.BackdropService +import com.github.damontecres.wholphin.services.ImageUrlService import com.github.damontecres.wholphin.services.MusicService import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.UserPreferencesService @@ -29,6 +35,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.extensions.lyricsApi +import org.jellyfin.sdk.api.client.extensions.userLibraryApi import org.jellyfin.sdk.model.api.LyricDto import org.jellyfin.sdk.model.extensions.ticks import timber.log.Timber @@ -38,6 +45,7 @@ import kotlin.math.ceil import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds +@UnstableApi @HiltViewModel(assistedFactory = NowPlayingViewModel.Factory::class) class NowPlayingViewModel @AssistedInject @@ -46,6 +54,9 @@ class NowPlayingViewModel @param:ApplicationContext private val context: Context, val navigationManager: NavigationManager, private val musicService: MusicService, + private val imageUrlService: ImageUrlService, + private val backdropService: BackdropService, + private val preferencesDataStore: DataStore, val userPreferencesService: UserPreferencesService, ) : ViewModel(), Visualizer.OnDataCaptureListener, @@ -103,6 +114,9 @@ class NowPlayingViewModel }.join() controllerViewState.pulseControls() } + viewModelScope.launchDefault { + updateBackdrop(getCurrent()) + } playbackLoop() } @@ -163,9 +177,10 @@ class NowPlayingViewModel mediaItem: MediaItem?, reason: Int, ) { + val audio = mediaItem?.localConfiguration?.tag as? AudioItem + Timber.v("onMediaItemTransition to %s", audio?.id) + updateBackdrop(audio) viewModelScope.launchDefault { - val audio = mediaItem?.localConfiguration?.tag as? AudioItem - Timber.v("onMediaItemTransition to %s", audio?.id) state.update { it.copy( lyrics = null, @@ -190,6 +205,32 @@ class NowPlayingViewModel } } + private fun updateBackdrop(audio: AudioItem?) { + viewModelScope.launchDefault { + val showBackdrop = + userPreferencesService + .getCurrent() + .appPreferences.musicPreferences.showBackdrop + if (audio?.albumId != null && showBackdrop) { + try { + api.userLibraryApi.getItem(audio.albumId).content.let { + val backdropItem = getBackdropItemForAlbum(api, BaseItem(it, false)) + if (backdropItem != null) { + backdropService.submit(backdropItem) + } else { + backdropService.clearBackdrop() + } + } + } catch (ex: Exception) { + Timber.e(ex, "Error fetching backdrop") + backdropService.clearBackdrop() + } + } else { + backdropService.clearBackdrop() + } + } + } + fun moveQueue( index: Int, direction: MoveDirection, @@ -239,4 +280,22 @@ class NowPlayingViewModel } viz.update { processed } } + + fun updatePreferences(prefs: AppPreferences) { + viewModelScope.launchDefault { + var backdropChanged = false + preferencesDataStore.updateData { + backdropChanged = + it.musicPreferences.showBackdrop != prefs.musicPreferences.showBackdrop + prefs + } + if (backdropChanged) { + if (prefs.musicPreferences.showBackdrop) { + updateBackdrop(getCurrent()) + } else { + backdropService.clearBackdrop() + } + } + } + } } diff --git a/app/src/main/proto/WholphinDataStore.proto b/app/src/main/proto/WholphinDataStore.proto index 4343a09d..ad8c7a20 100644 --- a/app/src/main/proto/WholphinDataStore.proto +++ b/app/src/main/proto/WholphinDataStore.proto @@ -182,6 +182,7 @@ message MusicPreferences { bool show_lyrics = 1; bool show_visualizer = 2; bool show_album_Art = 3; + bool show_backdrop = 4; } message AppPreferences { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dcee5679..3872fdef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -738,6 +738,9 @@ Show lyrics Song has lyrics Remove from queue + Show album cover + Show visualizer + Show backdrop