mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Fix crash when screensaver networks errors out (#1279)
## Description Fixes a crash if there is a network error when starting the screensaver. Instead, a black screen with an error message shows. The error message fades in/out to different parts of the screen. This applies to both in-app and OS screensavers. ### Related issues Fixes #1247 ### Testing Emulator w/ simulated errors ## Screenshots N/A ## AI or LLM usage None
This commit is contained in:
parent
61afe6ba86
commit
36b7390c4e
2 changed files with 186 additions and 81 deletions
|
|
@ -4,7 +4,7 @@ import android.content.Context
|
|||
import coil3.imageLoader
|
||||
import coil3.request.ImageRequest
|
||||
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
||||
import com.github.damontecres.wholphin.ui.components.CurrentItem
|
||||
import com.github.damontecres.wholphin.ui.components.ScreensaverItem
|
||||
import com.github.damontecres.wholphin.ui.formatDate
|
||||
import com.github.damontecres.wholphin.ui.launchDefault
|
||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||
|
|
@ -154,30 +154,20 @@ class ScreensaverService
|
|||
/**
|
||||
* Create a flow of items to show on the screensaver
|
||||
*/
|
||||
fun createItemFlow(scope: CoroutineScope): Flow<CurrentItem?> =
|
||||
fun createItemFlow(scope: CoroutineScope): Flow<ScreensaverItem?> =
|
||||
flow {
|
||||
val prefs =
|
||||
userPreferencesService.flow
|
||||
.first()
|
||||
.appPreferences
|
||||
.interfacePreferences.screensaverPreference
|
||||
val maxAge = prefs.maxAgeFilter.takeIf { it >= 0 }
|
||||
val itemTypes = prefs.itemTypesList.map { BaseItemKind.fromName(it) }
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
recursive = true,
|
||||
includeItemTypes = itemTypes,
|
||||
imageTypes = if (BaseItemKind.PHOTO in itemTypes) null else listOf(ImageType.BACKDROP),
|
||||
sortBy = listOf(ItemSortBy.RANDOM),
|
||||
maxOfficialRating = maxAge?.toString(),
|
||||
hasParentalRating = maxAge?.let { true },
|
||||
)
|
||||
val pager =
|
||||
ApiRequestPager(api, request, GetItemsRequestHandler, scope).init()
|
||||
try {
|
||||
createPager()
|
||||
} catch (ex: Exception) {
|
||||
Timber.e(ex, "Error creating pager for screensaver")
|
||||
emit(ScreensaverItem.Error(ex))
|
||||
return@flow
|
||||
}
|
||||
Timber.v("Got %s items", pager.size)
|
||||
var index = 0
|
||||
if (pager.isEmpty()) {
|
||||
emit(null)
|
||||
emit(ScreensaverItem.Empty)
|
||||
} else {
|
||||
val duration =
|
||||
userPreferencesService
|
||||
|
|
@ -213,7 +203,14 @@ class ScreensaverService
|
|||
.build(),
|
||||
).job
|
||||
.await()
|
||||
emit(CurrentItem(item, backdropUrl, logoUrl, title ?: ""))
|
||||
emit(
|
||||
ScreensaverItem.CurrentItem(
|
||||
item,
|
||||
backdropUrl,
|
||||
logoUrl,
|
||||
title ?: "",
|
||||
),
|
||||
)
|
||||
delay(duration)
|
||||
}
|
||||
}
|
||||
|
|
@ -228,6 +225,26 @@ class ScreensaverService
|
|||
}
|
||||
}
|
||||
}.flowOn(Dispatchers.Default).cancellable()
|
||||
|
||||
private suspend fun createPager(): ApiRequestPager<GetItemsRequest> {
|
||||
val prefs =
|
||||
userPreferencesService.flow
|
||||
.first()
|
||||
.appPreferences
|
||||
.interfacePreferences.screensaverPreference
|
||||
val maxAge = prefs.maxAgeFilter.takeIf { it >= 0 }
|
||||
val itemTypes = prefs.itemTypesList.map { BaseItemKind.fromName(it) }
|
||||
val request =
|
||||
GetItemsRequest(
|
||||
recursive = true,
|
||||
includeItemTypes = itemTypes,
|
||||
imageTypes = if (BaseItemKind.PHOTO in itemTypes) null else listOf(ImageType.BACKDROP),
|
||||
sortBy = listOf(ItemSortBy.RANDOM),
|
||||
maxOfficialRating = maxAge?.toString(),
|
||||
hasParentalRating = maxAge?.let { true },
|
||||
)
|
||||
return ApiRequestPager(api, request, GetItemsRequestHandler, scope).init()
|
||||
}
|
||||
}
|
||||
|
||||
data class ScreensaverState(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
|
|
@ -15,6 +19,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
|
|
@ -32,6 +37,7 @@ import androidx.compose.ui.graphics.Shader
|
|||
import androidx.compose.ui.graphics.ShaderBrush
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.core.DataStore
|
||||
|
|
@ -45,6 +51,7 @@ import coil3.compose.AsyncImage
|
|||
import coil3.compose.useExistingImageAsPlaceholder
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transitionFactory
|
||||
import com.github.damontecres.wholphin.R
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||
|
|
@ -53,6 +60,7 @@ import com.github.damontecres.wholphin.ui.CrossFadeFactory
|
|||
import com.github.damontecres.wholphin.ui.nav.TOP_SCRIM_ALPHA
|
||||
import com.github.damontecres.wholphin.ui.nav.TOP_SCRIM_END_FRACTION
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.delay
|
||||
import javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
|
@ -67,12 +75,20 @@ class ScreensaverViewModel
|
|||
val currentItem = screensaverService.createItemFlow(viewModelScope)
|
||||
}
|
||||
|
||||
data class CurrentItem(
|
||||
val item: BaseItem,
|
||||
val backdropUrl: String,
|
||||
val logoUrl: String?,
|
||||
val title: String,
|
||||
)
|
||||
sealed interface ScreensaverItem {
|
||||
data class Error(
|
||||
val exception: Exception,
|
||||
) : ScreensaverItem
|
||||
|
||||
data object Empty : ScreensaverItem
|
||||
|
||||
data class CurrentItem(
|
||||
val item: BaseItem,
|
||||
val backdropUrl: String,
|
||||
val logoUrl: String?,
|
||||
val title: String,
|
||||
) : ScreensaverItem
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AppScreensaver(
|
||||
|
|
@ -93,7 +109,7 @@ fun AppScreensaver(
|
|||
@OptIn(ExperimentalCoilApi::class)
|
||||
@Composable
|
||||
fun AppScreensaverContent(
|
||||
currentItem: CurrentItem?,
|
||||
currentItem: ScreensaverItem?,
|
||||
showClock: Boolean,
|
||||
duration: Duration,
|
||||
animate: Boolean,
|
||||
|
|
@ -116,61 +132,83 @@ fun AppScreensaverContent(
|
|||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
)
|
||||
AsyncImage(
|
||||
model =
|
||||
ImageRequest
|
||||
.Builder(LocalContext.current)
|
||||
.data(currentItem?.backdropUrl)
|
||||
.transitionFactory(CrossFadeFactory(2000.milliseconds))
|
||||
.useExistingImageAsPlaceholder(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
},
|
||||
)
|
||||
|
||||
var logoError by remember(currentItem) { mutableStateOf(false) }
|
||||
if (!logoError) {
|
||||
AsyncImage(
|
||||
model =
|
||||
ImageRequest
|
||||
.Builder(LocalContext.current)
|
||||
.data(currentItem?.logoUrl)
|
||||
.transitionFactory(CrossFadeFactory(750.milliseconds))
|
||||
.build(),
|
||||
contentDescription = "Logo",
|
||||
onError = {
|
||||
logoError = true
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.size(width = 240.dp, height = 120.dp)
|
||||
.padding(16.dp),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(.5f)
|
||||
.fillMaxHeight(.3f),
|
||||
) {
|
||||
Text(
|
||||
text = currentItem?.title ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.align(Alignment.BottomStart),
|
||||
when (currentItem) {
|
||||
ScreensaverItem.Empty -> {
|
||||
ScreensaverPlaceholder(
|
||||
text = stringResource(R.string.no_results),
|
||||
duration = duration,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
is ScreensaverItem.Error -> {
|
||||
ScreensaverPlaceholder(
|
||||
text = "Error connecting to Jellyfin server",
|
||||
duration = duration,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
|
||||
null,
|
||||
is ScreensaverItem.CurrentItem,
|
||||
-> {
|
||||
AsyncImage(
|
||||
model =
|
||||
ImageRequest
|
||||
.Builder(LocalContext.current)
|
||||
.data(currentItem?.backdropUrl)
|
||||
.transitionFactory(CrossFadeFactory(2000.milliseconds))
|
||||
.useExistingImageAsPlaceholder(true)
|
||||
.build(),
|
||||
contentDescription = null,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
},
|
||||
)
|
||||
|
||||
var logoError by remember(currentItem) { mutableStateOf(false) }
|
||||
if (!logoError) {
|
||||
AsyncImage(
|
||||
model =
|
||||
ImageRequest
|
||||
.Builder(LocalContext.current)
|
||||
.data(currentItem?.logoUrl)
|
||||
.transitionFactory(CrossFadeFactory(750.milliseconds))
|
||||
.build(),
|
||||
contentDescription = "Logo",
|
||||
onError = {
|
||||
logoError = true
|
||||
},
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.size(width = 240.dp, height = 120.dp)
|
||||
.padding(16.dp),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(Alignment.BottomStart)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(.5f)
|
||||
.fillMaxHeight(.3f),
|
||||
) {
|
||||
Text(
|
||||
text = currentItem?.title ?: "",
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.align(Alignment.BottomStart),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val largeRadialGradient =
|
||||
|
|
@ -212,3 +250,53 @@ fun AppScreensaverContent(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ScreensaverPlaceholder(
|
||||
text: String,
|
||||
duration: Duration,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var alignment by remember { mutableStateOf(Alignment.BottomStart) }
|
||||
val alignments =
|
||||
remember(alignment) {
|
||||
mutableListOf(
|
||||
Alignment.TopStart,
|
||||
Alignment.TopCenter,
|
||||
Alignment.TopEnd,
|
||||
Alignment.CenterStart,
|
||||
Alignment.Center,
|
||||
Alignment.CenterEnd,
|
||||
Alignment.BottomStart,
|
||||
Alignment.BottomCenter,
|
||||
Alignment.BottomEnd,
|
||||
).apply { remove(alignment) }
|
||||
}
|
||||
LaunchedEffect(Unit) {
|
||||
while (true) {
|
||||
delay(duration)
|
||||
alignment = alignments.random()
|
||||
}
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = alignment,
|
||||
label = "alignment animation",
|
||||
transitionSpec = {
|
||||
fadeIn(animationSpec = tween(500, delayMillis = 100))
|
||||
.togetherWith(fadeOut(animationSpec = tween(500, 100)))
|
||||
},
|
||||
modifier = modifier,
|
||||
) { align ->
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
modifier =
|
||||
Modifier
|
||||
.align(align)
|
||||
.padding(40.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue