diff --git a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt index 6596ba3f..e8f046fa 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/MainActivity.kt @@ -87,7 +87,6 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.jellyfin.sdk.model.api.BaseItemKind -import org.jellyfin.sdk.model.api.CollectionType import org.jellyfin.sdk.model.serializer.toUUID import org.jellyfin.sdk.model.serializer.toUUIDOrNull import timber.log.Timber @@ -326,9 +325,8 @@ class MainActivity : AppCompatActivity() { // ?: Destination.Home(), // TODO undo ?: Destination.MediaItem( - itemId = "7e64e319-657a-9516-ec78-490da03edccb".toUUID(), - type = BaseItemKind.COLLECTION_FOLDER, - collectionType = CollectionType.MUSIC, + itemId = "011ef0c7-ca45-684f-2cd9-dd3b020ca5f6".toUUID(), + type = BaseItemKind.MUSIC_ALBUM, ), navigationManager = navigationManager, preferences = preferences, diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt index 737ee7fa..451a8f18 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/MusicService.kt @@ -99,7 +99,7 @@ class MusicService player.shuffleModeEnabled = shuffled player.play() } - addAllToQueue(items) + addAllToQueue(items, startIndex) } suspend fun setQueue( @@ -140,16 +140,26 @@ class MusicService } } - suspend fun addAllToQueue(list: BlockingList) { + suspend fun addAllToQueue( + list: BlockingList, + startIndex: Int, + ) { + var remaining = startIndex list.indices .chunked(25) .forEach { val mediaItems = it.mapNotNull { - list - .getBlocking(it) - ?.takeIf { it.type == BaseItemKind.AUDIO } - ?.let(::convert) + if (remaining == 0) { + list + .getBlocking(it) + ?.takeIf { it.type == BaseItemKind.AUDIO } + ?.let(::convert) + } else { + Timber.v("Skipping $remaining") + remaining-- + null + } } onMain { player.addMediaItems(mediaItems) } } @@ -243,18 +253,24 @@ private class MusicPlayerListener( timeline: Timeline, reason: Int, ) { - Timber.v("MusicPlayerListener onTimelineChanged") +// Timber.v("MusicPlayerListener onTimelineChanged") if (reason == Player.TIMELINE_CHANGE_REASON_PLAYLIST_CHANGED) { updateCurrentIndex() } } private fun updateCurrentIndex() { - state.update { - it.copy( - currentIndex = player.currentMediaItemIndex, - currentItemId = player.getMediaItemAt(player.currentMediaItemIndex).mediaId.toUUIDOrNull(), - ) + state.update { state -> + player.currentMediaItemIndex.takeIf { it >= 0 }?.let { currentMediaItemIndex -> + if (currentMediaItemIndex in (0.. 0 + } + } + + val firstFocusRequester = remember { FocusRequester() } + BackHandler(hideButtons) { + scope.launch { + listState.animateScrollToItem(0) + firstFocusRequester.tryRequestFocus() + } + } + + Column( + modifier = + modifier + .padding(16.dp) + .fillMaxHeight(height), + ) { current?.title?.let { - Text(it) + Text( + text = it, + style = MaterialTheme.typography.titleMedium, + ) } current?.albumTitle?.let { - Text(it) + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + ) } current?.artistNames?.let { - Text(it) + Text( + text = it, + style = MaterialTheme.typography.titleSmall, + ) } SeekBar( player = player, @@ -69,47 +135,97 @@ fun NowPlayingOverlay( seekForward = Duration.ZERO, modifier = Modifier - .padding(vertical = 8.dp) + .padding(top = 8.dp) .fillMaxWidth(.95f), ) - Row( - modifier = Modifier.align(Alignment.CenterHorizontally), + AnimatedVisibility( + visible = !hideButtons, + enter = expandVertically(), + exit = shrinkVertically(), + modifier = + Modifier + .align(Alignment.CenterHorizontally), ) { - PlaybackButtons( - player = player, - initialFocusRequester = focusRequester, - onControllerInteraction = { controllerViewState.pulseControls() }, - onPlaybackActionClick = { - when (it) { - PlaybackAction.Next -> { - nextState.onClick() - } + Row( + modifier = + Modifier + .align(Alignment.CenterHorizontally), + ) { + PlaybackButtons( + player = player, + initialFocusRequester = focusRequester, + onControllerInteraction = { controllerViewState.pulseControls() }, + onPlaybackActionClick = { + when (it) { + PlaybackAction.Next -> { + nextState.onClick() + } - PlaybackAction.Previous -> { - previousState.onClick() - } + PlaybackAction.Previous -> { + previousState.onClick() + } - is PlaybackAction.ToggleCaptions -> { - TODO() - } + is PlaybackAction.ToggleCaptions -> { + TODO() + } - else -> {} - } - }, - showPlay = playPauseState.showPlay, - previousEnabled = previousState.isEnabled, - nextEnabled = nextState.isEnabled, - seekBack = 10.seconds, - skipBackOnResume = null, - seekForward = 30.seconds, - ) + else -> {} + } + }, + showPlay = playPauseState.showPlay, + previousEnabled = previousState.isEnabled, + nextEnabled = nextState.isEnabled, + seekBack = 10.seconds, + skipBackOnResume = null, + seekForward = 30.seconds, // TODO + ) + PlaybackFaButton( + iconRes = R.string.fa_shuffle, + onClick = { + shuffleState.onClick() + }, + onControllerInteraction = { controllerViewState.pulseControls() }, + textColor = + if (shuffleState.shuffleOn) { + MaterialTheme.colorScheme.secondary + } else { + Color.Unspecified + }, + ) + PlaybackFaButton( + iconRes = R.string.fa_repeat, + onClick = { + repeatState.onClick() + }, + onControllerInteraction = { controllerViewState.pulseControls() }, + textColor = + when (repeatState.repeatModeState) { + Player.REPEAT_MODE_ALL -> MaterialTheme.colorScheme.secondary + + // TODO + Player.REPEAT_MODE_ONE -> MaterialTheme.colorScheme.tertiary + + else -> Color.Unspecified + }, + ) + } } if (queue.isEmpty()) { Text("No items") } else { + Text( + text = stringResource(R.string.queue), + style = MaterialTheme.typography.titleMedium, + ) LazyColumn( - contentPadding = PaddingValues(16.dp), - modifier = Modifier.fillMaxSize(), + state = listState, + contentPadding = PaddingValues(bottom = 16.dp, start = 16.dp, end = 16.dp), + modifier = + Modifier + .fillMaxSize() + .onFocusChanged { + queueHasFocus = it.hasFocus + }, ) { itemsIndexed(queue) { index, song -> SongListItem( @@ -123,7 +239,11 @@ fun NowPlayingOverlay( player.seekTo(index, 0L) }, onLongClick = {}, - modifier = Modifier, + modifier = + Modifier.ifElse( + index == 0, + Modifier.focusRequester(firstFocusRequester), + ), ) } } 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 fcaae4d7..e8a00225 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 @@ -5,6 +5,7 @@ import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandHorizontally import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.background import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -31,6 +32,7 @@ import com.github.damontecres.wholphin.preferences.AppPreferences import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.skipBackOnResume import com.github.damontecres.wholphin.services.rememberQueue +import com.github.damontecres.wholphin.ui.AppColors import com.github.damontecres.wholphin.ui.playback.PlaybackKeyHandler import com.github.damontecres.wholphin.ui.tryRequestFocus import kotlin.time.Duration.Companion.milliseconds @@ -127,14 +129,22 @@ fun NowPlayingPage( BackHandler(controllerViewState.controlsVisible) { controllerViewState.hideControls() } - AnimatedVisibility(controllerViewState.controlsVisible) { + AnimatedVisibility( + controllerViewState.controlsVisible, + modifier = + Modifier + .align(Alignment.BottomCenter), + ) { NowPlayingOverlay( state = state, player = player, current = current, queue = queue, controllerViewState = controllerViewState, - modifier = Modifier.fillMaxSize(), + modifier = + Modifier + .background(AppColors.TransparentBlack50) + .align(Alignment.BottomCenter), ) } } 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 f84032c7..fbaba251 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 @@ -87,8 +87,12 @@ class NowPlayingViewModel private suspend fun getCurrent(): AudioItem? { val mediaItem = - onMain { player.currentMediaItemIndex.let { player.getMediaItemAt(it) } } - return mediaItem.localConfiguration?.tag as? AudioItem + onMain { + player.currentMediaItemIndex + .takeIf { it >= 0 } + ?.let { player.getMediaItemAt(it) } + } + return mediaItem?.localConfiguration?.tag as? AudioItem } private fun playbackLoop() { @@ -125,7 +129,7 @@ class NowPlayingViewModel } } - delay(250) + delay(150) } } } diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt index 75ac0951..417702e1 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackControls.kt @@ -5,10 +5,10 @@ package com.github.damontecres.wholphin.ui.playback import android.view.Gravity import androidx.annotation.DrawableRes import androidx.annotation.OptIn +import androidx.annotation.StringRes import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -24,7 +24,6 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable @@ -37,14 +36,18 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment 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.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.isSpecified import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogWindowProvider @@ -60,6 +63,7 @@ import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.wholphin.R import com.github.damontecres.wholphin.preferences.AppThemeColors import com.github.damontecres.wholphin.ui.AppColors +import com.github.damontecres.wholphin.ui.FontAwesome import com.github.damontecres.wholphin.ui.PreviewTvSpec import com.github.damontecres.wholphin.ui.components.Button import com.github.damontecres.wholphin.ui.components.SelectedLeadingContent @@ -462,6 +466,54 @@ fun PlaybackButton( } } +@Composable +fun PlaybackFaButton( + @StringRes iconRes: Int, + onClick: () -> Unit, + onControllerInteraction: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, + textColor: Color = Color.Unspecified, +) { + val selectedColor = MaterialTheme.colorScheme.border + Button( + enabled = enabled, + onClick = onClick, +// shape = ButtonDefaults.shape(CircleShape), + colors = + ClickableSurfaceDefaults.colors( + containerColor = AppColors.TransparentBlack25, + focusedContainerColor = selectedColor, + ), + contentPadding = PaddingValues(4.dp), + interactionSource = interactionSource, + modifier = + modifier + .size(36.dp, 36.dp) + .onFocusChanged { onControllerInteraction.invoke() }, + ) { + Text( + text = stringResource(iconRes), + fontSize = 18.sp, + fontFamily = FontAwesome, + textAlign = TextAlign.Center, + color = + if (textColor.isSpecified) { + textColor + } else if (LocalTheme.current == AppThemeColors.OLED_BLACK) { + LocalContentColor.current + } else { + MaterialTheme.colorScheme.onSurface + }, + modifier = + Modifier + .fillMaxWidth() + .align(Alignment.CenterVertically), + ) + } +} + @Composable fun BottomDialog( choices: List>, @@ -545,12 +597,18 @@ data class BottomDialogItem( @Composable private fun ButtonPreview() { WholphinTheme { - Row { + Row(Modifier.background(Color.Red)) { PlaybackButton( iconRes = R.drawable.baseline_play_arrow_24, onClick = {}, onControllerInteraction = {}, ) + PlaybackFaButton( + iconRes = R.string.fa_shuffle, + onClick = {}, + onControllerInteraction = {}, + textColor = Color.Green, + ) } } } diff --git a/app/src/main/res/values/fa_strings.xml b/app/src/main/res/values/fa_strings.xml index d3c3537a..c7df2e07 100644 --- a/app/src/main/res/values/fa_strings.xml +++ b/app/src/main/res/values/fa_strings.xml @@ -54,4 +54,5 @@ +