Several minor fixes (#1505)
## Description Collecting some minor fixes together: - Adds error handling when submitting a discover/seerr request, previously there was no visual feedback if the request failed - Fix MpvPlayer reporting that it was buffering forever - Fix discover results row on regular search page not being clickable - Fix focus loss when returning to movie details after playback advanced enough to switch the button from Play to Resume - Fix some focus issues when returning to the TV guide page ### Related issues Related to #1503 for MPV Fixes #1500 Fixes #1508 Fixes #1501 ### Testing Emulator & nvidia shield ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
a1b372667b
commit
a0869ca6e4
7 changed files with 103 additions and 66 deletions
|
|
@ -26,6 +26,7 @@ import androidx.compose.material.icons.filled.MoreVert
|
|||
import androidx.compose.material.icons.filled.PlayArrow
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
|
|
@ -59,6 +60,7 @@ import com.github.damontecres.wholphin.ui.data.SortAndDirection
|
|||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.theme.PreviewInteractionSource
|
||||
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||
import org.jellyfin.sdk.model.api.SortOrder
|
||||
import kotlin.time.Duration
|
||||
|
|
@ -85,6 +87,14 @@ fun ExpandablePlayButtons(
|
|||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val firstFocus = remember { FocusRequester() }
|
||||
val restartInteractionSource = remember { MutableInteractionSource() }
|
||||
val restartButtonFocused by restartInteractionSource.collectIsFocusedAsState()
|
||||
LaunchedEffect(resumePosition) {
|
||||
// Reset focus if the restart button was focuses and it is going to disappear
|
||||
if (restartButtonFocused && resumePosition == Duration.ZERO) {
|
||||
firstFocus.tryRequestFocus()
|
||||
}
|
||||
}
|
||||
LazyRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
contentPadding = PaddingValues(8.dp),
|
||||
|
|
@ -93,19 +103,19 @@ fun ExpandablePlayButtons(
|
|||
.focusGroup()
|
||||
.focusRestorer(firstFocus),
|
||||
) {
|
||||
item("play") {
|
||||
ExpandablePlayButton(
|
||||
title = if (resumePosition > Duration.ZERO) R.string.resume else R.string.play,
|
||||
resume = resumePosition,
|
||||
icon = Icons.Default.PlayArrow,
|
||||
onClick = playOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged(buttonOnFocusChanged)
|
||||
.focusRequester(firstFocus),
|
||||
)
|
||||
}
|
||||
if (resumePosition > Duration.ZERO) {
|
||||
item("play") {
|
||||
ExpandablePlayButton(
|
||||
title = R.string.resume,
|
||||
resume = resumePosition,
|
||||
icon = Icons.Default.PlayArrow,
|
||||
onClick = playOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged(buttonOnFocusChanged)
|
||||
.focusRequester(firstFocus),
|
||||
)
|
||||
}
|
||||
item("restart") {
|
||||
ExpandablePlayButton(
|
||||
title = R.string.restart,
|
||||
|
|
@ -114,19 +124,7 @@ fun ExpandablePlayButtons(
|
|||
onClick = playOnClick,
|
||||
modifier = Modifier.onFocusChanged(buttonOnFocusChanged),
|
||||
mirrorIcon = true,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
item("play") {
|
||||
ExpandablePlayButton(
|
||||
title = R.string.play,
|
||||
resume = Duration.ZERO,
|
||||
icon = Icons.Default.PlayArrow,
|
||||
onClick = playOnClick,
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged(buttonOnFocusChanged)
|
||||
.focusRequester(firstFocus),
|
||||
interactionSource = restartInteractionSource,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ fun CollectionFolderLiveTv(
|
|||
when (selectedTabIndex) {
|
||||
0 -> {
|
||||
TvGuideGrid(
|
||||
preferences = preferences,
|
||||
onRowPosition = {
|
||||
showHeader = it <= 0
|
||||
},
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.github.damontecres.wholphin.services.SeerrUserConfig
|
|||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import com.github.damontecres.wholphin.util.successValue
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -168,7 +169,7 @@ class DiscoverMovieViewModel
|
|||
is4k: Boolean,
|
||||
) {
|
||||
viewModelScope.launchIO {
|
||||
val request =
|
||||
try {
|
||||
seerrService.api.requestApi.requestPost(
|
||||
RequestPostRequest(
|
||||
is4k = is4k,
|
||||
|
|
@ -176,6 +177,12 @@ class DiscoverMovieViewModel
|
|||
mediaType = RequestPostRequest.MediaType.MOVIE,
|
||||
),
|
||||
)
|
||||
} catch (ex: kotlinx.coroutines.CancellationException) {
|
||||
throw ex
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error requesting %s", id)
|
||||
showToast(context, "An error occurred")
|
||||
}
|
||||
fetchAndSetItem().await()
|
||||
updateCanCancel()
|
||||
}
|
||||
|
|
@ -185,7 +192,14 @@ class DiscoverMovieViewModel
|
|||
viewModelScope.launchIO {
|
||||
state.value.movie.successValue?.mediaInfo?.requests?.firstOrNull()?.let {
|
||||
// TODO handle multiple requests? Or just delete self's request?
|
||||
seerrService.api.requestApi.requestRequestIdDelete(it.id.toString())
|
||||
try {
|
||||
seerrService.api.requestApi.requestRequestIdDelete(it.id.toString())
|
||||
} catch (ex: kotlinx.coroutines.CancellationException) {
|
||||
throw ex
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error requesting %s", id)
|
||||
showToast(context, "An error occurred")
|
||||
}
|
||||
fetchAndSetItem().await()
|
||||
updateCanCancel()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.github.damontecres.wholphin.services.SeerrService
|
|||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.showToast
|
||||
import com.github.damontecres.wholphin.util.DataLoadingState
|
||||
import com.github.damontecres.wholphin.util.successValue
|
||||
import dagger.assisted.Assisted
|
||||
|
|
@ -209,27 +210,34 @@ class DiscoverSeriesViewModel
|
|||
it.requestedBy?.id ==
|
||||
seerrServerRepository.currentUserId.first()
|
||||
}
|
||||
if (currentRequest != null) {
|
||||
Timber.v("User has pending request, will update")
|
||||
seerrService.api.requestApi.requestRequestIdPut(
|
||||
requestId = currentRequest.id.toString(),
|
||||
requestRequestIdPutRequest =
|
||||
RequestRequestIdPutRequest(
|
||||
try {
|
||||
if (currentRequest != null) {
|
||||
Timber.v("User has pending request, will update")
|
||||
seerrService.api.requestApi.requestRequestIdPut(
|
||||
requestId = currentRequest.id.toString(),
|
||||
requestRequestIdPutRequest =
|
||||
RequestRequestIdPutRequest(
|
||||
is4k = is4k,
|
||||
mediaType = RequestRequestIdPutRequest.MediaType.TV,
|
||||
seasons = seasons.toList(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Timber.v("New request for %s seasons", seasons.size)
|
||||
seerrService.api.requestApi.requestPost(
|
||||
RequestPostRequest(
|
||||
is4k = is4k,
|
||||
mediaType = RequestRequestIdPutRequest.MediaType.TV,
|
||||
mediaId = id,
|
||||
mediaType = RequestPostRequest.MediaType.TV,
|
||||
seasons = seasons.toList(),
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Timber.v("New request for %s seasons", seasons.size)
|
||||
seerrService.api.requestApi.requestPost(
|
||||
RequestPostRequest(
|
||||
is4k = is4k,
|
||||
mediaId = id,
|
||||
mediaType = RequestPostRequest.MediaType.TV,
|
||||
seasons = seasons.toList(),
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (ex: CancellationException) {
|
||||
throw ex
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error requesting %s", id)
|
||||
showToast(context, "An error occurred")
|
||||
}
|
||||
|
||||
fetchAndSetItem().await()?.let {
|
||||
|
|
@ -244,7 +252,15 @@ class DiscoverSeriesViewModel
|
|||
viewModelScope.launchIO {
|
||||
state.value.tvSeries.successValue?.mediaInfo?.requests?.firstOrNull()?.let {
|
||||
// TODO handle multiple requests? Or just delete self's request?
|
||||
seerrService.api.requestApi.requestRequestIdDelete(it.id.toString())
|
||||
try {
|
||||
seerrService.api.requestApi.requestRequestIdDelete(it.id.toString())
|
||||
} catch (ex: CancellationException) {
|
||||
throw ex
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error requesting %s", id)
|
||||
showToast(context, "An error occurred")
|
||||
}
|
||||
|
||||
fetchAndSetItem().await()?.let {
|
||||
updateSeasonStatus(it)
|
||||
updateCanCancel()
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
|
|
@ -30,7 +29,6 @@ import androidx.compose.runtime.setValue
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -43,8 +41,8 @@ import androidx.tv.material3.Surface
|
|||
import androidx.tv.material3.SurfaceDefaults
|
||||
import androidx.tv.material3.Text
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.preferences.LiveTvPreferences
|
||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||
import com.github.damontecres.wholphin.ui.components.CircularProgress
|
||||
import com.github.damontecres.wholphin.ui.components.DialogItem
|
||||
import com.github.damontecres.wholphin.ui.components.DialogParams
|
||||
|
|
@ -57,6 +55,7 @@ import com.github.damontecres.wholphin.ui.data.RowColumn
|
|||
import com.github.damontecres.wholphin.ui.ifElse
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.rememberInt
|
||||
import com.github.damontecres.wholphin.ui.rememberPosition
|
||||
import com.github.damontecres.wholphin.ui.tryRequestFocus
|
||||
import com.github.damontecres.wholphin.util.LoadingState
|
||||
|
|
@ -75,14 +74,14 @@ import kotlin.math.abs
|
|||
|
||||
@Composable
|
||||
fun TvGuideGrid(
|
||||
preferences: UserPreferences,
|
||||
onRowPosition: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: LiveTvViewModel = hiltViewModel(),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
val state by viewModel.state.collectAsState()
|
||||
val preferences by viewModel.preferences.data
|
||||
.collectAsState(AppPreferences.getDefaultInstance())
|
||||
val preferences by viewModel.preferences.data.collectAsState(preferences.appPreferences)
|
||||
val tvPrefs = preferences.interfacePreferences.liveTvPreferences
|
||||
var showViewOptions by remember { mutableStateOf(false) }
|
||||
var showChannelDialog by remember { mutableStateOf<Pair<Int, TvChannel>?>(null) }
|
||||
|
|
@ -196,11 +195,11 @@ fun TvGuideGrid(
|
|||
showItemDialog = index
|
||||
}
|
||||
},
|
||||
focusRequester = focusRequester,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.focusRequester(focusRequester),
|
||||
.background(MaterialTheme.colorScheme.background),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -308,6 +307,7 @@ fun TvGuideGridContent(
|
|||
onClickChannel: (Int, TvChannel) -> Unit,
|
||||
onClickProgram: (Int, TvProgram) -> Unit,
|
||||
onFocus: (RowColumn) -> Unit,
|
||||
focusRequester: FocusRequester,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
|
@ -315,8 +315,10 @@ fun TvGuideGridContent(
|
|||
val scope = rememberCoroutineScope()
|
||||
val guideStart = remember(guideTimes) { guideTimes.first() }
|
||||
|
||||
var focusedChannelIndex by rememberInt(-1)
|
||||
var focusedItem by rememberPosition(RowColumn(0, 0))
|
||||
var focusedProgramIndex by remember { mutableIntStateOf(0) }
|
||||
var focusedProgramIndex by rememberInt(0)
|
||||
|
||||
LaunchedEffect(onFocus, focusedProgramIndex) {
|
||||
withContext(Dispatchers.Default) {
|
||||
val program = programs.programs.getOrNull(focusedProgramIndex)
|
||||
|
|
@ -358,16 +360,13 @@ fun TvGuideGridContent(
|
|||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier) {
|
||||
Box(
|
||||
modifier = modifier,
|
||||
) {
|
||||
ProgramGuide(
|
||||
state = state,
|
||||
dimensions = tvGuideDimensions,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.focusProperties {
|
||||
onEnter = { programFocusRequester.tryRequestFocus() }
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
) {
|
||||
guideStartHour = 0f
|
||||
currentTime(
|
||||
|
|
@ -465,8 +464,12 @@ fun TvGuideGridContent(
|
|||
.ifElse(
|
||||
programIndex == focusedProgramIndex,
|
||||
Modifier.focusRequester(programFocusRequester),
|
||||
).ifElse(
|
||||
focusedChannelIndex < 0 && programIndex == focusedProgramIndex,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
focusedChannelIndex = -1
|
||||
focusedProgramIndex = programIndex
|
||||
scope.launch {
|
||||
try {
|
||||
|
|
@ -497,8 +500,12 @@ fun TvGuideGridContent(
|
|||
onLongClick = {},
|
||||
modifier =
|
||||
Modifier
|
||||
.onFocusChanged {
|
||||
.ifElse(
|
||||
focusedChannelIndex == channelIndex,
|
||||
Modifier.focusRequester(focusRequester),
|
||||
).onFocusChanged {
|
||||
if (it.isFocused) {
|
||||
focusedChannelIndex = channelIndex
|
||||
scope.launch {
|
||||
try {
|
||||
state.animateToChannel(
|
||||
|
|
|
|||
|
|
@ -808,6 +808,7 @@ fun SearchPage(
|
|||
position = position,
|
||||
focusRequester = focusRequesters[SEERR_ROW],
|
||||
onClickItem = onClickItem,
|
||||
onClickDiscover = onClickDiscover,
|
||||
onClickPosition = { position = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -628,12 +628,11 @@ class MpvPlayer(
|
|||
|
||||
MPVProperty.PAUSED_FOR_CACHE -> {
|
||||
Timber.v("paused-for-cache %s", value)
|
||||
val newState = if (value) STATE_BUFFERING else STATE_READY
|
||||
playbackState.update {
|
||||
it.copy(
|
||||
state = if (value) STATE_BUFFERING else it.state,
|
||||
)
|
||||
it.copy(state = newState)
|
||||
}
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(playbackState.load().state) }
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(newState) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -842,6 +841,7 @@ class MpvPlayer(
|
|||
)
|
||||
}
|
||||
notifyListeners(EVENT_IS_LOADING_CHANGED) { onIsLoadingChanged(true) }
|
||||
notifyListeners(EVENT_PLAYBACK_STATE_CHANGED) { onPlaybackStateChanged(STATE_READY) }
|
||||
val url =
|
||||
media.mediaItem.localConfiguration
|
||||
?.uri
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue