mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 23:51:21 +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.imageLoader
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
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.formatDate
|
||||||
import com.github.damontecres.wholphin.ui.launchDefault
|
import com.github.damontecres.wholphin.ui.launchDefault
|
||||||
import com.github.damontecres.wholphin.util.ApiRequestPager
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
|
|
@ -154,30 +154,20 @@ class ScreensaverService
|
||||||
/**
|
/**
|
||||||
* Create a flow of items to show on the screensaver
|
* Create a flow of items to show on the screensaver
|
||||||
*/
|
*/
|
||||||
fun createItemFlow(scope: CoroutineScope): Flow<CurrentItem?> =
|
fun createItemFlow(scope: CoroutineScope): Flow<ScreensaverItem?> =
|
||||||
flow {
|
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 =
|
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)
|
Timber.v("Got %s items", pager.size)
|
||||||
var index = 0
|
var index = 0
|
||||||
if (pager.isEmpty()) {
|
if (pager.isEmpty()) {
|
||||||
emit(null)
|
emit(ScreensaverItem.Empty)
|
||||||
} else {
|
} else {
|
||||||
val duration =
|
val duration =
|
||||||
userPreferencesService
|
userPreferencesService
|
||||||
|
|
@ -213,7 +203,14 @@ class ScreensaverService
|
||||||
.build(),
|
.build(),
|
||||||
).job
|
).job
|
||||||
.await()
|
.await()
|
||||||
emit(CurrentItem(item, backdropUrl, logoUrl, title ?: ""))
|
emit(
|
||||||
|
ScreensaverItem.CurrentItem(
|
||||||
|
item,
|
||||||
|
backdropUrl,
|
||||||
|
logoUrl,
|
||||||
|
title ?: "",
|
||||||
|
),
|
||||||
|
)
|
||||||
delay(duration)
|
delay(duration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -228,6 +225,26 @@ class ScreensaverService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.flowOn(Dispatchers.Default).cancellable()
|
}.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(
|
data class ScreensaverState(
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,15 @@
|
||||||
package com.github.damontecres.wholphin.ui.components
|
package com.github.damontecres.wholphin.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.core.LinearEasing
|
import androidx.compose.animation.core.LinearEasing
|
||||||
import androidx.compose.animation.core.RepeatMode
|
import androidx.compose.animation.core.RepeatMode
|
||||||
import androidx.compose.animation.core.animateFloat
|
import androidx.compose.animation.core.animateFloat
|
||||||
import androidx.compose.animation.core.infiniteRepeatable
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
import androidx.compose.animation.core.tween
|
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.Canvas
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
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.ShaderBrush
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
|
|
@ -45,6 +51,7 @@ import coil3.compose.AsyncImage
|
||||||
import coil3.compose.useExistingImageAsPlaceholder
|
import coil3.compose.useExistingImageAsPlaceholder
|
||||||
import coil3.request.ImageRequest
|
import coil3.request.ImageRequest
|
||||||
import coil3.request.transitionFactory
|
import coil3.request.transitionFactory
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
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_ALPHA
|
||||||
import com.github.damontecres.wholphin.ui.nav.TOP_SCRIM_END_FRACTION
|
import com.github.damontecres.wholphin.ui.nav.TOP_SCRIM_END_FRACTION
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
@ -67,12 +75,20 @@ class ScreensaverViewModel
|
||||||
val currentItem = screensaverService.createItemFlow(viewModelScope)
|
val currentItem = screensaverService.createItemFlow(viewModelScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class CurrentItem(
|
sealed interface ScreensaverItem {
|
||||||
val item: BaseItem,
|
data class Error(
|
||||||
val backdropUrl: String,
|
val exception: Exception,
|
||||||
val logoUrl: String?,
|
) : ScreensaverItem
|
||||||
val title: String,
|
|
||||||
)
|
data object Empty : ScreensaverItem
|
||||||
|
|
||||||
|
data class CurrentItem(
|
||||||
|
val item: BaseItem,
|
||||||
|
val backdropUrl: String,
|
||||||
|
val logoUrl: String?,
|
||||||
|
val title: String,
|
||||||
|
) : ScreensaverItem
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScreensaver(
|
fun AppScreensaver(
|
||||||
|
|
@ -93,7 +109,7 @@ fun AppScreensaver(
|
||||||
@OptIn(ExperimentalCoilApi::class)
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun AppScreensaverContent(
|
fun AppScreensaverContent(
|
||||||
currentItem: CurrentItem?,
|
currentItem: ScreensaverItem?,
|
||||||
showClock: Boolean,
|
showClock: Boolean,
|
||||||
duration: Duration,
|
duration: Duration,
|
||||||
animate: Boolean,
|
animate: Boolean,
|
||||||
|
|
@ -116,61 +132,83 @@ fun AppScreensaverContent(
|
||||||
repeatMode = RepeatMode.Reverse,
|
repeatMode = RepeatMode.Reverse,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
AsyncImage(
|
when (currentItem) {
|
||||||
model =
|
ScreensaverItem.Empty -> {
|
||||||
ImageRequest
|
ScreensaverPlaceholder(
|
||||||
.Builder(LocalContext.current)
|
text = stringResource(R.string.no_results),
|
||||||
.data(currentItem?.backdropUrl)
|
duration = duration,
|
||||||
.transitionFactory(CrossFadeFactory(2000.milliseconds))
|
modifier = Modifier.fillMaxSize(),
|
||||||
.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),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 =
|
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