Fixes for stopping now playing

This commit is contained in:
Damontecres 2026-03-04 09:19:11 -05:00
parent c443ddb6c4
commit 3359c8ac32
No known key found for this signature in database
5 changed files with 71 additions and 29 deletions

View file

@ -96,7 +96,13 @@ class MusicService
} }
mediaSession?.release() mediaSession?.release()
mediaSession = null mediaSession = null
onMain { player.stop() } onMain {
player.stop()
player.setMediaItems(emptyList())
}
_state.update {
MusicServiceState.EMPTY
}
} }
} }
@ -236,7 +242,6 @@ class MusicService
) )
} }
} }
start()
} }
suspend fun moveQueue( suspend fun moveQueue(
@ -256,7 +261,10 @@ class MusicService
} }
suspend fun playIndex(index: Int) { suspend fun playIndex(index: Int) {
onMain { player.seekTo(index, 0L) } onMain {
player.seekTo(index, 0L)
player.play()
}
// MusicPlayerListener will update state // MusicPlayerListener will update state
} }
@ -285,15 +293,21 @@ data class MusicServiceState(
val queueSize: Int, val queueSize: Int,
val currentIndex: Int, val currentIndex: Int,
val currentItemId: UUID?, val currentItemId: UUID?,
val isPlaying: Boolean, val status: NowPlayingStatus,
val currentItemTitle: String?, val currentItemTitle: String?,
val loadingState: LoadingState = LoadingState.Pending, val loadingState: LoadingState = LoadingState.Pending,
) { ) {
companion object { companion object {
val EMPTY = MusicServiceState(0L, 0, 0, null, false, null) val EMPTY = MusicServiceState(0L, 0, 0, null, NowPlayingStatus.IDLE, null)
} }
} }
enum class NowPlayingStatus {
PLAYING,
PAUSED,
IDLE,
}
/** /**
* Listens to [Player] events and updates the [StateFlow] * Listens to [Player] events and updates the [StateFlow]
*/ */
@ -307,7 +321,7 @@ private class MusicPlayerListener(
it.copy( it.copy(
queueSize = player.mediaItemCount, queueSize = player.mediaItemCount,
currentIndex = player.currentMediaItemIndex, currentIndex = player.currentMediaItemIndex,
isPlaying = player.isPlaying, status = if (player.isPlaying) NowPlayingStatus.PLAYING else NowPlayingStatus.IDLE,
) )
} }
} }
@ -316,7 +330,7 @@ private class MusicPlayerListener(
Timber.v("MusicPlayerListener onIsPlayingChanged") Timber.v("MusicPlayerListener onIsPlayingChanged")
state.update { state.update {
it.copy( it.copy(
isPlaying = isPlaying, status = if (isPlaying) NowPlayingStatus.PLAYING else NowPlayingStatus.PAUSED,
) )
} }
} }

View file

@ -36,6 +36,7 @@ import timber.log.Timber
import java.util.UUID import java.util.UUID
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
import kotlin.time.Duration.Companion.minutes
@Singleton @Singleton
class NavDrawerService class NavDrawerService
@ -80,22 +81,33 @@ class NavDrawerService
coroutineScope.launchDefault { coroutineScope.launchDefault {
musicService.state.collectLatest { music -> musicService.state.collectLatest { music ->
Timber.v("MusicService updated") Timber.v("MusicService updated")
if (music.isPlaying) { when (music.status) {
_state.update { NowPlayingStatus.PLAYING -> {
it.copy( _state.update {
nowPlayingEnabled = true, it.copy(
nowPlayingTitle = music.currentItemTitle, nowPlayingEnabled = true,
) nowPlayingTitle = music.currentItemTitle,
)
}
} }
} else {
// Don't immediately remove the now playing NowPlayingStatus.IDLE -> {
// TODO need better now playing state to distinguish between paused & stopped _state.update {
delay(30_000) it.copy(
_state.update { nowPlayingEnabled = false,
it.copy( nowPlayingTitle = null,
nowPlayingEnabled = false, )
nowPlayingTitle = null, }
) }
NowPlayingStatus.PAUSED -> {
delay(2.minutes)
_state.update {
it.copy(
nowPlayingEnabled = false,
nowPlayingTitle = null,
)
}
} }
} }
} }

View file

@ -3,6 +3,7 @@ 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.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup
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
@ -70,7 +71,7 @@ fun NowPlayingButtons(
val onControllerInteraction = remember { { controllerViewState.pulseControls() } } val onControllerInteraction = remember { { controllerViewState.pulseControls() } }
Box( Box(
modifier = modifier, modifier = modifier.focusGroup(),
) { ) {
Row( Row(
horizontalArrangement = Arrangement.spacedBy(buttonSpacing), horizontalArrangement = Arrangement.spacedBy(buttonSpacing),

View file

@ -34,7 +34,9 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.FocusDirection
import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusProperties
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
@ -151,6 +153,10 @@ fun NowPlayingOverlay(
.fillMaxSize() .fillMaxSize()
.onFocusChanged { .onFocusChanged {
queueHasFocus = it.hasFocus queueHasFocus = it.hasFocus
}.focusProperties {
onExit = {
if (requestedFocusDirection == FocusDirection.Up) focusRequester.requestFocus()
}
}, },
) { ) {
itemsIndexed(queue, key = { _, song -> song.key }) { index, song -> itemsIndexed(queue, key = { _, song -> song.key }) { index, song ->
@ -162,7 +168,10 @@ fun NowPlayingOverlay(
.background( .background(
color = MaterialTheme.colorScheme.surface.copy(alpha = .75f), color = MaterialTheme.colorScheme.surface.copy(alpha = .75f),
shape = RoundedCornerShape(8.dp), shape = RoundedCornerShape(8.dp),
).animateItem(), ).onFocusChanged {
if (it.hasFocus) showButtons = index < 3
controllerViewState.pulseControls()
}.animateItem(),
) { ) {
SongListItem( SongListItem(
title = song.title, title = song.title,
@ -176,10 +185,7 @@ fun NowPlayingOverlay(
modifier = modifier =
Modifier Modifier
.weight(1f) .weight(1f)
.onFocusChanged { .ifElse(
if (it.isFocused) showButtons = index < 3
controllerViewState.pulseControls()
}.ifElse(
index == 0, index == 0,
Modifier.focusRequester(firstFocusRequester), Modifier.focusRequester(firstFocusRequester),
), ),

View file

@ -76,6 +76,7 @@ import com.github.damontecres.wholphin.data.model.JellyfinUser
import com.github.damontecres.wholphin.preferences.AppThemeColors import com.github.damontecres.wholphin.preferences.AppThemeColors
import com.github.damontecres.wholphin.preferences.UserPreferences import com.github.damontecres.wholphin.preferences.UserPreferences
import com.github.damontecres.wholphin.services.BackdropService import com.github.damontecres.wholphin.services.BackdropService
import com.github.damontecres.wholphin.services.MusicService
import com.github.damontecres.wholphin.services.NavDrawerService import com.github.damontecres.wholphin.services.NavDrawerService
import com.github.damontecres.wholphin.services.NavigationManager import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.SetupDestination import com.github.damontecres.wholphin.services.SetupDestination
@ -108,6 +109,7 @@ class NavDrawerViewModel
val navigationManager: NavigationManager, val navigationManager: NavigationManager,
val setupNavigationManager: SetupNavigationManager, val setupNavigationManager: SetupNavigationManager,
val backdropService: BackdropService, val backdropService: BackdropService,
private val musicService: MusicService,
) : ViewModel() { ) : ViewModel() {
val state = navDrawerService.state val state = navDrawerService.state
@ -204,6 +206,13 @@ class NavDrawerViewModel
} }
} }
} }
fun navigateToSetup(userList: SetupDestination) {
viewModelScope.launchDefault {
musicService.stop()
setupNavigationManager.navigateTo(userList)
}
}
} }
sealed interface NavDrawerItem { sealed interface NavDrawerItem {
@ -334,7 +343,7 @@ fun NavDrawer(
drawerOpen = isOpen, drawerOpen = isOpen,
interactionSource = interactionSource, interactionSource = interactionSource,
onClick = { onClick = {
viewModel.setupNavigationManager.navigateTo( viewModel.navigateToSetup(
SetupDestination.UserList(server), SetupDestination.UserList(server),
) )
}, },