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.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)
}
}

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.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,

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.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..<pager.size)
if (DEBUG) Timber.d("newPosition=$newPosition")
@ -179,7 +180,7 @@ fun CardGrid(
}
}
val jumpToTop = {
scope.launch {
scope.launch(ExceptionHandler()) {
if (focusedIndex < (columns * 6)) {
// If close, animate the scroll
gridState.animateScrollToItem(0, 0)
@ -205,7 +206,7 @@ fun CardGrid(
if (DEBUG) Timber.d("Back long pressed: newPosition=$newPosition")
if (newPosition > 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)

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.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()
}

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.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<T : DolphinModel>(
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)

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.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 =

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.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<ApiRequestPager<*>> =
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 =

View file

@ -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<BaseItem?>(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<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) {
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,
)

View file

@ -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>(LoadingState.Loading)
val homeRows = MutableLiveData<List<HomeRow>>()
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
}
}
}

View file

@ -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<List<Library>>(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,
)
}
}
},

View file

@ -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)

View file

@ -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,

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.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 <T> 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
}

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.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)

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.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)

View file

@ -131,7 +131,7 @@ class ApiRequestPager<T>(
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) {

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
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) {

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() {
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