Add in-app & Android TV screensaver (#946)
## Description Adds both an optional in-app and Android TV OS (DreamService) screensaver Has settings for: - Starting delay, options from 30s to 1 hour - Duration to show each image, 30s to 2 minutes - Show clock (separate from app-wide clock) - Toggle animated zooming - Set a max age rating to limit what content is shown - Included media types: movies, tv shows, and/or photos By default only Movies & TV Shows are included. But if you have like family albums on your server, you could enable Photos too. There's also an button to start the screensaver so you can see what it looks like. ### Related issues Closes #230 ### Testing Emulator, Onn 4k pro, nvidia shield using both in-app and OS screensavers ## Screenshots  ## AI or LLM usage None ## Acknowledgements Some of the skeleton code for setting up the `DreamService` with the right lifecycle was copied and modified from the [official app's implementation](https://github.com/jellyfin/jellyfin-androidtv/blob/master/app/src/main/java/org/jellyfin/androidtv/integration/dream/DreamServiceCompat.kt).
This commit is contained in:
parent
536e8d366c
commit
a7e86fbc15
25 changed files with 954 additions and 78 deletions
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
|||
import android.content.ContextWrapper
|
||||
import android.media.AudioManager
|
||||
import android.view.KeyEvent
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.MarqueeAnimationMode
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
|
|
@ -296,7 +295,7 @@ fun Arrangement.spacedByWithFooter(space: Dp) =
|
|||
}
|
||||
|
||||
/**
|
||||
* Tries to find the [Activity] for the given [Context]. Often used for [keepScreenOn].
|
||||
* Tries to find the [Activity] for the given [Context]
|
||||
*/
|
||||
fun Context.findActivity(): Activity? {
|
||||
if (this is Activity) {
|
||||
|
|
@ -310,18 +309,6 @@ fun Context.findActivity(): Activity? {
|
|||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep the screen on for an [Activity]. Often used with [findActivity].
|
||||
*/
|
||||
fun Activity.keepScreenOn(keep: Boolean) {
|
||||
Timber.v("Keep screen on: $keep")
|
||||
if (keep) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Selectively log errors from Coil image loading.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -0,0 +1,214 @@
|
|||
package com.github.damontecres.wholphin.ui.components
|
||||
|
||||
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.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
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.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.geometry.center
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RadialGradientShader
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil3.annotation.ExperimentalCoilApi
|
||||
import coil3.compose.AsyncImage
|
||||
import coil3.compose.useExistingImageAsPlaceholder
|
||||
import coil3.request.ImageRequest
|
||||
import coil3.request.transitionFactory
|
||||
import com.github.damontecres.wholphin.data.model.BaseItem
|
||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
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 javax.inject.Inject
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@HiltViewModel
|
||||
class ScreensaverViewModel
|
||||
@Inject
|
||||
constructor(
|
||||
private val screensaverService: ScreensaverService,
|
||||
val preferencesDataStore: DataStore<AppPreferences>,
|
||||
) : ViewModel() {
|
||||
val currentItem = screensaverService.createItemFlow(viewModelScope)
|
||||
}
|
||||
|
||||
data class CurrentItem(
|
||||
val item: BaseItem,
|
||||
val backdropUrl: String,
|
||||
val logoUrl: String?,
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun AppScreensaver(
|
||||
prefs: AppPreferences,
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: ScreensaverViewModel = hiltViewModel(),
|
||||
) {
|
||||
val currentItem by viewModel.currentItem.collectAsState(null)
|
||||
AppScreensaverContent(
|
||||
currentItem = currentItem,
|
||||
showClock = prefs.interfacePreferences.screensaverPreference.showClock,
|
||||
duration = prefs.interfacePreferences.screensaverPreference.duration.milliseconds,
|
||||
animate = prefs.interfacePreferences.screensaverPreference.animate,
|
||||
modifier = modifier,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoilApi::class)
|
||||
@Composable
|
||||
fun AppScreensaverContent(
|
||||
currentItem: CurrentItem?,
|
||||
showClock: Boolean,
|
||||
duration: Duration,
|
||||
animate: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(
|
||||
modifier
|
||||
.background(Color.Black),
|
||||
) {
|
||||
val infiniteTransition = rememberInfiniteTransition()
|
||||
val scale by infiniteTransition.animateFloat(
|
||||
1f,
|
||||
if (animate) 1.1f else 1f,
|
||||
infiniteRepeatable(
|
||||
tween(
|
||||
durationMillis = duration.inWholeMilliseconds.toInt(),
|
||||
delayMillis = 500,
|
||||
easing = LinearEasing,
|
||||
),
|
||||
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) }
|
||||
val alignment = listOf(Alignment.BottomStart, Alignment.BottomEnd).random()
|
||||
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)
|
||||
.size(width = 240.dp, height = 120.dp)
|
||||
.padding(16.dp),
|
||||
)
|
||||
} else {
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.align(alignment)
|
||||
.padding(16.dp)
|
||||
.fillMaxWidth(.5f)
|
||||
.fillMaxHeight(.3f),
|
||||
) {
|
||||
Text(
|
||||
text = currentItem?.title ?: "",
|
||||
style = MaterialTheme.typography.displaySmall,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.align(alignment),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val largeRadialGradient =
|
||||
remember {
|
||||
object : ShaderBrush() {
|
||||
override fun createShader(size: Size): Shader {
|
||||
val biggerDimension = maxOf(size.height, size.width)
|
||||
return RadialGradientShader(
|
||||
colors = listOf(Color.Transparent, AppColors.TransparentBlack25),
|
||||
center = size.center,
|
||||
radius = biggerDimension / 1.5f,
|
||||
colorStops = listOf(0f, 0.85f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Canvas(Modifier.fillMaxSize()) {
|
||||
drawRect(
|
||||
brush = largeRadialGradient,
|
||||
blendMode = BlendMode.Multiply,
|
||||
)
|
||||
if (showClock) {
|
||||
// Add scrim to make clock more readable
|
||||
drawRect(
|
||||
brush =
|
||||
Brush.verticalGradient(
|
||||
colorStops =
|
||||
arrayOf(
|
||||
0f to Color.Black.copy(alpha = TOP_SCRIM_ALPHA),
|
||||
TOP_SCRIM_END_FRACTION to Color.Transparent,
|
||||
),
|
||||
),
|
||||
blendMode = BlendMode.Multiply,
|
||||
)
|
||||
}
|
||||
}
|
||||
if (showClock) {
|
||||
TimeDisplay()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -55,8 +55,8 @@ import javax.inject.Inject
|
|||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
// Top scrim configuration for text readability (clock, season tabs)
|
||||
private const val TOP_SCRIM_ALPHA = 0.55f
|
||||
private const val TOP_SCRIM_END_FRACTION = 0.25f // Fraction of backdrop image height
|
||||
const val TOP_SCRIM_ALPHA = 0.55f
|
||||
const val TOP_SCRIM_END_FRACTION = 0.25f // Fraction of backdrop image height
|
||||
|
||||
@HiltViewModel
|
||||
class ApplicationContentViewModel
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
package com.github.damontecres.wholphin.ui.playback
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Player.Listener
|
||||
import com.github.damontecres.wholphin.ui.findActivity
|
||||
import com.github.damontecres.wholphin.ui.keepScreenOn
|
||||
|
||||
/**
|
||||
* Starts a [Player.Listener] that ensures the screen stays on without a screen saber during playback
|
||||
*
|
||||
* This will clean up the listener when disposed
|
||||
*/
|
||||
@Composable
|
||||
fun AmbientPlayerListener(player: Player) {
|
||||
val context = LocalContext.current
|
||||
DisposableEffect(player) {
|
||||
val listener =
|
||||
object : Listener {
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
context.findActivity()?.keepScreenOn(isPlaying)
|
||||
}
|
||||
}
|
||||
player.addListener(listener)
|
||||
onDispose {
|
||||
player.removeListener(listener)
|
||||
context.findActivity()?.keepScreenOn(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -186,7 +186,6 @@ fun PlaybackPageContent(
|
|||
}
|
||||
}
|
||||
|
||||
AmbientPlayerListener(player)
|
||||
var contentScale by remember(playerBackend) {
|
||||
mutableStateOf(
|
||||
if (playerBackend == PlayerBackend.MPV) {
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import com.github.damontecres.wholphin.services.PlayerFactory
|
|||
import com.github.damontecres.wholphin.services.PlaylistCreationResult
|
||||
import com.github.damontecres.wholphin.services.PlaylistCreator
|
||||
import com.github.damontecres.wholphin.services.RefreshRateService
|
||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||
import com.github.damontecres.wholphin.services.StreamChoiceService
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||
|
|
@ -140,6 +141,7 @@ class PlaybackViewModel
|
|||
val streamChoiceService: StreamChoiceService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val imageUrlService: ImageUrlService,
|
||||
private val screensaverService: ScreensaverService,
|
||||
@Assisted private val destination: Destination,
|
||||
) : ViewModel(),
|
||||
Player.Listener,
|
||||
|
|
@ -189,7 +191,10 @@ class PlaybackViewModel
|
|||
|
||||
init {
|
||||
viewModelScope.launchIO {
|
||||
addCloseable { disconnectPlayer() }
|
||||
addCloseable {
|
||||
screensaverService.keepScreenOn(false)
|
||||
disconnectPlayer()
|
||||
}
|
||||
init()
|
||||
}
|
||||
}
|
||||
|
|
@ -1424,6 +1429,10 @@ class PlaybackViewModel
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
screensaverService.keepScreenOn(isPlaying)
|
||||
}
|
||||
}
|
||||
|
||||
data class PlayerState(
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
|||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.SortedSet
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Composable
|
||||
|
|
@ -226,7 +225,7 @@ fun <T> ComposablePreference(
|
|||
}
|
||||
|
||||
is AppMultiChoicePreference<*, *> -> {
|
||||
val values = stringArrayResource(preference.displayValues).toSortedSet()
|
||||
val values = stringArrayResource(preference.displayValues)
|
||||
val summary =
|
||||
preference.summary?.let { stringResource(it) }
|
||||
?: preference.summary(context, value)
|
||||
|
|
@ -237,12 +236,15 @@ fun <T> ComposablePreference(
|
|||
list
|
||||
}
|
||||
MultiChoicePreference(
|
||||
possibleValues = values as SortedSet<Any>,
|
||||
possibleValues = preference.allValues,
|
||||
selectedValues = selectedValues,
|
||||
title = title,
|
||||
summary = summary,
|
||||
onValueChange = {
|
||||
onValueChange.invoke(selectedValues.toList() as T)
|
||||
onValueChange.invoke(it.toList() as T)
|
||||
},
|
||||
valueDisplay = { index, _ ->
|
||||
Text(values[index])
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,12 +19,12 @@ import com.github.damontecres.wholphin.ui.components.DialogPopup
|
|||
fun <T> MultiChoicePreference(
|
||||
title: String,
|
||||
summary: String?,
|
||||
possibleValues: Set<T>,
|
||||
possibleValues: List<T>,
|
||||
selectedValues: Set<T>,
|
||||
onValueChange: (List<T>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
valueDisplay: @Composable (item: T) -> Unit = { Text(it.toString()) },
|
||||
valueDisplay: @Composable (index: Int, item: T) -> Unit = { _, item -> Text(item.toString()) },
|
||||
) {
|
||||
// val values = stringArrayResource(preference.displayValues).toList()
|
||||
// val summary =
|
||||
|
|
@ -59,7 +59,7 @@ fun <T> MultiChoicePreference(
|
|||
items =
|
||||
possibleValues.mapIndexed { index, item ->
|
||||
DialogItem(
|
||||
headlineContent = { valueDisplay.invoke(item) },
|
||||
headlineContent = { valueDisplay.invoke(index, item) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = selectedValues.contains(item),
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ enum class PreferenceScreenOption {
|
|||
ADVANCED,
|
||||
EXO_PLAYER,
|
||||
MPV,
|
||||
SCREENSAVER,
|
||||
;
|
||||
|
||||
companion object {
|
||||
|
|
|
|||
|
|
@ -50,8 +50,10 @@ import com.github.damontecres.wholphin.preferences.AppPreferences
|
|||
import com.github.damontecres.wholphin.preferences.ExoPlayerPreferences
|
||||
import com.github.damontecres.wholphin.preferences.MpvPreferences
|
||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
||||
import com.github.damontecres.wholphin.preferences.ScreensaverPreference
|
||||
import com.github.damontecres.wholphin.preferences.advancedPreferences
|
||||
import com.github.damontecres.wholphin.preferences.basicPreferences
|
||||
import com.github.damontecres.wholphin.preferences.screensaverPreferences
|
||||
import com.github.damontecres.wholphin.preferences.updatePlaybackPreferences
|
||||
import com.github.damontecres.wholphin.services.SeerrConnectionStatus
|
||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||
|
|
@ -128,6 +130,7 @@ fun PreferencesContent(
|
|||
PreferenceScreenOption.ADVANCED -> advancedPreferences
|
||||
PreferenceScreenOption.EXO_PLAYER -> ExoPlayerPreferences
|
||||
PreferenceScreenOption.MPV -> MpvPreferences
|
||||
PreferenceScreenOption.SCREENSAVER -> screensaverPreferences
|
||||
}
|
||||
val screenTitle =
|
||||
when (preferenceScreenOption) {
|
||||
|
|
@ -135,6 +138,7 @@ fun PreferencesContent(
|
|||
PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
|
||||
PreferenceScreenOption.EXO_PLAYER -> R.string.exoplayer_options
|
||||
PreferenceScreenOption.MPV -> R.string.mpv_options
|
||||
PreferenceScreenOption.SCREENSAVER -> R.string.screensaver_settings
|
||||
}
|
||||
|
||||
var visible by remember { mutableStateOf(false) }
|
||||
|
|
@ -283,7 +287,9 @@ fun PreferencesContent(
|
|||
if (movementSounds) playOnClickSound(context)
|
||||
if (release != null && updateAvailable) {
|
||||
release?.let {
|
||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
||||
viewModel.navigationManager.navigateTo(
|
||||
Destination.UpdateApp,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
updateVM.init(preferences.updateUrl)
|
||||
|
|
@ -313,7 +319,8 @@ fun PreferencesContent(
|
|||
val summary =
|
||||
remember(cacheUsage) {
|
||||
cacheUsage.let {
|
||||
val diskMB = it.imageDiskUsed / AppPreference.MEGA_BIT
|
||||
val diskMB =
|
||||
it.imageDiskUsed / AppPreference.MEGA_BIT
|
||||
val memoryUsedMB =
|
||||
it.imageMemoryUsed / AppPreference.MEGA_BIT
|
||||
val memoryMaxMB =
|
||||
|
|
@ -392,7 +399,10 @@ fun PreferencesContent(
|
|||
seerrDialogMode =
|
||||
when (val conn = seerrConnection) {
|
||||
is SeerrConnectionStatus.Error -> {
|
||||
SeerrDialogMode.Error(conn.serverUrl, conn.ex)
|
||||
SeerrDialogMode.Error(
|
||||
conn.serverUrl,
|
||||
conn.ex,
|
||||
)
|
||||
}
|
||||
|
||||
SeerrConnectionStatus.NotConfigured -> {
|
||||
|
|
@ -409,9 +419,19 @@ fun PreferencesContent(
|
|||
modifier = Modifier,
|
||||
summary =
|
||||
when (seerrConnection) {
|
||||
is SeerrConnectionStatus.Error -> stringResource(R.string.voice_error_server)
|
||||
SeerrConnectionStatus.NotConfigured -> stringResource(R.string.add_server)
|
||||
is SeerrConnectionStatus.Success -> stringResource(R.string.enabled)
|
||||
is SeerrConnectionStatus.Error -> {
|
||||
stringResource(R.string.voice_error_server)
|
||||
}
|
||||
|
||||
SeerrConnectionStatus.NotConfigured -> {
|
||||
stringResource(
|
||||
R.string.add_server,
|
||||
)
|
||||
}
|
||||
|
||||
is SeerrConnectionStatus.Success -> {
|
||||
stringResource(R.string.enabled)
|
||||
}
|
||||
},
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
|
|
@ -434,6 +454,19 @@ fun PreferencesContent(
|
|||
)
|
||||
}
|
||||
|
||||
ScreensaverPreference.Start -> {
|
||||
ClickPreference(
|
||||
title = stringResource(pref.title),
|
||||
onClick = {
|
||||
viewModel.screensaverService.start()
|
||||
},
|
||||
modifier = Modifier,
|
||||
summary = pref.summary(context, null),
|
||||
onLongClick = {},
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val value = pref.getter.invoke(preferences)
|
||||
ComposablePreference(
|
||||
|
|
@ -597,6 +630,7 @@ fun PreferencesPage(
|
|||
PreferenceScreenOption.ADVANCED,
|
||||
PreferenceScreenOption.EXO_PLAYER,
|
||||
PreferenceScreenOption.MPV,
|
||||
PreferenceScreenOption.SCREENSAVER,
|
||||
-> {
|
||||
PreferencesContent(
|
||||
initialPreferences,
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.preferences.resetSubtitles
|
|||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||
import com.github.damontecres.wholphin.services.BackdropService
|
||||
import com.github.damontecres.wholphin.services.NavigationManager
|
||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||
import com.github.damontecres.wholphin.services.SeerrServerRepository
|
||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
|
|
@ -35,6 +36,7 @@ class PreferencesViewModel
|
|||
val preferenceDataStore: DataStore<AppPreferences>,
|
||||
val navigationManager: NavigationManager,
|
||||
val backdropService: BackdropService,
|
||||
val screensaverService: ScreensaverService,
|
||||
private val rememberTabManager: RememberTabManager,
|
||||
private val serverRepository: ServerRepository,
|
||||
private val seerrServerRepository: SeerrServerRepository,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Box
|
|||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -63,8 +62,6 @@ import com.github.damontecres.wholphin.data.model.VideoFilter
|
|||
import com.github.damontecres.wholphin.ui.AppColors
|
||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
||||
import com.github.damontecres.wholphin.ui.findActivity
|
||||
import com.github.damontecres.wholphin.ui.keepScreenOn
|
||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||
import com.github.damontecres.wholphin.ui.playback.isDirectionalDpad
|
||||
import com.github.damontecres.wholphin.ui.playback.isDpad
|
||||
|
|
@ -208,12 +205,6 @@ fun SlideshowPage(
|
|||
LaunchedEffect(slideshowActive) {
|
||||
player.repeatMode =
|
||||
if (slideshowState.enabled) Player.REPEAT_MODE_OFF else Player.REPEAT_MODE_ONE
|
||||
context.findActivity()?.keepScreenOn(slideshowActive)
|
||||
}
|
||||
DisposableEffect(Unit) {
|
||||
onDispose {
|
||||
context.findActivity()?.keepScreenOn(false)
|
||||
}
|
||||
}
|
||||
|
||||
var longPressing by remember { mutableStateOf(false) }
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import com.github.damontecres.wholphin.data.model.VideoFilter
|
|||
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||
import com.github.damontecres.wholphin.services.PlayerFactory
|
||||
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||
import com.github.damontecres.wholphin.ui.PhotoItemFields
|
||||
import com.github.damontecres.wholphin.ui.launchIO
|
||||
|
|
@ -67,6 +68,7 @@ class SlideshowViewModel
|
|||
private val serverRepository: ServerRepository,
|
||||
private val imageUrlService: ImageUrlService,
|
||||
private val userPreferencesService: UserPreferencesService,
|
||||
private val screensaverService: ScreensaverService,
|
||||
@Assisted val slideshowSettings: Destination.Slideshow,
|
||||
) : ViewModel(),
|
||||
Player.Listener {
|
||||
|
|
@ -110,6 +112,7 @@ class SlideshowViewModel
|
|||
|
||||
init {
|
||||
addCloseable {
|
||||
screensaverService.keepScreenOn(false)
|
||||
player.removeListener(this@SlideshowViewModel)
|
||||
player.release()
|
||||
}
|
||||
|
|
@ -282,6 +285,7 @@ class SlideshowViewModel
|
|||
private var slideshowJob: Job? = null
|
||||
|
||||
fun startSlideshow() {
|
||||
screensaverService.keepScreenOn(true)
|
||||
_slideshow.update {
|
||||
SlideshowState(enabled = true, paused = false)
|
||||
}
|
||||
|
|
@ -295,6 +299,7 @@ class SlideshowViewModel
|
|||
}
|
||||
|
||||
fun stopSlideshow() {
|
||||
screensaverService.keepScreenOn(false)
|
||||
slideshowJob?.cancel()
|
||||
_slideshow.update {
|
||||
SlideshowState(enabled = false, paused = false)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue