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
![screensaver_settings
Large](https://github.com/user-attachments/assets/9b3240dd-81cb-4c20-bc4e-b2bf38698b2f)

## 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:
Ray 2026-02-25 14:11:00 -05:00 committed by GitHub
parent 536e8d366c
commit a7e86fbc15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 954 additions and 78 deletions

View file

@ -3,10 +3,12 @@ package com.github.damontecres.wholphin
import android.content.Intent
import android.content.res.Configuration
import android.os.Bundle
import android.view.KeyEvent
import android.view.WindowManager
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@ -49,6 +51,7 @@ import com.github.damontecres.wholphin.services.ImageUrlService
import com.github.damontecres.wholphin.services.NavigationManager
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
import com.github.damontecres.wholphin.services.RefreshRateService
import com.github.damontecres.wholphin.services.ScreensaverService
import com.github.damontecres.wholphin.services.ServerEventListener
import com.github.damontecres.wholphin.services.SetupDestination
import com.github.damontecres.wholphin.services.SetupNavigationManager
@ -59,8 +62,10 @@ import com.github.damontecres.wholphin.services.hilt.AuthOkHttpClient
import com.github.damontecres.wholphin.services.tvprovider.TvProviderSchedulerService
import com.github.damontecres.wholphin.ui.CoilConfig
import com.github.damontecres.wholphin.ui.LocalImageUrlService
import com.github.damontecres.wholphin.ui.components.AppScreensaver
import com.github.damontecres.wholphin.ui.components.LoadingPage
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
import com.github.damontecres.wholphin.ui.launchDefault
import com.github.damontecres.wholphin.ui.launchIO
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
import com.github.damontecres.wholphin.ui.nav.Destination
@ -134,6 +139,9 @@ class MainActivity : AppCompatActivity() {
@Inject
lateinit var datePlayedInvalidationService: DatePlayedInvalidationService
@Inject
lateinit var screensaverService: ScreensaverService
private var signInAuto = true
@OptIn(ExperimentalTvMaterial3Api::class)
@ -317,6 +325,15 @@ class MainActivity : AppCompatActivity() {
}
},
)
val screenSaverState by screensaverService.state.collectAsState()
if (screenSaverState.enabled || screenSaverState.enabledTemp) {
AnimatedVisibility(
screenSaverState.show,
Modifier.fillMaxSize(),
) {
AppScreensaver(appPreferences, Modifier.fillMaxSize())
}
}
}
}
}
@ -324,10 +341,22 @@ class MainActivity : AppCompatActivity() {
}
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (screensaverService.state.value.show) {
screensaverService.stop(false)
screensaverService.pulse()
return true
} else {
screensaverService.pulse()
return super.dispatchKeyEvent(event)
}
}
override fun onResume() {
super.onResume()
Timber.d("onResume")
lifecycleScope.launchIO {
lifecycleScope.launchDefault {
screensaverService.pulse()
appUpgradeHandler.run()
}
}
@ -341,6 +370,7 @@ class MainActivity : AppCompatActivity() {
override fun onStop() {
super.onStop()
Timber.d("onStop")
screensaverService.stop(true)
tvProviderSchedulerService.launchOneTimeRefresh()
}