mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-11-06 02:40:58 +01: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
|
|
@ -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