Use ExceptionHandler

This commit is contained in:
Damontecres 2025-10-02 16:12:51 -04:00
parent 805f63cb0e
commit 9265a06fd7
No known key found for this signature in database
20 changed files with 207 additions and 87 deletions

View file

@ -46,6 +46,7 @@ import androidx.tv.material3.MaterialTheme
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.surfaceColorAtElevation import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.dolphin.ui.FontAwesome import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -229,7 +230,7 @@ fun ScrollableDialog(
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
fun scroll(reverse: Boolean = false) { fun scroll(reverse: Boolean = false) {
scope.launch { scope.launch(ExceptionHandler()) {
columnState.scrollBy(if (reverse) -scrollAmount else scrollAmount) columnState.scrollBy(if (reverse) -scrollAmount else scrollAmount)
} }
} }

View file

@ -38,6 +38,7 @@ import com.github.damontecres.dolphin.ui.components.TitleValueText
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.playOnClickSound import com.github.damontecres.dolphin.ui.playOnClickSound
import com.github.damontecres.dolphin.ui.playSoundOnFocus import com.github.damontecres.dolphin.ui.playSoundOnFocus
import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.SortedMap import java.util.SortedMap
import kotlin.time.Duration import kotlin.time.Duration
@ -137,7 +138,7 @@ fun VideoDetailsHeader(
.background(bgColor, shape = RoundedCornerShape(8.dp)) .background(bgColor, shape = RoundedCornerShape(8.dp))
.onFocusChanged { .onFocusChanged {
if (it.isFocused) { if (it.isFocused) {
scope.launch { bringIntoViewRequester.bringIntoView() } scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() }
} }
}.playSoundOnFocus(true) }.playSoundOnFocus(true)
.clickable( .clickable(
@ -181,7 +182,7 @@ fun VideoDetailsHeader(
}, },
moreOnClick = { moreOnClick.invoke() }, moreOnClick = { moreOnClick.invoke() },
buttonOnFocusChanged = { buttonOnFocusChanged = {
scope.launch { bringIntoViewRequester.bringIntoView() } scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() }
}, },
focusRequester = focusRequester, focusRequester = focusRequester,
modifier = Modifier, modifier = Modifier,

View file

@ -51,6 +51,7 @@ import com.github.damontecres.dolphin.ui.playback.isBackwardButton
import com.github.damontecres.dolphin.ui.playback.isForwardButton import com.github.damontecres.dolphin.ui.playback.isForwardButton
import com.github.damontecres.dolphin.ui.playback.isPlayKeyUp import com.github.damontecres.dolphin.ui.playback.isPlayKeyUp
import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.ItemPager import com.github.damontecres.dolphin.util.ItemPager
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
@ -169,7 +170,7 @@ fun CardGrid(
} }
val jump = { jump: Int -> val jump = { jump: Int ->
scope.launch { scope.launch(ExceptionHandler()) {
val newPosition = val newPosition =
(gridState.firstVisibleItemIndex + jump).coerceIn(0..<pager.size) (gridState.firstVisibleItemIndex + jump).coerceIn(0..<pager.size)
if (DEBUG) Timber.d("newPosition=$newPosition") if (DEBUG) Timber.d("newPosition=$newPosition")
@ -179,7 +180,7 @@ fun CardGrid(
} }
} }
val jumpToTop = { val jumpToTop = {
scope.launch { scope.launch(ExceptionHandler()) {
if (focusedIndex < (columns * 6)) { if (focusedIndex < (columns * 6)) {
// If close, animate the scroll // If close, animate the scroll
gridState.animateScrollToItem(0, 0) gridState.animateScrollToItem(0, 0)
@ -205,7 +206,7 @@ fun CardGrid(
if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition") if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition")
if (newPosition > 0) { if (newPosition > 0) {
focusOn(newPosition) focusOn(newPosition)
scope.launch { scope.launch(ExceptionHandler()) {
gridState.scrollToItem(newPosition, -columns) gridState.scrollToItem(newPosition, -columns)
firstFocus.tryRequestFocus() firstFocus.tryRequestFocus()
} }
@ -366,7 +367,7 @@ fun CardGrid(
AlphabetButtons( AlphabetButtons(
modifier = Modifier.align(Alignment.CenterVertically), modifier = Modifier.align(Alignment.CenterVertically),
letterClicked = { letter -> letterClicked = { letter ->
scope.launch { scope.launch(ExceptionHandler()) {
val jumpPosition = letterPosition.invoke(letter) val jumpPosition = letterPosition.invoke(letter)
Timber.d("Alphabet jump to $jumpPosition") Timber.d("Alphabet jump to $jumpPosition")
gridState.scrollToItem(jumpPosition) gridState.scrollToItem(jumpPosition)

View file

@ -16,6 +16,7 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.ItemPager import com.github.damontecres.dolphin.util.ItemPager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -43,7 +44,7 @@ class CollectionFolderViewModel
itemId: UUID, itemId: UUID,
potential: BaseItem?, potential: BaseItem?,
): Job = ): Job =
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
super.init(itemId, potential)?.join() super.init(itemId, potential)?.join()
setup() setup()
} }

View file

@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope
import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.data.model.DolphinModel import com.github.damontecres.dolphin.data.model.DolphinModel
import com.github.damontecres.dolphin.data.model.convertModel import com.github.damontecres.dolphin.data.model.convertModel
import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -32,7 +33,7 @@ abstract class ItemViewModel<T : DolphinModel>(
if (item.value?.id == itemId) { if (item.value?.id == itemId) {
return null return null
} }
return viewModelScope.launch { return viewModelScope.launch(ExceptionHandler()) {
try { try {
val fetchedItem = api.userLibraryApi.getItem(itemId).content val fetchedItem = api.userLibraryApi.getItem(itemId).content
val modelInstance = convertModel(fetchedItem, api) val modelInstance = convertModel(fetchedItem, api)

View file

@ -21,6 +21,7 @@ import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.ItemRow import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.ItemPager import com.github.damontecres.dolphin.util.ItemPager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -46,7 +47,7 @@ class SeasonViewModel
itemId: UUID, itemId: UUID,
potential: BaseItem?, potential: BaseItem?,
): Job? = ): Job? =
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
super.init(itemId, potential)?.join() super.init(itemId, potential)?.join()
item.value?.let { item -> item.value?.let { item ->
val request = val request =

View file

@ -19,6 +19,7 @@ import com.github.damontecres.dolphin.ui.indexOfFirstOrNull
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ApiRequestPager import com.github.damontecres.dolphin.util.ApiRequestPager
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler import com.github.damontecres.dolphin.util.GetEpisodesRequestHandler
import com.github.damontecres.dolphin.util.ItemPager import com.github.damontecres.dolphin.util.ItemPager
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -52,7 +53,7 @@ class SeriesViewModel
itemId: UUID, itemId: UUID,
potential: BaseItem?, potential: BaseItem?,
): Job = ): Job =
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
super.init(itemId, potential)?.join() super.init(itemId, potential)?.join()
item.value?.let { item -> item.value?.let { item ->
val request = val request =
@ -90,7 +91,7 @@ class SeriesViewModel
season: Int?, season: Int?,
episode: Int?, episode: Int?,
) { ) {
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
init(itemId, potential).join() init(itemId, potential).join()
season?.let { seasonNum -> season?.let { seasonNum ->
val targetSeasonPosition = val targetSeasonPosition =
@ -103,7 +104,7 @@ class SeriesViewModel
} }
fun loadEpisodes(season: Int): Deferred<ApiRequestPager<*>> = fun loadEpisodes(season: Int): Deferred<ApiRequestPager<*>> =
viewModelScope.async { viewModelScope.async(ExceptionHandler()) {
val request = val request =
GetEpisodesRequest( GetEpisodesRequest(
seriesId = item.value!!.id, seriesId = item.value!!.id,
@ -130,7 +131,7 @@ class SeriesViewModel
itemId: UUID, itemId: UUID,
played: Boolean, played: Boolean,
listIndex: Int, listIndex: Int,
) = viewModelScope.launch { ) = viewModelScope.launch(ExceptionHandler()) {
if (played) { if (played) {
api.playStateApi.markPlayedItem(itemId) api.playStateApi.markPlayedItem(itemId)
} else { } else {
@ -142,7 +143,7 @@ class SeriesViewModel
fun refreshEpisode( fun refreshEpisode(
itemId: UUID, itemId: UUID,
listIndex: Int, listIndex: Int,
) = viewModelScope.launch { ) = viewModelScope.launch(ExceptionHandler()) {
val base = api.userLibraryApi.getItem(itemId).content val base = api.userLibraryApi.getItem(itemId).content
val item = BaseItem.from(base, api) val item = BaseItem.from(base, api)
episodes.value = episodes.value =

View file

@ -10,9 +10,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -22,6 +24,8 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
@ -37,11 +41,15 @@ import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.cards.BannerCard import com.github.damontecres.dolphin.ui.cards.BannerCard
import com.github.damontecres.dolphin.ui.cards.ItemRow import com.github.damontecres.dolphin.ui.cards.ItemRow
import com.github.damontecres.dolphin.ui.components.CircularProgress
import com.github.damontecres.dolphin.ui.components.DotSeparatedRow import com.github.damontecres.dolphin.ui.components.DotSeparatedRow
import com.github.damontecres.dolphin.ui.ifElse
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.roundMinutes import com.github.damontecres.dolphin.ui.roundMinutes
import com.github.damontecres.dolphin.ui.timeRemaining import com.github.damontecres.dolphin.ui.timeRemaining
import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.LoadingState
import com.github.damontecres.dolphin.util.formatDateTime import com.github.damontecres.dolphin.util.formatDateTime
import org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.api.BaseItemKind
import org.jellyfin.sdk.model.extensions.ticks import org.jellyfin.sdk.model.extensions.ticks
@ -59,8 +67,39 @@ fun MainPage(
modifier: Modifier, modifier: Modifier,
viewModel: MainViewModel = hiltViewModel(), viewModel: MainViewModel = hiltViewModel(),
) { ) {
val loading by viewModel.loadingState.observeAsState(LoadingState.Loading)
when (val state = loading) {
is LoadingState.Error -> Text(text = "Error: ${state.message}", modifier = modifier)
LoadingState.Loading ->
Box(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
CircularProgress(modifier = Modifier.size(250.dp))
}
LoadingState.Success -> {
val homeRows by viewModel.homeRows.observeAsState(listOf()) val homeRows by viewModel.homeRows.observeAsState(listOf())
var focusedItem by remember { mutableStateOf<BaseItem?>(null) } MainPageContent(navigationManager, homeRows, modifier)
}
}
}
@Composable
fun MainPageContent(
navigationManager: NavigationManager,
homeRows: List<HomeRow>,
modifier: Modifier = Modifier,
) {
var focusedItem by remember {
mutableStateOf<BaseItem?>(
homeRows.getOrNull(0)?.items?.getOrNull(
0,
),
)
}
val focusRequester = remember { FocusRequester() }
LaunchedEffect(Unit) { if (focusedItem != null) focusRequester.tryRequestFocus() }
Box(modifier = modifier) { Box(modifier = modifier) {
if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) { if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) {
val gradientColor = MaterialTheme.colorScheme.background val gradientColor = MaterialTheme.colorScheme.background
@ -136,7 +175,11 @@ fun MainPage(
playPercent = item?.data?.userData?.playedPercentage ?: 0.0, playPercent = item?.data?.userData?.playedPercentage ?: 0.0,
onClick = onClick, onClick = onClick,
onLongClick = onLongClick, onLongClick = onLongClick,
modifier = modifier, modifier =
modifier.ifElse(
focusedItem == item,
Modifier.focusRequester(focusRequester),
),
interactionSource = null, interactionSource = null,
cardHeight = 200.dp, cardHeight = 200.dp,
) )

View file

@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope
import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.data.model.BaseItem
import com.github.damontecres.dolphin.ui.DefaultItemFields import com.github.damontecres.dolphin.ui.DefaultItemFields
import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.isNotNullOrBlank
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.LoadingState
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -29,10 +31,11 @@ class MainViewModel
constructor( constructor(
val api: ApiClient, val api: ApiClient,
) : ViewModel() { ) : ViewModel() {
val loadingState = MutableLiveData<LoadingState>(LoadingState.Loading)
val homeRows = MutableLiveData<List<HomeRow>>() val homeRows = MutableLiveData<List<HomeRow>>()
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
val user = api.userApi.getCurrentUser().content val user = api.userApi.getCurrentUser().content
val displayPrefs = val displayPrefs =
api.displayPreferencesApi api.displayPreferencesApi
@ -145,6 +148,7 @@ class MainViewModel
.filter { it.items.isNotEmpty() } .filter { it.items.isNotEmpty() }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
this@MainViewModel.homeRows.value = homeRows this@MainViewModel.homeRows.value = homeRows
loadingState.value = LoadingState.Success
} }
} }
} }

View file

@ -5,6 +5,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.focusGroup import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@ -25,6 +26,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
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.focusRestorer
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
@ -46,11 +48,13 @@ import androidx.tv.material3.NavigationDrawerScope
import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.ProvideTextStyle
import androidx.tv.material3.Text import androidx.tv.material3.Text
import androidx.tv.material3.rememberDrawerState import androidx.tv.material3.rememberDrawerState
import androidx.tv.material3.surfaceColorAtElevation
import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.data.ServerRepository import com.github.damontecres.dolphin.data.ServerRepository
import com.github.damontecres.dolphin.data.model.Library import com.github.damontecres.dolphin.data.model.Library
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.FontAwesome import com.github.damontecres.dolphin.ui.FontAwesome
import com.github.damontecres.dolphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.jellyfin.sdk.api.client.ApiClient import org.jellyfin.sdk.api.client.ApiClient
@ -69,7 +73,7 @@ class NavDrawerViewModel
val libraries = MutableLiveData<List<Library>>(listOf()) val libraries = MutableLiveData<List<Library>>(listOf())
init { init {
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
val userViews = val userViews =
api.userViewsApi api.userViewsApi
.getUserViews() .getUserViews()
@ -105,42 +109,45 @@ fun NavDrawer(
NavigationDrawer( NavigationDrawer(
modifier = modifier =
modifier modifier
.focusRequester(drawerFocusRequester), .focusRequester(drawerFocusRequester)
.background(MaterialTheme.colorScheme.background),
drawerState = drawerState, drawerState = drawerState,
drawerContent = { drawerContent = {
ProvideTextStyle(MaterialTheme.typography.labelMedium) { ProvideTextStyle(MaterialTheme.typography.labelMedium) {
LazyColumn( Column(
state = listState, verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
modifier = modifier =
Modifier Modifier
.focusGroup()
// TODO fix focus restorer
.focusRestorer()
.fillMaxHeight() .fillMaxHeight()
.background(MaterialTheme.colorScheme.background) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)),
.focusGroup(),
) { ) {
item {
IconNavItem( IconNavItem(
text = user?.name ?: "", text = user?.name ?: "",
icon = Icons.Default.AccountCircle, icon = Icons.Default.AccountCircle,
onClick = { navigationManager.navigateTo(Destination.UserList) }, onClick = { navigationManager.navigateTo(Destination.UserList) },
) )
}
item {
IconNavItem( IconNavItem(
text = "Search", text = "Search",
icon = Icons.Default.Search, icon = Icons.Default.Search,
onClick = { navigationManager.navigateTo(Destination.Search) }, onClick = { navigationManager.navigateTo(Destination.Search) },
) )
}
item {
IconNavItem( IconNavItem(
text = "Home", text = "Home",
icon = Icons.Default.Home, icon = Icons.Default.Home,
onClick = { navigationManager.goToHome() }, onClick = { navigationManager.goToHome() },
) )
} LazyColumn(
state = listState,
contentPadding = PaddingValues(0.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier =
Modifier
.weight(1f),
) {
items(libraries) { items(libraries) {
LibraryNavItem( LibraryNavItem(
library = it, library = it,
@ -154,15 +161,15 @@ fun NavDrawer(
}, },
) )
} }
item { }
IconNavItem( IconNavItem(
text = "Settings", text = "Settings",
icon = Icons.Default.Settings, icon = Icons.Default.Settings,
onClick = { navigationManager.navigateTo(Destination.Settings) }, onClick = { navigationManager.navigateTo(Destination.Settings) },
modifier = Modifier,
) )
} }
} }
}
}, },
) { ) {
// Drawer content // Drawer content

View file

@ -59,6 +59,7 @@ import androidx.tv.material3.Text
import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.R
import com.github.damontecres.dolphin.ui.AppColors import com.github.damontecres.dolphin.ui.AppColors
import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -117,13 +118,13 @@ fun PlaybackControls(
val bringIntoViewRequester = remember { BringIntoViewRequester() } val bringIntoViewRequester = remember { BringIntoViewRequester() }
val onControllerInteraction = { val onControllerInteraction = {
scope.launch { scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView() bringIntoViewRequester.bringIntoView()
} }
controllerViewState.pulseControls() controllerViewState.pulseControls()
} }
val onControllerInteractionForDialog = { val onControllerInteractionForDialog = {
scope.launch { scope.launch(ExceptionHandler()) {
bringIntoViewRequester.bringIntoView() bringIntoViewRequester.bringIntoView()
} }
controllerViewState.pulseControls(Int.MAX_VALUE) controllerViewState.pulseControls(Int.MAX_VALUE)

View file

@ -10,6 +10,7 @@ import androidx.media3.common.MediaItem
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.preferences.UserPreferences
import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.Destination
import com.github.damontecres.dolphin.util.ExceptionHandler
import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener import com.github.damontecres.dolphin.util.TrackActivityPlaybackListener
import com.github.damontecres.dolphin.util.profile.PlaybackListener import com.github.damontecres.dolphin.util.profile.PlaybackListener
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
@ -95,7 +96,7 @@ class PlaybackViewModel
this.deviceProfile = deviceProfile this.deviceProfile = deviceProfile
val itemId = destination.itemId val itemId = destination.itemId
val item = destination.item val item = destination.item
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
val base = item?.data ?: api.userLibraryApi.getItem(itemId).content val base = item?.data ?: api.userLibraryApi.getItem(itemId).content
dto = base dto = base
@ -265,7 +266,7 @@ class PlaybackViewModel
fun changeAudioStream(index: Int) { fun changeAudioStream(index: Int) {
val itemId = currentPlayback.value?.itemId ?: return val itemId = currentPlayback.value?.itemId ?: return
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
changeStreams( changeStreams(
itemId, itemId,
index, index,
@ -277,7 +278,7 @@ class PlaybackViewModel
fun changeSubtitleStream(index: Int?) { fun changeSubtitleStream(index: Int?) {
val itemId = currentPlayback.value?.itemId ?: return val itemId = currentPlayback.value?.itemId ?: return
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
changeStreams( changeStreams(
itemId, itemId,
currentPlayback.value?.audioIndex, currentPlayback.value?.audioIndex,

View file

@ -43,6 +43,7 @@ import com.github.damontecres.dolphin.ui.components.DialogParams
import com.github.damontecres.dolphin.ui.components.DialogPopup import com.github.damontecres.dolphin.ui.components.DialogPopup
import com.github.damontecres.dolphin.ui.components.EditTextBox import com.github.damontecres.dolphin.ui.components.EditTextBox
import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
@ -64,14 +65,14 @@ fun <T> ComposablePreference(
val title = stringResource(preference.title) val title = stringResource(preference.title)
val onClick: () -> Unit = { val onClick: () -> Unit = {
scope.launch { scope.launch(ExceptionHandler()) {
when (preference) { when (preference) {
else -> {} else -> {}
} }
} }
} }
val onLongClick: () -> Unit = { val onLongClick: () -> Unit = {
scope.launch { scope.launch(ExceptionHandler()) {
when (preference) { when (preference) {
else -> null else -> null
} }

View file

@ -44,6 +44,7 @@ import com.github.damontecres.dolphin.ui.nav.NavigationManager
import com.github.damontecres.dolphin.ui.playOnClickSound import com.github.damontecres.dolphin.ui.playOnClickSound
import com.github.damontecres.dolphin.ui.playSoundOnFocus import com.github.damontecres.dolphin.ui.playSoundOnFocus
import com.github.damontecres.dolphin.ui.tryRequestFocus import com.github.damontecres.dolphin.ui.tryRequestFocus
import com.github.damontecres.dolphin.util.ExceptionHandler
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
val basicPreferences = val basicPreferences =
@ -295,7 +296,7 @@ fun PreferencesContent(
} }
PreferenceValidation.Valid -> { PreferenceValidation.Valid -> {
scope.launch { scope.launch(ExceptionHandler()) {
preferences = preferences =
viewModel.preferenceDataStore.updateData { prefs -> viewModel.preferenceDataStore.updateData { prefs ->
pref.setter(prefs, newValue) pref.setter(prefs, newValue)

View file

@ -7,6 +7,7 @@ import com.github.damontecres.dolphin.data.JellyfinServer
import com.github.damontecres.dolphin.data.JellyfinServerDao import com.github.damontecres.dolphin.data.JellyfinServerDao
import com.github.damontecres.dolphin.data.JellyfinUser import com.github.damontecres.dolphin.data.JellyfinUser
import com.github.damontecres.dolphin.data.ServerRepository import com.github.damontecres.dolphin.data.ServerRepository
import com.github.damontecres.dolphin.util.ExceptionHandler
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -42,7 +43,7 @@ class SwitchUserViewModel
private var quickConnectJob: Job? = null private var quickConnectJob: Job? = null
init { init {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
val allServers = val allServers =
serverDao serverDao
.getServers() .getServers()
@ -87,7 +88,7 @@ class SwitchUserViewModel
} }
} }
} }
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
serverRepository.currentServer?.let { serverRepository.currentServer?.let {
val quickConnect = val quickConnect =
jellyfin jellyfin
@ -112,7 +113,7 @@ class SwitchUserViewModel
user: JellyfinUser, user: JellyfinUser,
callback: () -> Unit, callback: () -> Unit,
) { ) {
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
serverRepository.changeUser(server, user) serverRepository.changeUser(server, user)
callback.invoke() callback.invoke()
} }
@ -125,7 +126,7 @@ class SwitchUserViewModel
onAuthenticated: () -> Unit, onAuthenticated: () -> Unit,
) { ) {
quickConnectJob?.cancel() quickConnectJob?.cancel()
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
try { try {
val api = jellyfin.createApi(baseUrl = server.url) val api = jellyfin.createApi(baseUrl = server.url)
val authenticationResult by api.userApi.authenticateUserByName( val authenticationResult by api.userApi.authenticateUserByName(
@ -149,7 +150,7 @@ class SwitchUserViewModel
onAuthenticated: () -> Unit, onAuthenticated: () -> Unit,
) { ) {
quickConnectJob?.cancel() quickConnectJob?.cancel()
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) {
try { try {
val api = jellyfin.createApi(server.url) val api = jellyfin.createApi(server.url)
var state = var state =
@ -162,7 +163,7 @@ class SwitchUserViewModel
} }
quickConnectJob = quickConnectJob =
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
while (!state.authenticated) { while (!state.authenticated) {
delay(5_000L) delay(5_000L)
state = state =
@ -203,7 +204,7 @@ class SwitchUserViewModel
serverUrl: String, serverUrl: String,
callback: () -> Unit, callback: () -> Unit,
) { ) {
viewModelScope.launch { viewModelScope.launch(ExceptionHandler()) {
try { try {
val serverInfo by jellyfin val serverInfo by jellyfin
.createApi(serverUrl) .createApi(serverUrl)

View file

@ -131,7 +131,7 @@ class ApiRequestPager<T>(
get() = totalCount get() = totalCount
private fun fetchPage(position: Int): Job = private fun fetchPage(position: Int): Job =
scope.launch(Dispatchers.IO) { scope.launch(ExceptionHandler() + Dispatchers.IO) {
mutex.withLock { mutex.withLock {
val pageNumber = position / pageSize val pageNumber = position / pageSize
if (cachedPages.getIfPresent(pageNumber) == null) { if (cachedPages.getIfPresent(pageNumber) == null) {

View file

@ -0,0 +1,42 @@
package com.github.damontecres.dolphin.util
import android.widget.Toast
import com.github.damontecres.dolphin.DolphinApplication
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import timber.log.Timber
import kotlin.coroutines.CoroutineContext
/**
* A general [CoroutineExceptionHandler] which can optionally show [Toast]s when an exception is thrown
*
* Note: a toast will be shown for each parameter given, up to three!
*
* @param autoToast automatically show a toast with the exception's message
*/
class ExceptionHandler(
private val autoToast: Boolean = false,
) : CoroutineExceptionHandler {
override val key: CoroutineContext.Key<*>
get() = CoroutineExceptionHandler
override fun handleException(
context: CoroutineContext,
exception: Throwable,
) {
if (exception is CancellationException) {
// Don't log/toast cancellations
return
}
Timber.e(exception, "Exception in coroutine")
if (autoToast) {
Toast
.makeText(
DolphinApplication.instance,
"Error: ${exception.message}",
Toast.LENGTH_LONG,
).show()
}
}
}

View file

@ -80,7 +80,7 @@ class ItemPager(
get() = totalCount get() = totalCount
private fun fetchPage(position: Int): Job = private fun fetchPage(position: Int): Job =
scope.launch(Dispatchers.IO) { scope.launch(ExceptionHandler() + Dispatchers.IO) {
mutex.withLock { mutex.withLock {
val pageNumber = position / pageSize val pageNumber = position / pageSize
if (cachedPages.getIfPresent(pageNumber) == null) { if (cachedPages.getIfPresent(pageNumber) == null) {

View file

@ -0,0 +1,12 @@
package com.github.damontecres.dolphin.util
sealed interface LoadingState {
object Loading : LoadingState
object Success : LoadingState
data class Error(
val message: String? = null,
val exception: Exception? = null,
) : LoadingState
}

View file

@ -74,7 +74,7 @@ class TrackActivityPlaybackListener(
fun release() { fun release() {
task.cancel() task.cancel()
TIMER.purge() TIMER.purge()
coroutineScope.launch { coroutineScope.launch(ExceptionHandler()) {
api.playStateApi.reportPlaybackStopped( api.playStateApi.reportPlaybackStopped(
PlaybackStopInfo( PlaybackStopInfo(
itemId = itemId, itemId = itemId,
@ -104,7 +104,7 @@ class TrackActivityPlaybackListener(
} }
private fun saveActivity(position: Long) { private fun saveActivity(position: Long) {
coroutineScope.launch { coroutineScope.launch(ExceptionHandler()) {
val totalDuration = totalPlayDurationMilliseconds.get() val totalDuration = totalPlayDurationMilliseconds.get()
val calcPosition = val calcPosition =
(if (position >= 0) position else player.currentPosition).milliseconds (if (position >= 0) position else player.currentPosition).milliseconds