From 9265a06fd76d4fcc9fa2d0280f8dddc32b4b3506 Mon Sep 17 00:00:00 2001 From: Damontecres Date: Thu, 2 Oct 2025 16:12:51 -0400 Subject: [PATCH] Use ExceptionHandler --- .../dolphin/ui/components/Dialogs.kt | 3 +- .../components/details/VideoDetailsHeader.kt | 5 +- .../damontecres/dolphin/ui/detail/CardGrid.kt | 9 +- .../ui/detail/CollectionFolderDetails.kt | 3 +- .../dolphin/ui/detail/ItemViewModel.kt | 3 +- .../dolphin/ui/detail/SeasonDetails.kt | 3 +- .../dolphin/ui/detail/SeriesDetails.kt | 11 +- .../damontecres/dolphin/ui/main/MainPage.kt | 49 +++++++- .../dolphin/ui/main/MainViewModel.kt | 6 +- .../damontecres/dolphin/ui/nav/NavDrawer.kt | 105 ++++++++++-------- .../dolphin/ui/playback/PlaybackControls.kt | 5 +- .../dolphin/ui/playback/PlaybackViewModel.kt | 7 +- .../ui/preferences/ComposablePreference.kt | 5 +- .../ui/preferences/PreferencesContent.kt | 3 +- .../dolphin/ui/setup/SwitchUserViewModel.kt | 15 +-- .../dolphin/util/ApiRequestPager.kt | 2 +- .../dolphin/util/ExceptionHandler.kt | 42 +++++++ .../damontecres/dolphin/util/ItemPager.kt | 2 +- .../damontecres/dolphin/util/LoadingState.kt | 12 ++ .../util/TrackActivityPlaybackListener.kt | 4 +- 20 files changed, 207 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/com/github/damontecres/dolphin/util/ExceptionHandler.kt create mode 100644 app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/Dialogs.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/Dialogs.kt index d728b46c..a1549e6e 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/Dialogs.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/Dialogs.kt @@ -46,6 +46,7 @@ import androidx.tv.material3.MaterialTheme import androidx.tv.material3.Text import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.dolphin.ui.FontAwesome +import com.github.damontecres.dolphin.util.ExceptionHandler import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -229,7 +230,7 @@ fun ScrollableDialog( val scope = rememberCoroutineScope() fun scroll(reverse: Boolean = false) { - scope.launch { + scope.launch(ExceptionHandler()) { columnState.scrollBy(if (reverse) -scrollAmount else scrollAmount) } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/components/details/VideoDetailsHeader.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/components/details/VideoDetailsHeader.kt index 453cb997..4795d231 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/components/details/VideoDetailsHeader.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/components/details/VideoDetailsHeader.kt @@ -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.playOnClickSound import com.github.damontecres.dolphin.ui.playSoundOnFocus +import com.github.damontecres.dolphin.util.ExceptionHandler import kotlinx.coroutines.launch import java.util.SortedMap import kotlin.time.Duration @@ -137,7 +138,7 @@ fun VideoDetailsHeader( .background(bgColor, shape = RoundedCornerShape(8.dp)) .onFocusChanged { if (it.isFocused) { - scope.launch { bringIntoViewRequester.bringIntoView() } + scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() } } }.playSoundOnFocus(true) .clickable( @@ -181,7 +182,7 @@ fun VideoDetailsHeader( }, moreOnClick = { moreOnClick.invoke() }, buttonOnFocusChanged = { - scope.launch { bringIntoViewRequester.bringIntoView() } + scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() } }, focusRequester = focusRequester, modifier = Modifier, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt index c211d32d..43772834 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CardGrid.kt @@ -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.isPlayKeyUp import com.github.damontecres.dolphin.ui.tryRequestFocus +import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ItemPager import kotlinx.coroutines.launch import timber.log.Timber @@ -169,7 +170,7 @@ fun CardGrid( } val jump = { jump: Int -> - scope.launch { + scope.launch(ExceptionHandler()) { val newPosition = (gridState.firstVisibleItemIndex + jump).coerceIn(0.. 0) { focusOn(newPosition) - scope.launch { + scope.launch(ExceptionHandler()) { gridState.scrollToItem(newPosition, -columns) firstFocus.tryRequestFocus() } @@ -366,7 +367,7 @@ fun CardGrid( AlphabetButtons( modifier = Modifier.align(Alignment.CenterVertically), letterClicked = { letter -> - scope.launch { + scope.launch(ExceptionHandler()) { val jumpPosition = letterPosition.invoke(letter) Timber.d("Alphabet jump to $jumpPosition") gridState.scrollToItem(jumpPosition) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt index 13a6f504..bfeedede 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/CollectionFolderDetails.kt @@ -16,6 +16,7 @@ import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.ui.OneTimeLaunchedEffect import com.github.damontecres.dolphin.ui.nav.Destination import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ItemPager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -43,7 +44,7 @@ class CollectionFolderViewModel itemId: UUID, potential: BaseItem?, ): Job = - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { super.init(itemId, potential)?.join() setup() } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/ItemViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/ItemViewModel.kt index 3c75edfb..6486de5b 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/ItemViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/ItemViewModel.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.viewModelScope import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.data.model.DolphinModel import com.github.damontecres.dolphin.data.model.convertModel +import com.github.damontecres.dolphin.util.ExceptionHandler import kotlinx.coroutines.Job import kotlinx.coroutines.launch import org.jellyfin.sdk.api.client.ApiClient @@ -32,7 +33,7 @@ abstract class ItemViewModel( if (item.value?.id == itemId) { return null } - return viewModelScope.launch { + return viewModelScope.launch(ExceptionHandler()) { try { val fetchedItem = api.userLibraryApi.getItem(itemId).content val modelInstance = convertModel(fetchedItem, api) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeasonDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeasonDetails.kt index ec8a9994..b65623c3 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeasonDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeasonDetails.kt @@ -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.nav.Destination import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.util.ExceptionHandler import com.github.damontecres.dolphin.util.ItemPager import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Job @@ -46,7 +47,7 @@ class SeasonViewModel itemId: UUID, potential: BaseItem?, ): Job? = - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { super.init(itemId, potential)?.join() item.value?.let { item -> val request = diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt index 93127257..509dd65d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/detail/SeriesDetails.kt @@ -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.NavigationManager 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.ItemPager import dagger.hilt.android.lifecycle.HiltViewModel @@ -52,7 +53,7 @@ class SeriesViewModel itemId: UUID, potential: BaseItem?, ): Job = - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { super.init(itemId, potential)?.join() item.value?.let { item -> val request = @@ -90,7 +91,7 @@ class SeriesViewModel season: Int?, episode: Int?, ) { - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { init(itemId, potential).join() season?.let { seasonNum -> val targetSeasonPosition = @@ -103,7 +104,7 @@ class SeriesViewModel } fun loadEpisodes(season: Int): Deferred> = - viewModelScope.async { + viewModelScope.async(ExceptionHandler()) { val request = GetEpisodesRequest( seriesId = item.value!!.id, @@ -130,7 +131,7 @@ class SeriesViewModel itemId: UUID, played: Boolean, listIndex: Int, - ) = viewModelScope.launch { + ) = viewModelScope.launch(ExceptionHandler()) { if (played) { api.playStateApi.markPlayedItem(itemId) } else { @@ -142,7 +143,7 @@ class SeriesViewModel fun refreshEpisode( itemId: UUID, listIndex: Int, - ) = viewModelScope.launch { + ) = viewModelScope.launch(ExceptionHandler()) { val base = api.userLibraryApi.getItem(itemId).content val item = BaseItem.from(base, api) episodes.value = diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt index cb26a12e..216e8a9d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainPage.kt @@ -10,9 +10,11 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -22,6 +24,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha 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.Color 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.ui.cards.BannerCard 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.ifElse import com.github.damontecres.dolphin.ui.isNotNullOrBlank import com.github.damontecres.dolphin.ui.nav.NavigationManager import com.github.damontecres.dolphin.ui.roundMinutes 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 org.jellyfin.sdk.model.api.BaseItemKind import org.jellyfin.sdk.model.extensions.ticks @@ -59,8 +67,39 @@ fun MainPage( modifier: Modifier, viewModel: MainViewModel = hiltViewModel(), ) { - val homeRows by viewModel.homeRows.observeAsState(listOf()) - var focusedItem by remember { mutableStateOf(null) } + 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()) + MainPageContent(navigationManager, homeRows, modifier) + } + } +} + +@Composable +fun MainPageContent( + navigationManager: NavigationManager, + homeRows: List, + modifier: Modifier = Modifier, +) { + var focusedItem by remember { + mutableStateOf( + homeRows.getOrNull(0)?.items?.getOrNull( + 0, + ), + ) + } + val focusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { if (focusedItem != null) focusRequester.tryRequestFocus() } Box(modifier = modifier) { if (focusedItem?.backdropImageUrl.isNotNullOrBlank()) { val gradientColor = MaterialTheme.colorScheme.background @@ -136,7 +175,11 @@ fun MainPage( playPercent = item?.data?.userData?.playedPercentage ?: 0.0, onClick = onClick, onLongClick = onLongClick, - modifier = modifier, + modifier = + modifier.ifElse( + focusedItem == item, + Modifier.focusRequester(focusRequester), + ), interactionSource = null, cardHeight = 200.dp, ) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainViewModel.kt index fcfa1c8b..c0b83b72 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/main/MainViewModel.kt @@ -6,6 +6,8 @@ import androidx.lifecycle.viewModelScope import com.github.damontecres.dolphin.data.model.BaseItem import com.github.damontecres.dolphin.ui.DefaultItemFields 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -29,10 +31,11 @@ class MainViewModel constructor( val api: ApiClient, ) : ViewModel() { + val loadingState = MutableLiveData(LoadingState.Loading) val homeRows = MutableLiveData>() init { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { val user = api.userApi.getCurrentUser().content val displayPrefs = api.displayPreferencesApi @@ -145,6 +148,7 @@ class MainViewModel .filter { it.items.isNotEmpty() } withContext(Dispatchers.Main) { this@MainViewModel.homeRows.value = homeRows + loadingState.value = LoadingState.Success } } } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt index dca44ea8..07f7a908 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/nav/NavDrawer.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.focusGroup import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize @@ -25,6 +26,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.platform.LocalContext import androidx.compose.ui.platform.LocalView @@ -46,11 +48,13 @@ import androidx.tv.material3.NavigationDrawerScope import androidx.tv.material3.ProvideTextStyle import androidx.tv.material3.Text import androidx.tv.material3.rememberDrawerState +import androidx.tv.material3.surfaceColorAtElevation import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.data.ServerRepository import com.github.damontecres.dolphin.data.model.Library import com.github.damontecres.dolphin.preferences.UserPreferences import com.github.damontecres.dolphin.ui.FontAwesome +import com.github.damontecres.dolphin.util.ExceptionHandler import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch import org.jellyfin.sdk.api.client.ApiClient @@ -69,7 +73,7 @@ class NavDrawerViewModel val libraries = MutableLiveData>(listOf()) init { - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { val userViews = api.userViewsApi .getUserViews() @@ -105,62 +109,65 @@ fun NavDrawer( NavigationDrawer( modifier = modifier - .focusRequester(drawerFocusRequester), + .focusRequester(drawerFocusRequester) + .background(MaterialTheme.colorScheme.background), drawerState = drawerState, drawerContent = { ProvideTextStyle(MaterialTheme.typography.labelMedium) { - LazyColumn( - state = listState, - contentPadding = PaddingValues(0.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.SpaceBetween, + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier + .focusGroup() + // TODO fix focus restorer + .focusRestorer() .fillMaxHeight() - .background(MaterialTheme.colorScheme.background) - .focusGroup(), + .background(MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)), ) { - item { - IconNavItem( - text = user?.name ?: "", - icon = Icons.Default.AccountCircle, - onClick = { navigationManager.navigateTo(Destination.UserList) }, - ) - } - item { - IconNavItem( - text = "Search", - icon = Icons.Default.Search, - onClick = { navigationManager.navigateTo(Destination.Search) }, - ) - } - item { - IconNavItem( - text = "Home", - icon = Icons.Default.Home, - onClick = { navigationManager.goToHome() }, - ) - } - items(libraries) { - LibraryNavItem( - library = it, - onClick = { - navigationManager.navigateTo( - Destination.MediaItem( - it.id, - it.type, - ), - ) - }, - ) - } - item { - IconNavItem( - text = "Settings", - icon = Icons.Default.Settings, - onClick = { navigationManager.navigateTo(Destination.Settings) }, - ) + IconNavItem( + text = user?.name ?: "", + icon = Icons.Default.AccountCircle, + onClick = { navigationManager.navigateTo(Destination.UserList) }, + ) + IconNavItem( + text = "Search", + icon = Icons.Default.Search, + onClick = { navigationManager.navigateTo(Destination.Search) }, + ) + IconNavItem( + text = "Home", + icon = Icons.Default.Home, + onClick = { navigationManager.goToHome() }, + ) + LazyColumn( + state = listState, + contentPadding = PaddingValues(0.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = + Modifier + .weight(1f), + ) { + items(libraries) { + LibraryNavItem( + library = it, + onClick = { + navigationManager.navigateTo( + Destination.MediaItem( + it.id, + it.type, + ), + ) + }, + ) + } } + IconNavItem( + text = "Settings", + icon = Icons.Default.Settings, + onClick = { navigationManager.navigateTo(Destination.Settings) }, + modifier = Modifier, + ) } } }, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt index ebc56562..8e4193a8 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackControls.kt @@ -59,6 +59,7 @@ import androidx.tv.material3.Text import com.github.damontecres.dolphin.R import com.github.damontecres.dolphin.ui.AppColors import com.github.damontecres.dolphin.ui.tryRequestFocus +import com.github.damontecres.dolphin.util.ExceptionHandler import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -117,13 +118,13 @@ fun PlaybackControls( val bringIntoViewRequester = remember { BringIntoViewRequester() } val onControllerInteraction = { - scope.launch { + scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() } controllerViewState.pulseControls() } val onControllerInteractionForDialog = { - scope.launch { + scope.launch(ExceptionHandler()) { bringIntoViewRequester.bringIntoView() } controllerViewState.pulseControls(Int.MAX_VALUE) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt index 2f8b5ef9..5ad02a3f 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/playback/PlaybackViewModel.kt @@ -10,6 +10,7 @@ import androidx.media3.common.MediaItem import androidx.media3.exoplayer.ExoPlayer import com.github.damontecres.dolphin.preferences.UserPreferences 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.profile.PlaybackListener import dagger.hilt.android.lifecycle.HiltViewModel @@ -95,7 +96,7 @@ class PlaybackViewModel this.deviceProfile = deviceProfile val itemId = destination.itemId val item = destination.item - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { val base = item?.data ?: api.userLibraryApi.getItem(itemId).content dto = base @@ -265,7 +266,7 @@ class PlaybackViewModel fun changeAudioStream(index: Int) { val itemId = currentPlayback.value?.itemId ?: return - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { changeStreams( itemId, index, @@ -277,7 +278,7 @@ class PlaybackViewModel fun changeSubtitleStream(index: Int?) { val itemId = currentPlayback.value?.itemId ?: return - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { changeStreams( itemId, currentPlayback.value?.audioIndex, diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/ComposablePreference.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/ComposablePreference.kt index 4a4a81a5..d8efbed9 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/ComposablePreference.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/ComposablePreference.kt @@ -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.EditTextBox import com.github.damontecres.dolphin.ui.nav.NavigationManager +import com.github.damontecres.dolphin.util.ExceptionHandler import kotlinx.coroutines.launch @Suppress("UNCHECKED_CAST") @@ -64,14 +65,14 @@ fun ComposablePreference( val title = stringResource(preference.title) val onClick: () -> Unit = { - scope.launch { + scope.launch(ExceptionHandler()) { when (preference) { else -> {} } } } val onLongClick: () -> Unit = { - scope.launch { + scope.launch(ExceptionHandler()) { when (preference) { else -> null } diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/PreferencesContent.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/PreferencesContent.kt index 5fc72517..065578e0 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/PreferencesContent.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/preferences/PreferencesContent.kt @@ -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.playSoundOnFocus import com.github.damontecres.dolphin.ui.tryRequestFocus +import com.github.damontecres.dolphin.util.ExceptionHandler import kotlinx.coroutines.launch val basicPreferences = @@ -295,7 +296,7 @@ fun PreferencesContent( } PreferenceValidation.Valid -> { - scope.launch { + scope.launch(ExceptionHandler()) { preferences = viewModel.preferenceDataStore.updateData { prefs -> pref.setter(prefs, newValue) diff --git a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt index ea2dffe9..8556a8eb 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/ui/setup/SwitchUserViewModel.kt @@ -7,6 +7,7 @@ import com.github.damontecres.dolphin.data.JellyfinServer import com.github.damontecres.dolphin.data.JellyfinServerDao import com.github.damontecres.dolphin.data.JellyfinUser import com.github.damontecres.dolphin.data.ServerRepository +import com.github.damontecres.dolphin.util.ExceptionHandler import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -42,7 +43,7 @@ class SwitchUserViewModel private var quickConnectJob: Job? = null init { - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { val allServers = serverDao .getServers() @@ -87,7 +88,7 @@ class SwitchUserViewModel } } } - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { serverRepository.currentServer?.let { val quickConnect = jellyfin @@ -112,7 +113,7 @@ class SwitchUserViewModel user: JellyfinUser, callback: () -> Unit, ) { - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { serverRepository.changeUser(server, user) callback.invoke() } @@ -125,7 +126,7 @@ class SwitchUserViewModel onAuthenticated: () -> Unit, ) { quickConnectJob?.cancel() - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { try { val api = jellyfin.createApi(baseUrl = server.url) val authenticationResult by api.userApi.authenticateUserByName( @@ -149,7 +150,7 @@ class SwitchUserViewModel onAuthenticated: () -> Unit, ) { quickConnectJob?.cancel() - viewModelScope.launch(Dispatchers.IO) { + viewModelScope.launch(ExceptionHandler() + Dispatchers.IO) { try { val api = jellyfin.createApi(server.url) var state = @@ -162,7 +163,7 @@ class SwitchUserViewModel } quickConnectJob = - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { while (!state.authenticated) { delay(5_000L) state = @@ -203,7 +204,7 @@ class SwitchUserViewModel serverUrl: String, callback: () -> Unit, ) { - viewModelScope.launch { + viewModelScope.launch(ExceptionHandler()) { try { val serverInfo by jellyfin .createApi(serverUrl) diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt b/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt index e2060602..462956df 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/util/ApiRequestPager.kt @@ -131,7 +131,7 @@ class ApiRequestPager( get() = totalCount private fun fetchPage(position: Int): Job = - scope.launch(Dispatchers.IO) { + scope.launch(ExceptionHandler() + Dispatchers.IO) { mutex.withLock { val pageNumber = position / pageSize if (cachedPages.getIfPresent(pageNumber) == null) { diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/ExceptionHandler.kt b/app/src/main/java/com/github/damontecres/dolphin/util/ExceptionHandler.kt new file mode 100644 index 00000000..6a9b4f50 --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/util/ExceptionHandler.kt @@ -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() + } + } +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/ItemPager.kt b/app/src/main/java/com/github/damontecres/dolphin/util/ItemPager.kt index 991e226c..d999155d 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/util/ItemPager.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/util/ItemPager.kt @@ -80,7 +80,7 @@ class ItemPager( get() = totalCount private fun fetchPage(position: Int): Job = - scope.launch(Dispatchers.IO) { + scope.launch(ExceptionHandler() + Dispatchers.IO) { mutex.withLock { val pageNumber = position / pageSize if (cachedPages.getIfPresent(pageNumber) == null) { diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt b/app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt new file mode 100644 index 00000000..c69e482b --- /dev/null +++ b/app/src/main/java/com/github/damontecres/dolphin/util/LoadingState.kt @@ -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 +} diff --git a/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt b/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt index a032f2ad..fe192222 100644 --- a/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt +++ b/app/src/main/java/com/github/damontecres/dolphin/util/TrackActivityPlaybackListener.kt @@ -74,7 +74,7 @@ class TrackActivityPlaybackListener( fun release() { task.cancel() TIMER.purge() - coroutineScope.launch { + coroutineScope.launch(ExceptionHandler()) { api.playStateApi.reportPlaybackStopped( PlaybackStopInfo( itemId = itemId, @@ -104,7 +104,7 @@ class TrackActivityPlaybackListener( } private fun saveActivity(position: Long) { - coroutineScope.launch { + coroutineScope.launch(ExceptionHandler()) { val totalDuration = totalPlayDurationMilliseconds.get() val calcPosition = (if (position >= 0) position else player.currentPosition).milliseconds