mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Customize now playing page
This commit is contained in:
parent
29269faa63
commit
7233ffa0d9
7 changed files with 278 additions and 74 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -8,11 +8,35 @@ import com.github.damontecres.wholphin.preferences.updateMusicPreferences
|
|||
fun getMusicPreferences() =
|
||||
listOf(
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
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<AppPreferences>(
|
||||
title = R.string.show_visualizer,
|
||||
defaultValue = false,
|
||||
getter = { it.musicPreferences.showVisualizer },
|
||||
setter = { prefs, value ->
|
||||
prefs.updateMusicPreferences { showVisualizer = value }
|
||||
},
|
||||
),
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.show_backdrop,
|
||||
defaultValue = false,
|
||||
getter = { it.musicPreferences.showBackdrop },
|
||||
setter = { prefs, value ->
|
||||
prefs.updateMusicPreferences { showBackdrop = value }
|
||||
},
|
||||
),
|
||||
AppSwitchPreference<AppPreferences>(
|
||||
title = R.string.show_lyrics,
|
||||
defaultValue = false,
|
||||
getter = { it.musicPreferences.showLyrics },
|
||||
setter = { prefs, value ->
|
||||
prefs.updateMusicPreferences { showLyrics = value }
|
||||
},
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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<AppPreferences, Any>
|
||||
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 ->
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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<DialogParams?>(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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<AppPreferences>,
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -738,6 +738,9 @@
|
|||
<string name="show_lyrics">Show lyrics</string>
|
||||
<string name="song_has_lyrics">Song has lyrics</string>
|
||||
<string name="remove_from_queue">Remove from queue</string>
|
||||
<string name="show_album_cover">Show album cover</string>
|
||||
<string name="show_visualizer">Show visualizer</string>
|
||||
<string name="show_backdrop">Show backdrop</string>
|
||||
|
||||
|
||||
</resources>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue