mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +02:00
WIP
This commit is contained in:
parent
6a1725fd3d
commit
ea243975ad
11 changed files with 234 additions and 84 deletions
|
|
@ -325,8 +325,8 @@ class MainActivity : AppCompatActivity() {
|
||||||
// ?: Destination.Home(),
|
// ?: Destination.Home(),
|
||||||
// TODO undo
|
// TODO undo
|
||||||
?: Destination.MediaItem(
|
?: Destination.MediaItem(
|
||||||
itemId = "bbc3e562-6045-5521-dfda-7effa56e14f2".toUUID(),
|
itemId = "011ef0c7ca45684f2cd9dd3b020ca5f6".toUUID(),
|
||||||
type = BaseItemKind.MUSIC_ARTIST,
|
type = BaseItemKind.MUSIC_ALBUM,
|
||||||
),
|
),
|
||||||
navigationManager = navigationManager,
|
navigationManager = navigationManager,
|
||||||
preferences = preferences,
|
preferences = preferences,
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import com.github.damontecres.wholphin.data.model.AudioItem
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
|
||||||
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
import com.github.damontecres.wholphin.ui.DefaultItemFields
|
||||||
|
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
|
||||||
import com.github.damontecres.wholphin.ui.onMain
|
import com.github.damontecres.wholphin.ui.onMain
|
||||||
import com.github.damontecres.wholphin.ui.toServerString
|
import com.github.damontecres.wholphin.ui.toServerString
|
||||||
import com.github.damontecres.wholphin.util.BlockingList
|
import com.github.damontecres.wholphin.util.BlockingList
|
||||||
|
|
@ -200,6 +201,14 @@ class MusicService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun moveQueue(
|
||||||
|
index: Int,
|
||||||
|
direction: MoveDirection,
|
||||||
|
) = withContext(Dispatchers.Main) {
|
||||||
|
player.moveMediaItem(index, if (direction == MoveDirection.UP) index - 1 else index + 1)
|
||||||
|
updateQueueSize()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
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
|
||||||
|
|
@ -21,9 +22,11 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
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.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -330,6 +333,9 @@ fun AlbumDetailsPage(
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.songs),
|
text = stringResource(R.string.songs),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemsIndexed(state.songs) { index, song ->
|
itemsIndexed(state.songs) { index, song ->
|
||||||
|
|
@ -389,7 +395,11 @@ fun AlbumHeader(
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageUrl,
|
model = imageUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxWidth(.25f),
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(.20f)
|
||||||
|
.clip(RoundedCornerShape(16.dp)),
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
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
|
||||||
|
|
@ -22,9 +23,11 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
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.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
|
@ -58,9 +61,11 @@ import com.github.damontecres.wholphin.ui.components.GenreText
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.components.OverviewText
|
import com.github.damontecres.wholphin.ui.components.OverviewText
|
||||||
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
import com.github.damontecres.wholphin.ui.components.QuickDetails
|
||||||
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
import com.github.damontecres.wholphin.ui.toBaseItems
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
|
@ -149,13 +154,44 @@ class ArtistViewModel
|
||||||
loading = LoadingState.Success,
|
loading = LoadingState.Success,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
backdropService.submit(artist)
|
|
||||||
|
viewModelScope.launchIO {
|
||||||
|
val request =
|
||||||
|
GetItemsRequest(
|
||||||
|
parentId = itemId,
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
recursive = true,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||||
|
minCommunityRating = 1.0,
|
||||||
|
sortBy =
|
||||||
|
listOf(
|
||||||
|
ItemSortBy.COMMUNITY_RATING,
|
||||||
|
),
|
||||||
|
sortOrder = listOf(SortOrder.DESCENDING),
|
||||||
|
limit = 10,
|
||||||
|
)
|
||||||
|
val topSongs =
|
||||||
|
GetItemsRequestHandler.execute(api, request).toBaseItems(api, false)
|
||||||
|
if (topSongs.isNotEmpty()) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(topSongs = topSongs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (ex: Exception) {
|
} catch (ex: Exception) {
|
||||||
_state.update { it.copy(loading = LoadingState.Error(ex)) }
|
_state.update { it.copy(loading = LoadingState.Error(ex)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun init() {
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
state.value.artist?.let {
|
||||||
|
backdropService.submit(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setFavorite(
|
fun setFavorite(
|
||||||
itemId: UUID,
|
itemId: UUID,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
|
|
@ -169,14 +205,25 @@ class ArtistViewModel
|
||||||
_state.update { it.copy(artist = artist) }
|
_state.update { it.copy(artist = artist) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun play(
|
fun play(shuffled: Boolean) {
|
||||||
shuffled: Boolean,
|
|
||||||
startIndex: Int = 0,
|
|
||||||
) {
|
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
Timber.v("Playing artist %s from %s", itemId, startIndex)
|
Timber.v("Playing artist %s", itemId)
|
||||||
val songs = state.value.topSongs as ApiRequestPager<*>
|
val request =
|
||||||
musicService.setQueue(songs, startIndex, shuffled)
|
GetItemsRequest(
|
||||||
|
parentId = itemId,
|
||||||
|
fields = DefaultItemFields,
|
||||||
|
recursive = true,
|
||||||
|
includeItemTypes = listOf(BaseItemKind.AUDIO),
|
||||||
|
sortBy =
|
||||||
|
listOf(
|
||||||
|
ItemSortBy.PREMIERE_DATE,
|
||||||
|
ItemSortBy.INDEX_NUMBER,
|
||||||
|
),
|
||||||
|
sortOrder = listOf(SortOrder.DESCENDING, SortOrder.ASCENDING),
|
||||||
|
)
|
||||||
|
val pager =
|
||||||
|
ApiRequestPager(api, request, GetItemsRequestHandler, viewModelScope).init()
|
||||||
|
musicService.setQueue(pager, 0, shuffled)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,6 +246,12 @@ class ArtistViewModel
|
||||||
navigationManager.navigateTo(Destination.NowPlaying)
|
navigationManager.navigateTo(Destination.NowPlaying)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun playSong(song: BaseItem) {
|
||||||
|
viewModelScope.launchDefault {
|
||||||
|
musicService.setQueue(listOf(song), false)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class ArtistState(
|
data class ArtistState(
|
||||||
|
|
@ -233,7 +286,7 @@ fun ArtistDetailsPage(
|
||||||
remember {
|
remember {
|
||||||
MusicMoreDialogActions(
|
MusicMoreDialogActions(
|
||||||
onNavigate = { viewModel.navigationManager.navigateTo(it) },
|
onNavigate = { viewModel.navigationManager.navigateTo(it) },
|
||||||
onClickPlay = { index, _ -> viewModel.play(false, index) },
|
onClickPlay = { index, song -> TODO() },
|
||||||
onClickAddToQueue = { index, itemId -> viewModel.addToQueue(itemId, index) },
|
onClickAddToQueue = { index, itemId -> viewModel.addToQueue(itemId, index) },
|
||||||
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
onClickFavorite = { itemId, favorite -> viewModel.setFavorite(itemId, favorite) },
|
||||||
onClickAddPlaylist = {},
|
onClickAddPlaylist = {},
|
||||||
|
|
@ -254,7 +307,10 @@ fun ArtistDetailsPage(
|
||||||
LoadingState.Success -> {
|
LoadingState.Success -> {
|
||||||
val artist = state.artist!!
|
val artist = state.artist!!
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
LaunchedEffect(Unit) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
viewModel.init()
|
||||||
|
}
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
Box(modifier = modifier) {
|
Box(modifier = modifier) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|
@ -284,7 +340,7 @@ fun ArtistDetailsPage(
|
||||||
actions =
|
actions =
|
||||||
remember {
|
remember {
|
||||||
MusicButtonActions(
|
MusicButtonActions(
|
||||||
onClickPlay = { viewModel.play(it, 0) },
|
onClickPlay = { viewModel.play(it) },
|
||||||
onClickInstantMix = viewModel::startInstantMix,
|
onClickInstantMix = viewModel::startInstantMix,
|
||||||
onClickFavorite = {
|
onClickFavorite = {
|
||||||
viewModel.setFavorite(
|
viewModel.setFavorite(
|
||||||
|
|
@ -320,13 +376,16 @@ fun ArtistDetailsPage(
|
||||||
if (state.topSongs.isNotEmpty()) {
|
if (state.topSongs.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.songs),
|
text = stringResource(R.string.popular_songs),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
itemsIndexed(state.topSongs) { index, song ->
|
itemsIndexed(state.topSongs) { index, song ->
|
||||||
SongListItem(
|
SongListItem(
|
||||||
song = song,
|
song = song,
|
||||||
onClick = { viewModel.play(false, index) },
|
onClick = { song?.let { viewModel.playSong(it) } },
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
if (song != null) {
|
if (song != null) {
|
||||||
moreDialog =
|
moreDialog =
|
||||||
|
|
@ -403,7 +462,11 @@ fun ArtistHeader(
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = imageUrl,
|
model = imageUrl,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.fillMaxWidth(.25f),
|
contentScale = ContentScale.FillWidth,
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth(.20f)
|
||||||
|
.clip(RoundedCornerShape(16.dp)),
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
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
|
||||||
|
|
@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
|
@ -46,11 +48,13 @@ import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.R
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.AudioItem
|
import com.github.damontecres.wholphin.data.model.AudioItem
|
||||||
import com.github.damontecres.wholphin.ui.ifElse
|
import com.github.damontecres.wholphin.ui.ifElse
|
||||||
|
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
|
||||||
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.PlaybackButtons
|
import com.github.damontecres.wholphin.ui.playback.PlaybackButtons
|
||||||
import com.github.damontecres.wholphin.ui.playback.PlaybackFaButton
|
import com.github.damontecres.wholphin.ui.playback.PlaybackFaButton
|
||||||
import com.github.damontecres.wholphin.ui.playback.SeekBar
|
import com.github.damontecres.wholphin.ui.playback.SeekBar
|
||||||
|
import com.github.damontecres.wholphin.ui.preferences.MoveButton
|
||||||
import com.github.damontecres.wholphin.ui.roundSeconds
|
import com.github.damontecres.wholphin.ui.roundSeconds
|
||||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
@ -65,6 +69,7 @@ fun NowPlayingOverlay(
|
||||||
current: AudioItem?,
|
current: AudioItem?,
|
||||||
queue: List<AudioItem>,
|
queue: List<AudioItem>,
|
||||||
controllerViewState: ControllerViewState,
|
controllerViewState: ControllerViewState,
|
||||||
|
onMoveQueue: (Int, MoveDirection) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
@ -228,23 +233,46 @@ fun NowPlayingOverlay(
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
itemsIndexed(queue) { index, song ->
|
itemsIndexed(queue) { index, song ->
|
||||||
SongListItem(
|
Row(
|
||||||
title = song.title,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
artist = song.artistNames,
|
modifier = Modifier.fillMaxWidth(),
|
||||||
indexNumber = index + 1,
|
) {
|
||||||
runtime = song.runtime?.roundSeconds,
|
SongListItem(
|
||||||
showArtist = true,
|
title = song.title,
|
||||||
isPlaying = current?.id == song.id,
|
artist = song.artistNames,
|
||||||
onClick = {
|
indexNumber = index + 1,
|
||||||
player.seekTo(index, 0L)
|
runtime = song.runtime?.roundSeconds,
|
||||||
},
|
showArtist = true,
|
||||||
onLongClick = {},
|
isPlaying = current?.id == song.id,
|
||||||
modifier =
|
onClick = {
|
||||||
Modifier.ifElse(
|
player.seekTo(index, 0L)
|
||||||
index == 0,
|
},
|
||||||
Modifier.focusRequester(firstFocusRequester),
|
onLongClick = {},
|
||||||
),
|
modifier =
|
||||||
)
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.ifElse(
|
||||||
|
index == 0,
|
||||||
|
Modifier.focusRequester(firstFocusRequester),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.wrapContentWidth(),
|
||||||
|
) {
|
||||||
|
MoveButton(
|
||||||
|
icon = R.string.fa_caret_up,
|
||||||
|
enabled = index > 0,
|
||||||
|
onClick = { onMoveQueue.invoke(index, MoveDirection.UP) },
|
||||||
|
)
|
||||||
|
MoveButton(
|
||||||
|
icon = R.string.fa_caret_down,
|
||||||
|
enabled = index < queue.lastIndex,
|
||||||
|
onClick = { onMoveQueue.invoke(index, MoveDirection.DOWN) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import androidx.annotation.OptIn
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.expandHorizontally
|
import androidx.compose.animation.expandHorizontally
|
||||||
import androidx.compose.animation.shrinkHorizontally
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
|
@ -131,7 +133,9 @@ fun NowPlayingPage(
|
||||||
controllerViewState.hideControls()
|
controllerViewState.hideControls()
|
||||||
}
|
}
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
controllerViewState.controlsVisible,
|
visible = controllerViewState.controlsVisible,
|
||||||
|
enter = slideInVertically { it },
|
||||||
|
exit = slideOutVertically { it },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.BottomCenter),
|
.align(Alignment.BottomCenter),
|
||||||
|
|
@ -142,6 +146,7 @@ fun NowPlayingPage(
|
||||||
current = current,
|
current = current,
|
||||||
queue = queue,
|
queue = queue,
|
||||||
controllerViewState = controllerViewState,
|
controllerViewState = controllerViewState,
|
||||||
|
onMoveQueue = { index, direction -> viewModel.moveQueue(index, direction) },
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.background(AppColors.TransparentBlack50)
|
.background(AppColors.TransparentBlack50)
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import com.github.damontecres.wholphin.services.MusicService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
import com.github.damontecres.wholphin.services.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
|
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
|
||||||
import com.github.damontecres.wholphin.ui.onMain
|
import com.github.damontecres.wholphin.ui.onMain
|
||||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||||
import com.mayakapps.kache.InMemoryKache
|
import com.mayakapps.kache.InMemoryKache
|
||||||
|
|
@ -164,4 +165,9 @@ class NowPlayingViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun moveQueue(
|
||||||
|
index: Int,
|
||||||
|
direction: MoveDirection,
|
||||||
|
) = viewModelScope.launchDefault { musicService.moveQueue(index, direction) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package com.github.damontecres.wholphin.ui.detail.music
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
|
@ -22,7 +23,6 @@ import androidx.tv.material3.Text
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
import com.github.damontecres.wholphin.ui.PreviewTvSpec
|
||||||
import com.github.damontecres.wholphin.ui.enableMarquee
|
import com.github.damontecres.wholphin.ui.enableMarquee
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
|
||||||
import com.github.damontecres.wholphin.ui.letNotEmpty
|
import com.github.damontecres.wholphin.ui.letNotEmpty
|
||||||
import com.github.damontecres.wholphin.ui.roundMinutes
|
import com.github.damontecres.wholphin.ui.roundMinutes
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
|
@ -98,51 +98,66 @@ fun SongListItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
val focused by interactionSource.collectIsFocusedAsState()
|
val focused by interactionSource.collectIsFocusedAsState()
|
||||||
ListItem(
|
val leadingContent: @Composable (BoxScope.() -> Unit) = {
|
||||||
selected = isPlaying,
|
Row(
|
||||||
onClick = onClick,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
onLongClick = onLongClick,
|
) {
|
||||||
interactionSource = interactionSource,
|
Text(
|
||||||
leadingContent = {
|
text = indexNumber?.toString() ?: "",
|
||||||
Row(
|
)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
if (isPlaying) {
|
||||||
) {
|
Icon(
|
||||||
Text(
|
imageVector = Icons.Default.PlayArrow,
|
||||||
text = indexNumber?.toString() ?: "",
|
contentDescription = null,
|
||||||
)
|
)
|
||||||
if (isPlaying) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.PlayArrow,
|
|
||||||
contentDescription = null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
headlineContent = {
|
}
|
||||||
Text(
|
val headlineContent = @Composable {
|
||||||
text = title ?: "",
|
Text(
|
||||||
maxLines = 1,
|
text = title ?: "",
|
||||||
modifier = Modifier.enableMarquee(focused),
|
maxLines = 1,
|
||||||
)
|
modifier = Modifier.enableMarquee(focused),
|
||||||
},
|
)
|
||||||
supportingContent =
|
}
|
||||||
if (showArtist && artist.isNotNullOrBlank()) {
|
val trailingContent = @Composable {
|
||||||
{
|
Text(
|
||||||
Text(
|
text = runtime.toString(),
|
||||||
text = artist,
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
} else {
|
if (showArtist) {
|
||||||
null
|
// TODO use dense?
|
||||||
|
ListItem(
|
||||||
|
selected = isPlaying,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
text = artist ?: "",
|
||||||
|
)
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = trailingContent,
|
||||||
Text(
|
scale = ListItemDefaults.scale(1f, 1f, .95f),
|
||||||
text = runtime.toString(),
|
modifier = Modifier,
|
||||||
)
|
)
|
||||||
},
|
} else {
|
||||||
scale = ListItemDefaults.scale(1f, 1f, .95f),
|
ListItem(
|
||||||
modifier = Modifier.weight(1f),
|
selected = isPlaying,
|
||||||
)
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
leadingContent = leadingContent,
|
||||||
|
headlineContent = headlineContent,
|
||||||
|
supportingContent = null,
|
||||||
|
trailingContent = trailingContent,
|
||||||
|
scale = ListItemDefaults.scale(1f, 1f, .95f),
|
||||||
|
modifier = Modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import com.github.damontecres.wholphin.ui.components.BasicDialog
|
||||||
import com.github.damontecres.wholphin.ui.components.Button
|
import com.github.damontecres.wholphin.ui.components.Button
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
import com.github.damontecres.wholphin.ui.main.settings.MoveDirection
|
||||||
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
import com.github.damontecres.wholphin.ui.nav.NavDrawerItem
|
||||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
|
@ -85,11 +86,6 @@ data class NavDrawerPin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class MoveDirection {
|
|
||||||
UP,
|
|
||||||
DOWN,
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> List<T>.move(
|
private fun <T> List<T>.move(
|
||||||
direction: MoveDirection,
|
direction: MoveDirection,
|
||||||
index: Int,
|
index: Int,
|
||||||
|
|
@ -246,7 +242,7 @@ fun NavDrawerPreferenceListItem(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MoveButton(
|
fun MoveButton(
|
||||||
@StringRes icon: Int,
|
@StringRes icon: Int,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,26 @@
|
||||||
package com.github.damontecres.wholphin.util
|
package com.github.damontecres.wholphin.util
|
||||||
|
|
||||||
|
import java.util.function.IntFunction
|
||||||
import java.util.function.Predicate
|
import java.util.function.Predicate
|
||||||
|
|
||||||
interface BlockingList<T> : List<T> {
|
interface BlockingList<T> : List<T> {
|
||||||
suspend fun getBlocking(index: Int): T
|
suspend fun getBlocking(index: Int): T
|
||||||
|
|
||||||
suspend fun indexOfBlocking(predicate: Predicate<T>): Int
|
suspend fun indexOfBlocking(predicate: Predicate<T>): Int
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun <T> of(list: List<T>): BlockingList<T> = BlockingListWrapper(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BlockingListWrapper<T>(
|
||||||
|
private val list: List<T>,
|
||||||
|
) : BlockingList<T>,
|
||||||
|
List<T> by list {
|
||||||
|
override suspend fun getBlocking(index: Int): T = get(index)
|
||||||
|
|
||||||
|
override suspend fun indexOfBlocking(predicate: Predicate<T>): Int = indexOfFirst { predicate.test(it) }
|
||||||
|
|
||||||
|
@Deprecated("Deprecated")
|
||||||
|
override fun <T> toArray(generator: IntFunction<Array<out T?>?>): Array<out T?> = super<List>.toArray(generator)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -726,5 +726,6 @@
|
||||||
<string name="artist">Artist</string>
|
<string name="artist">Artist</string>
|
||||||
<string name="album_artist">Album artist</string>
|
<string name="album_artist">Album artist</string>
|
||||||
<string name="album">Album</string>
|
<string name="album">Album</string>
|
||||||
|
<string name="popular_songs">Popular songs</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue