mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
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
|
|
@ -68,6 +68,17 @@
|
||||||
android:name="androidx.startup.InitializationProvider"
|
android:name="androidx.startup.InitializationProvider"
|
||||||
android:authorities="${applicationId}.androidx-startup"
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
tools:node="remove" />
|
tools:node="remove" />
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".WholphinDreamService"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:permission="android.permission.BIND_DREAM_SERVICE">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.service.dreams.DreamService" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
</intent-filter>
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ package com.github.damontecres.wholphin
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.KeyEvent
|
||||||
import android.view.WindowManager
|
import android.view.WindowManager
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.NavigationManager
|
||||||
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
|
import com.github.damontecres.wholphin.services.PlaybackLifecycleObserver
|
||||||
import com.github.damontecres.wholphin.services.RefreshRateService
|
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.ServerEventListener
|
||||||
import com.github.damontecres.wholphin.services.SetupDestination
|
import com.github.damontecres.wholphin.services.SetupDestination
|
||||||
import com.github.damontecres.wholphin.services.SetupNavigationManager
|
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.services.tvprovider.TvProviderSchedulerService
|
||||||
import com.github.damontecres.wholphin.ui.CoilConfig
|
import com.github.damontecres.wholphin.ui.CoilConfig
|
||||||
import com.github.damontecres.wholphin.ui.LocalImageUrlService
|
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.components.LoadingPage
|
||||||
import com.github.damontecres.wholphin.ui.detail.series.SeasonEpisodeIds
|
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.launchIO
|
||||||
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
|
import com.github.damontecres.wholphin.ui.nav.ApplicationContent
|
||||||
import com.github.damontecres.wholphin.ui.nav.Destination
|
import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
|
|
@ -134,6 +139,9 @@ class MainActivity : AppCompatActivity() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var datePlayedInvalidationService: DatePlayedInvalidationService
|
lateinit var datePlayedInvalidationService: DatePlayedInvalidationService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var screensaverService: ScreensaverService
|
||||||
|
|
||||||
private var signInAuto = true
|
private var signInAuto = true
|
||||||
|
|
||||||
@OptIn(ExperimentalTvMaterial3Api::class)
|
@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() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
Timber.d("onResume")
|
Timber.d("onResume")
|
||||||
lifecycleScope.launchIO {
|
lifecycleScope.launchDefault {
|
||||||
|
screensaverService.pulse()
|
||||||
appUpgradeHandler.run()
|
appUpgradeHandler.run()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -341,6 +370,7 @@ class MainActivity : AppCompatActivity() {
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
Timber.d("onStop")
|
Timber.d("onStop")
|
||||||
|
screensaverService.stop(true)
|
||||||
tvProviderSchedulerService.launchOneTimeRefresh()
|
tvProviderSchedulerService.launchOneTimeRefresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,98 @@
|
||||||
|
package com.github.damontecres.wholphin
|
||||||
|
|
||||||
|
import android.service.dreams.DreamService
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.platform.ComposeView
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleRegistry
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
|
import androidx.lifecycle.setViewTreeLifecycleOwner
|
||||||
|
import androidx.savedstate.SavedStateRegistry
|
||||||
|
import androidx.savedstate.SavedStateRegistryController
|
||||||
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
|
||||||
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
|
import com.github.damontecres.wholphin.services.ScreensaverService
|
||||||
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
|
import com.github.damontecres.wholphin.ui.components.AppScreensaverContent
|
||||||
|
import com.github.damontecres.wholphin.ui.theme.WholphinTheme
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class WholphinDreamService :
|
||||||
|
DreamService(),
|
||||||
|
SavedStateRegistryOwner {
|
||||||
|
@Inject
|
||||||
|
lateinit var screensaverService: ScreensaverService
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userPreferencesService: UserPreferencesService
|
||||||
|
|
||||||
|
private val lifecycleRegistry = LifecycleRegistry(this)
|
||||||
|
|
||||||
|
private val savedStateRegistryController =
|
||||||
|
SavedStateRegistryController.create(this).apply {
|
||||||
|
performAttach()
|
||||||
|
}
|
||||||
|
|
||||||
|
override val lifecycle: Lifecycle get() = lifecycleRegistry
|
||||||
|
override val savedStateRegistry: SavedStateRegistry get() = savedStateRegistryController.savedStateRegistry
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
savedStateRegistryController.performRestore(null)
|
||||||
|
lifecycleRegistry.currentState = Lifecycle.State.CREATED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAttachedToWindow() {
|
||||||
|
super.onAttachedToWindow()
|
||||||
|
val itemFlow = screensaverService.createItemFlow(lifecycleScope)
|
||||||
|
setContentView(
|
||||||
|
ComposeView(this).apply {
|
||||||
|
setViewTreeLifecycleOwner(this@WholphinDreamService)
|
||||||
|
setViewTreeSavedStateRegistryOwner(this@WholphinDreamService)
|
||||||
|
setContent {
|
||||||
|
var prefs by remember { mutableStateOf<UserPreferences?>(null) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
userPreferencesService.flow.collectLatest { prefs = it }
|
||||||
|
}
|
||||||
|
prefs?.let { prefs ->
|
||||||
|
WholphinTheme(appThemeColors = prefs.appPreferences.interfacePreferences.appThemeColors) {
|
||||||
|
val screensaverPrefs =
|
||||||
|
prefs.appPreferences.interfacePreferences.screensaverPreference
|
||||||
|
val currentItem by itemFlow.collectAsState(null)
|
||||||
|
AppScreensaverContent(
|
||||||
|
currentItem = currentItem,
|
||||||
|
showClock = screensaverPrefs.showClock,
|
||||||
|
duration = screensaverPrefs.duration.milliseconds,
|
||||||
|
animate = screensaverPrefs.animate,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDreamingStarted() {
|
||||||
|
super.onDreamingStarted()
|
||||||
|
lifecycleRegistry.currentState = Lifecycle.State.STARTED
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDreamingStopped() {
|
||||||
|
super.onDreamingStopped()
|
||||||
|
lifecycleRegistry.currentState = Lifecycle.State.DESTROYED
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -997,6 +997,12 @@ sealed interface AppPreference<Pref, T> {
|
||||||
summaryOn = R.string.enabled,
|
summaryOn = R.string.enabled,
|
||||||
summaryOff = R.string.disabled,
|
summaryOff = R.string.disabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val ScreensaverSettings =
|
||||||
|
AppDestinationPreference<AppPreferences>(
|
||||||
|
title = R.string.screensaver_settings,
|
||||||
|
destination = Destination.Settings(PreferenceScreenOption.SCREENSAVER),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1011,6 +1017,7 @@ val basicPreferences =
|
||||||
AppPreference.RememberSelectedTab,
|
AppPreference.RememberSelectedTab,
|
||||||
AppPreference.SubtitleStyle,
|
AppPreference.SubtitleStyle,
|
||||||
AppPreference.ThemeColors,
|
AppPreference.ThemeColors,
|
||||||
|
AppPreference.ScreensaverSettings,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
PreferenceGroup(
|
PreferenceGroup(
|
||||||
|
|
@ -1201,6 +1208,24 @@ val liveTvPreferences =
|
||||||
AppPreference.LiveTvColorCodePrograms,
|
AppPreference.LiveTvColorCodePrograms,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val screensaverPreferences =
|
||||||
|
listOf(
|
||||||
|
PreferenceGroup(
|
||||||
|
title = R.string.screensaver,
|
||||||
|
preferences =
|
||||||
|
listOf(
|
||||||
|
ScreensaverPreference.Enabled,
|
||||||
|
ScreensaverPreference.StartDelay,
|
||||||
|
ScreensaverPreference.Duration,
|
||||||
|
ScreensaverPreference.ShowClock,
|
||||||
|
ScreensaverPreference.Animate,
|
||||||
|
ScreensaverPreference.MaxAge,
|
||||||
|
ScreensaverPreference.ItemTypes,
|
||||||
|
ScreensaverPreference.Start,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
data class AppSwitchPreference<Pref>(
|
data class AppSwitchPreference<Pref>(
|
||||||
@get:StringRes override val title: Int,
|
@get:StringRes override val title: Int,
|
||||||
override val defaultValue: Boolean,
|
override val defaultValue: Boolean,
|
||||||
|
|
@ -1255,8 +1280,6 @@ data class AppMultiChoicePreference<Pref, T>(
|
||||||
override val getter: (prefs: Pref) -> List<T>,
|
override val getter: (prefs: Pref) -> List<T>,
|
||||||
override val setter: (prefs: Pref, value: List<T>) -> Pref,
|
override val setter: (prefs: Pref, value: List<T>) -> Pref,
|
||||||
@param:StringRes val summary: Int? = null,
|
@param:StringRes val summary: Int? = null,
|
||||||
val toSharedPrefs: (T) -> String,
|
|
||||||
val fromSharedPrefs: (String) -> T?,
|
|
||||||
) : AppPreference<Pref, List<T>>
|
) : AppPreference<Pref, List<T>>
|
||||||
|
|
||||||
data class AppClickablePreference<Pref>(
|
data class AppClickablePreference<Pref>(
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.datastore.core.CorruptionException
|
||||||
import androidx.datastore.core.Serializer
|
import androidx.datastore.core.Serializer
|
||||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
||||||
import com.google.protobuf.InvalidProtocolBufferException
|
import com.google.protobuf.InvalidProtocolBufferException
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -120,6 +121,20 @@ class AppPreferencesSerializer
|
||||||
colorCodePrograms =
|
colorCodePrograms =
|
||||||
AppPreference.LiveTvColorCodePrograms.defaultValue
|
AppPreference.LiveTvColorCodePrograms.defaultValue
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
|
screensaverPreference =
|
||||||
|
ScreensaverPreferences
|
||||||
|
.newBuilder()
|
||||||
|
.apply {
|
||||||
|
startDelay = ScreensaverPreference.DEFAULT_START_DELAY
|
||||||
|
duration = ScreensaverPreference.DEFAULT_DURATION
|
||||||
|
animate = ScreensaverPreference.Animate.defaultValue
|
||||||
|
maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE
|
||||||
|
showClock = ScreensaverPreference.ShowClock.defaultValue
|
||||||
|
clearItemTypes()
|
||||||
|
addItemTypes(BaseItemKind.MOVIE.serialName)
|
||||||
|
addItemTypes(BaseItemKind.SERIES.serialName)
|
||||||
|
}.build()
|
||||||
}.build()
|
}.build()
|
||||||
|
|
||||||
advancedPreferences =
|
advancedPreferences =
|
||||||
|
|
@ -200,6 +215,11 @@ inline fun AppPreferences.updatePhotoPreferences(block: PhotoPreferences.Builder
|
||||||
photoPreferences = photoPreferences.toBuilder().apply(block).build()
|
photoPreferences = photoPreferences.toBuilder().apply(block).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline fun AppPreferences.updateScreensaverPreferences(block: ScreensaverPreferences.Builder.() -> Unit): AppPreferences =
|
||||||
|
updateInterfacePreferences {
|
||||||
|
screensaverPreference = screensaverPreference.toBuilder().apply(block).build()
|
||||||
|
}
|
||||||
|
|
||||||
fun SubtitlePreferences.Builder.resetSubtitles() {
|
fun SubtitlePreferences.Builder.resetSubtitles() {
|
||||||
fontSize = SubtitleSettings.FontSize.defaultValue.toInt()
|
fontSize = SubtitleSettings.FontSize.defaultValue.toInt()
|
||||||
fontColor = SubtitleSettings.FontColor.defaultValue.toArgb()
|
fontColor = SubtitleSettings.FontColor.defaultValue.toArgb()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,183 @@
|
||||||
|
package com.github.damontecres.wholphin.preferences
|
||||||
|
|
||||||
|
import com.github.damontecres.wholphin.R
|
||||||
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
object ScreensaverPreference {
|
||||||
|
val Enabled =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.in_app_screensaver,
|
||||||
|
defaultValue = false,
|
||||||
|
getter = { it.interfacePreferences.screensaverPreference.enabled },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateScreensaverPreferences { enabled = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.yes,
|
||||||
|
summaryOff = R.string.no,
|
||||||
|
)
|
||||||
|
|
||||||
|
const val DEFAULT_START_DELAY = 15 * 60_000L
|
||||||
|
private val startDelayValues =
|
||||||
|
listOf(
|
||||||
|
30_000L,
|
||||||
|
60_000L,
|
||||||
|
2 * 60_000L,
|
||||||
|
5 * 60_000L,
|
||||||
|
10 * 60_000L,
|
||||||
|
15 * 60_000L,
|
||||||
|
30 * 60_000L,
|
||||||
|
60 * 60_000L,
|
||||||
|
)
|
||||||
|
val StartDelay =
|
||||||
|
AppSliderPreference<AppPreferences>(
|
||||||
|
title = R.string.start_after,
|
||||||
|
defaultValue = startDelayValues.indexOf(DEFAULT_START_DELAY).toLong(),
|
||||||
|
min = 0,
|
||||||
|
max = startDelayValues.size - 1L,
|
||||||
|
interval = 1,
|
||||||
|
getter = {
|
||||||
|
startDelayValues.indexOf(it.interfacePreferences.screensaverPreference.startDelay).toLong()
|
||||||
|
},
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateScreensaverPreferences {
|
||||||
|
startDelay = startDelayValues[value.toInt()]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
summarizer = { value ->
|
||||||
|
if (value != null) {
|
||||||
|
val v = startDelayValues.getOrNull(value.toInt()) ?: DEFAULT_START_DELAY
|
||||||
|
v.milliseconds.toString()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const val DEFAULT_DURATION = 30_000L
|
||||||
|
private val durationValues =
|
||||||
|
listOf(
|
||||||
|
15_000L,
|
||||||
|
30_000L,
|
||||||
|
60_000L,
|
||||||
|
2 * 60_000L,
|
||||||
|
)
|
||||||
|
val Duration =
|
||||||
|
AppSliderPreference<AppPreferences>(
|
||||||
|
title = R.string.duration,
|
||||||
|
defaultValue = durationValues.indexOf(DEFAULT_DURATION).toLong(),
|
||||||
|
min = 0,
|
||||||
|
max = durationValues.size - 1L,
|
||||||
|
interval = 1,
|
||||||
|
getter = {
|
||||||
|
durationValues.indexOf(it.interfacePreferences.screensaverPreference.duration).toLong()
|
||||||
|
},
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateScreensaverPreferences {
|
||||||
|
duration = durationValues[value.toInt()]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
summarizer = { value ->
|
||||||
|
if (value != null) {
|
||||||
|
val v = durationValues.getOrNull(value.toInt()) ?: DEFAULT_DURATION
|
||||||
|
v.milliseconds.toString()
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val ShowClock =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.show_clock,
|
||||||
|
defaultValue = AppPreference.ShowClock.defaultValue,
|
||||||
|
getter = { it.interfacePreferences.screensaverPreference.showClock },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateScreensaverPreferences { showClock = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.yes,
|
||||||
|
summaryOff = R.string.no,
|
||||||
|
)
|
||||||
|
|
||||||
|
val Animate =
|
||||||
|
AppSwitchPreference<AppPreferences>(
|
||||||
|
title = R.string.animate,
|
||||||
|
defaultValue = true,
|
||||||
|
getter = { it.interfacePreferences.screensaverPreference.animate },
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateScreensaverPreferences { animate = value }
|
||||||
|
},
|
||||||
|
summaryOn = R.string.enabled,
|
||||||
|
summaryOff = R.string.disabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
const val DEFAULT_MAX_AGE = 16
|
||||||
|
private val maxAgeValues = listOf(0, 5, 10, 13, 14, 16, 18, 21, -1)
|
||||||
|
val MaxAge =
|
||||||
|
AppSliderPreference<AppPreferences>(
|
||||||
|
title = R.string.max_age_rating,
|
||||||
|
defaultValue = maxAgeValues.indexOf(DEFAULT_MAX_AGE).toLong(),
|
||||||
|
min = 0,
|
||||||
|
max = maxAgeValues.size - 1L,
|
||||||
|
interval = 1,
|
||||||
|
getter = {
|
||||||
|
it.interfacePreferences.screensaverPreference.maxAgeFilter
|
||||||
|
.takeIf { it >= 0 }
|
||||||
|
?.let { maxAgeValues.indexOf(it).toLong() }
|
||||||
|
?: maxAgeValues.lastIndex.toLong()
|
||||||
|
},
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateScreensaverPreferences {
|
||||||
|
maxAgeFilter = maxAgeValues[value.toInt()]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
summarizer = { value ->
|
||||||
|
when (value) {
|
||||||
|
null -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
maxAgeValues.lastIndex.toLong() -> {
|
||||||
|
WholphinApplication.instance.getString(R.string.no_max)
|
||||||
|
}
|
||||||
|
|
||||||
|
0L -> {
|
||||||
|
WholphinApplication.instance.getString(R.string.for_all_ages)
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
WholphinApplication.instance.getString(
|
||||||
|
R.string.up_to_age,
|
||||||
|
maxAgeValues[value.toInt()].toString(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val ItemTypes =
|
||||||
|
AppMultiChoicePreference<AppPreferences, BaseItemKind>(
|
||||||
|
title = R.string.include_types,
|
||||||
|
summary = R.string.include_types_summary,
|
||||||
|
defaultValue = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||||
|
allValues = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES, BaseItemKind.PHOTO),
|
||||||
|
displayValues = R.array.screensaver_item_types,
|
||||||
|
getter = {
|
||||||
|
it.interfacePreferences.screensaverPreference.itemTypesList.map { type ->
|
||||||
|
BaseItemKind.fromName(type)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setter = { prefs, value ->
|
||||||
|
prefs.updateScreensaverPreferences {
|
||||||
|
clearItemTypes()
|
||||||
|
addAllItemTypes(value.map { it.serialName })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
val Start =
|
||||||
|
AppClickablePreference<AppPreferences>(
|
||||||
|
title = R.string.start_screensaver,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ import androidx.preference.PreferenceManager
|
||||||
import com.github.damontecres.wholphin.WholphinApplication
|
import com.github.damontecres.wholphin.WholphinApplication
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreference
|
import com.github.damontecres.wholphin.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
|
import com.github.damontecres.wholphin.preferences.ScreensaverPreference
|
||||||
import com.github.damontecres.wholphin.preferences.update
|
import com.github.damontecres.wholphin.preferences.update
|
||||||
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
|
import com.github.damontecres.wholphin.preferences.updateAdvancedPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateHomePagePreferences
|
import com.github.damontecres.wholphin.preferences.updateHomePagePreferences
|
||||||
|
|
@ -16,11 +17,13 @@ import com.github.damontecres.wholphin.preferences.updateLiveTvPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
import com.github.damontecres.wholphin.preferences.updateMpvOptions
|
||||||
import com.github.damontecres.wholphin.preferences.updatePhotoPreferences
|
import com.github.damontecres.wholphin.preferences.updatePhotoPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
|
import com.github.damontecres.wholphin.preferences.updatePlaybackOverrides
|
||||||
|
import com.github.damontecres.wholphin.preferences.updateScreensaverPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||||
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
import com.github.damontecres.wholphin.ui.preferences.PreferencesViewModel
|
||||||
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
import com.github.damontecres.wholphin.ui.preferences.subtitle.SubtitleSettings
|
||||||
import com.github.damontecres.wholphin.util.Version
|
import com.github.damontecres.wholphin.util.Version
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -246,4 +249,18 @@ suspend fun upgradeApp(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (previous.isEqualOrBefore(Version.fromString("0.5.0-6-g0"))) {
|
||||||
|
appPreferences.updateData {
|
||||||
|
it.updateScreensaverPreferences {
|
||||||
|
startDelay = ScreensaverPreference.DEFAULT_START_DELAY
|
||||||
|
duration = ScreensaverPreference.DEFAULT_DURATION
|
||||||
|
animate = ScreensaverPreference.Animate.defaultValue
|
||||||
|
maxAgeFilter = ScreensaverPreference.DEFAULT_MAX_AGE
|
||||||
|
clearItemTypes()
|
||||||
|
addItemTypes(BaseItemKind.MOVIE.serialName)
|
||||||
|
addItemTypes(BaseItemKind.SERIES.serialName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ class ImageUrlService
|
||||||
fillHeight: Int? = null,
|
fillHeight: Int? = null,
|
||||||
): String? =
|
): String? =
|
||||||
when (imageType) {
|
when (imageType) {
|
||||||
ImageType.BACKDROP,
|
ImageType.LOGO -> {
|
||||||
ImageType.LOGO,
|
|
||||||
-> {
|
|
||||||
if (seriesId != null && (itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)) {
|
if (seriesId != null && (itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)) {
|
||||||
getItemImageUrl(
|
getItemImageUrl(
|
||||||
itemId = seriesId,
|
itemId = seriesId,
|
||||||
|
|
@ -57,6 +55,27 @@ class ImageUrlService
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ImageType.BACKDROP,
|
||||||
|
-> {
|
||||||
|
if (seriesId != null && (itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)) {
|
||||||
|
getItemImageUrl(
|
||||||
|
itemId = seriesId,
|
||||||
|
imageType = imageType,
|
||||||
|
fillWidth = fillWidth,
|
||||||
|
fillHeight = fillHeight,
|
||||||
|
)
|
||||||
|
} else if (backdropTags.isNotEmpty()) {
|
||||||
|
getItemImageUrl(
|
||||||
|
itemId = itemId,
|
||||||
|
imageType = imageType,
|
||||||
|
fillWidth = fillWidth,
|
||||||
|
fillHeight = fillHeight,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ImageType.THUMB -> {
|
ImageType.THUMB -> {
|
||||||
if (useSeriesForPrimary && parentThumbId != null &&
|
if (useSeriesForPrimary && parentThumbId != null &&
|
||||||
(itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)
|
(itemType == BaseItemKind.EPISODE || itemType == BaseItemKind.SEASON)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,224 @@
|
||||||
|
package com.github.damontecres.wholphin.services
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.view.WindowManager
|
||||||
|
import coil3.imageLoader
|
||||||
|
import coil3.request.ImageRequest
|
||||||
|
import com.github.damontecres.wholphin.MainActivity
|
||||||
|
import com.github.damontecres.wholphin.services.hilt.DefaultCoroutineScope
|
||||||
|
import com.github.damontecres.wholphin.ui.components.CurrentItem
|
||||||
|
import com.github.damontecres.wholphin.ui.formatDate
|
||||||
|
import com.github.damontecres.wholphin.util.ApiRequestPager
|
||||||
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
|
import com.github.damontecres.wholphin.util.GetItemsRequestHandler
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.cancellable
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.launchIn
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.jellyfin.sdk.api.client.ApiClient
|
||||||
|
import org.jellyfin.sdk.api.client.extensions.libraryApi
|
||||||
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
|
import org.jellyfin.sdk.model.api.ImageType
|
||||||
|
import org.jellyfin.sdk.model.api.ItemSortBy
|
||||||
|
import org.jellyfin.sdk.model.api.request.GetItemsRequest
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class ScreensaverService
|
||||||
|
@Inject
|
||||||
|
constructor(
|
||||||
|
@param:ApplicationContext private val context: Context,
|
||||||
|
@param:DefaultCoroutineScope private val scope: CoroutineScope,
|
||||||
|
private val api: ApiClient,
|
||||||
|
private val userPreferencesService: UserPreferencesService,
|
||||||
|
private val imageUrlService: ImageUrlService,
|
||||||
|
) {
|
||||||
|
private val _state = MutableStateFlow(ScreensaverState(false, false, false, false))
|
||||||
|
val state: StateFlow<ScreensaverState> = _state
|
||||||
|
|
||||||
|
private var waitJob: Job? = null
|
||||||
|
|
||||||
|
init {
|
||||||
|
userPreferencesService.flow
|
||||||
|
.onEach { prefs ->
|
||||||
|
_state.update {
|
||||||
|
val enabled =
|
||||||
|
prefs.appPreferences.interfacePreferences.screensaverPreference.enabled
|
||||||
|
keepScreenOnInternal(enabled)
|
||||||
|
ScreensaverState(enabled, false, false, false)
|
||||||
|
}
|
||||||
|
}.launchIn(scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pulse() {
|
||||||
|
waitJob?.cancel()
|
||||||
|
if (_state.value.enabled) {
|
||||||
|
// Timber.v("pulse")
|
||||||
|
_state.update {
|
||||||
|
if (!it.active) {
|
||||||
|
it.copy(active = false)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_state.value.paused) {
|
||||||
|
waitJob =
|
||||||
|
scope.launch(ExceptionHandler()) {
|
||||||
|
val startDelay =
|
||||||
|
userPreferencesService
|
||||||
|
.getCurrent()
|
||||||
|
.appPreferences.interfacePreferences.screensaverPreference.startDelay.milliseconds
|
||||||
|
delay(startDelay)
|
||||||
|
_state.update {
|
||||||
|
it.copy(active = true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun start() {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
enabledTemp = true,
|
||||||
|
active = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stop(cancelJob: Boolean) {
|
||||||
|
_state.update {
|
||||||
|
it.copy(
|
||||||
|
enabledTemp = false,
|
||||||
|
active = false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (cancelJob) waitJob?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun keepScreenOn(keep: Boolean) {
|
||||||
|
scope.launch {
|
||||||
|
val screensaverEnabled = _state.value.enabled
|
||||||
|
Timber.d("Keep screen on: %s, screensaverEnabled=%s", keep, screensaverEnabled)
|
||||||
|
if (screensaverEnabled) {
|
||||||
|
// Page is requesting to keep screen on, so we don't wait to show the screensaver
|
||||||
|
_state.update {
|
||||||
|
it.copy(active = false, paused = keep)
|
||||||
|
}
|
||||||
|
if (!keep) {
|
||||||
|
pulse()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
keepScreenOnInternal(keep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun keepScreenOnInternal(keep: Boolean) =
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
val window = MainActivity.instance.window
|
||||||
|
if (keep) {
|
||||||
|
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
} else {
|
||||||
|
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createItemFlow(scope: CoroutineScope): Flow<CurrentItem?> =
|
||||||
|
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()
|
||||||
|
Timber.v("Got %s items", pager.size)
|
||||||
|
var index = 0
|
||||||
|
if (pager.isEmpty()) {
|
||||||
|
emit(null)
|
||||||
|
} else {
|
||||||
|
while (true) {
|
||||||
|
val item = pager.getBlocking(index)
|
||||||
|
Timber.v("Next index=%s, item=%s", index, item?.id)
|
||||||
|
if (item != null) {
|
||||||
|
val backdropUrl =
|
||||||
|
if (item.type == BaseItemKind.PHOTO) {
|
||||||
|
api.libraryApi.getDownloadUrl(item.id)
|
||||||
|
} else {
|
||||||
|
imageUrlService.getItemImageUrl(item, ImageType.BACKDROP)
|
||||||
|
}
|
||||||
|
val title =
|
||||||
|
if (item.type == BaseItemKind.PHOTO) {
|
||||||
|
item.data.premiereDate?.let {
|
||||||
|
formatDate(it.toLocalDate())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item.title
|
||||||
|
}
|
||||||
|
val logoUrl = imageUrlService.getItemImageUrl(item, ImageType.LOGO)
|
||||||
|
if (backdropUrl != null) {
|
||||||
|
val result =
|
||||||
|
context.imageLoader
|
||||||
|
.enqueue(
|
||||||
|
ImageRequest
|
||||||
|
.Builder(context)
|
||||||
|
.data(backdropUrl)
|
||||||
|
.build(),
|
||||||
|
).job
|
||||||
|
.await()
|
||||||
|
try {
|
||||||
|
emit(CurrentItem(item, backdropUrl, logoUrl, title ?: ""))
|
||||||
|
} catch (_: CancellationException) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
val duration =
|
||||||
|
userPreferencesService
|
||||||
|
.getCurrent()
|
||||||
|
.appPreferences
|
||||||
|
.interfacePreferences.screensaverPreference.duration.milliseconds
|
||||||
|
delay(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.cancellable()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ScreensaverState(
|
||||||
|
val enabled: Boolean,
|
||||||
|
val enabledTemp: Boolean,
|
||||||
|
val active: Boolean,
|
||||||
|
val paused: Boolean,
|
||||||
|
) {
|
||||||
|
val show get() = (enabled || enabledTemp) && active && !paused
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ import com.github.damontecres.wholphin.data.ServerRepository
|
||||||
import com.github.damontecres.wholphin.preferences.AppPreferences
|
import com.github.damontecres.wholphin.preferences.AppPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.UserPreferences
|
import com.github.damontecres.wholphin.preferences.UserPreferences
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
|
@ -15,6 +16,8 @@ class UserPreferencesService
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val preferencesDataStore: DataStore<AppPreferences>,
|
private val preferencesDataStore: DataStore<AppPreferences>,
|
||||||
) {
|
) {
|
||||||
|
val flow = preferencesDataStore.data.map { UserPreferences(it) }
|
||||||
|
|
||||||
suspend fun getCurrent(): UserPreferences =
|
suspend fun getCurrent(): UserPreferences =
|
||||||
serverRepository.currentUserDto.value!!.configuration.let { userConfig ->
|
serverRepository.currentUserDto.value!!.configuration.let { userConfig ->
|
||||||
val appPrefs = preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
val appPrefs = preferencesDataStore.data.firstOrNull() ?: AppPreferences.getDefaultInstance()
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
import android.media.AudioManager
|
import android.media.AudioManager
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.WindowManager
|
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.compose.foundation.MarqueeAnimationMode
|
import androidx.compose.foundation.MarqueeAnimationMode
|
||||||
import androidx.compose.foundation.basicMarquee
|
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? {
|
fun Context.findActivity(): Activity? {
|
||||||
if (this is Activity) {
|
if (this is Activity) {
|
||||||
|
|
@ -310,18 +309,6 @@ fun Context.findActivity(): Activity? {
|
||||||
return null
|
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.
|
* 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
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
// Top scrim configuration for text readability (clock, season tabs)
|
// Top scrim configuration for text readability (clock, season tabs)
|
||||||
private const val TOP_SCRIM_ALPHA = 0.55f
|
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_END_FRACTION = 0.25f // Fraction of backdrop image height
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class ApplicationContentViewModel
|
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) {
|
var contentScale by remember(playerBackend) {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
if (playerBackend == PlayerBackend.MPV) {
|
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.PlaylistCreationResult
|
||||||
import com.github.damontecres.wholphin.services.PlaylistCreator
|
import com.github.damontecres.wholphin.services.PlaylistCreator
|
||||||
import com.github.damontecres.wholphin.services.RefreshRateService
|
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.StreamChoiceService
|
||||||
import com.github.damontecres.wholphin.services.UserPreferencesService
|
import com.github.damontecres.wholphin.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
import com.github.damontecres.wholphin.ui.isNotNullOrBlank
|
||||||
|
|
@ -140,6 +141,7 @@ class PlaybackViewModel
|
||||||
val streamChoiceService: StreamChoiceService,
|
val streamChoiceService: StreamChoiceService,
|
||||||
private val userPreferencesService: UserPreferencesService,
|
private val userPreferencesService: UserPreferencesService,
|
||||||
private val imageUrlService: ImageUrlService,
|
private val imageUrlService: ImageUrlService,
|
||||||
|
private val screensaverService: ScreensaverService,
|
||||||
@Assisted private val destination: Destination,
|
@Assisted private val destination: Destination,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
Player.Listener,
|
Player.Listener,
|
||||||
|
|
@ -189,7 +191,10 @@ class PlaybackViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launchIO {
|
viewModelScope.launchIO {
|
||||||
addCloseable { disconnectPlayer() }
|
addCloseable {
|
||||||
|
screensaverService.keepScreenOn(false)
|
||||||
|
disconnectPlayer()
|
||||||
|
}
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1424,6 +1429,10 @@ class PlaybackViewModel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||||
|
screensaverService.keepScreenOn(isPlaying)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class PlayerState(
|
data class PlayerState(
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,6 @@ import com.github.damontecres.wholphin.ui.nav.Destination
|
||||||
import com.github.damontecres.wholphin.util.ExceptionHandler
|
import com.github.damontecres.wholphin.util.ExceptionHandler
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.util.SortedSet
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -226,7 +225,7 @@ fun <T> ComposablePreference(
|
||||||
}
|
}
|
||||||
|
|
||||||
is AppMultiChoicePreference<*, *> -> {
|
is AppMultiChoicePreference<*, *> -> {
|
||||||
val values = stringArrayResource(preference.displayValues).toSortedSet()
|
val values = stringArrayResource(preference.displayValues)
|
||||||
val summary =
|
val summary =
|
||||||
preference.summary?.let { stringResource(it) }
|
preference.summary?.let { stringResource(it) }
|
||||||
?: preference.summary(context, value)
|
?: preference.summary(context, value)
|
||||||
|
|
@ -237,12 +236,15 @@ fun <T> ComposablePreference(
|
||||||
list
|
list
|
||||||
}
|
}
|
||||||
MultiChoicePreference(
|
MultiChoicePreference(
|
||||||
possibleValues = values as SortedSet<Any>,
|
possibleValues = preference.allValues,
|
||||||
selectedValues = selectedValues,
|
selectedValues = selectedValues,
|
||||||
title = title,
|
title = title,
|
||||||
summary = summary,
|
summary = summary,
|
||||||
onValueChange = {
|
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(
|
fun <T> MultiChoicePreference(
|
||||||
title: String,
|
title: String,
|
||||||
summary: String?,
|
summary: String?,
|
||||||
possibleValues: Set<T>,
|
possibleValues: List<T>,
|
||||||
selectedValues: Set<T>,
|
selectedValues: Set<T>,
|
||||||
onValueChange: (List<T>) -> Unit,
|
onValueChange: (List<T>) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
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 values = stringArrayResource(preference.displayValues).toList()
|
||||||
// val summary =
|
// val summary =
|
||||||
|
|
@ -59,7 +59,7 @@ fun <T> MultiChoicePreference(
|
||||||
items =
|
items =
|
||||||
possibleValues.mapIndexed { index, item ->
|
possibleValues.mapIndexed { index, item ->
|
||||||
DialogItem(
|
DialogItem(
|
||||||
headlineContent = { valueDisplay.invoke(item) },
|
headlineContent = { valueDisplay.invoke(index, item) },
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
Switch(
|
Switch(
|
||||||
checked = selectedValues.contains(item),
|
checked = selectedValues.contains(item),
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ enum class PreferenceScreenOption {
|
||||||
ADVANCED,
|
ADVANCED,
|
||||||
EXO_PLAYER,
|
EXO_PLAYER,
|
||||||
MPV,
|
MPV,
|
||||||
|
SCREENSAVER,
|
||||||
;
|
;
|
||||||
|
|
||||||
companion object {
|
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.ExoPlayerPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.MpvPreferences
|
import com.github.damontecres.wholphin.preferences.MpvPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.PlayerBackend
|
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.advancedPreferences
|
||||||
import com.github.damontecres.wholphin.preferences.basicPreferences
|
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.preferences.updatePlaybackPreferences
|
||||||
import com.github.damontecres.wholphin.services.SeerrConnectionStatus
|
import com.github.damontecres.wholphin.services.SeerrConnectionStatus
|
||||||
import com.github.damontecres.wholphin.services.UpdateChecker
|
import com.github.damontecres.wholphin.services.UpdateChecker
|
||||||
|
|
@ -128,6 +130,7 @@ fun PreferencesContent(
|
||||||
PreferenceScreenOption.ADVANCED -> advancedPreferences
|
PreferenceScreenOption.ADVANCED -> advancedPreferences
|
||||||
PreferenceScreenOption.EXO_PLAYER -> ExoPlayerPreferences
|
PreferenceScreenOption.EXO_PLAYER -> ExoPlayerPreferences
|
||||||
PreferenceScreenOption.MPV -> MpvPreferences
|
PreferenceScreenOption.MPV -> MpvPreferences
|
||||||
|
PreferenceScreenOption.SCREENSAVER -> screensaverPreferences
|
||||||
}
|
}
|
||||||
val screenTitle =
|
val screenTitle =
|
||||||
when (preferenceScreenOption) {
|
when (preferenceScreenOption) {
|
||||||
|
|
@ -135,6 +138,7 @@ fun PreferencesContent(
|
||||||
PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
|
PreferenceScreenOption.ADVANCED -> R.string.advanced_settings
|
||||||
PreferenceScreenOption.EXO_PLAYER -> R.string.exoplayer_options
|
PreferenceScreenOption.EXO_PLAYER -> R.string.exoplayer_options
|
||||||
PreferenceScreenOption.MPV -> R.string.mpv_options
|
PreferenceScreenOption.MPV -> R.string.mpv_options
|
||||||
|
PreferenceScreenOption.SCREENSAVER -> R.string.screensaver_settings
|
||||||
}
|
}
|
||||||
|
|
||||||
var visible by remember { mutableStateOf(false) }
|
var visible by remember { mutableStateOf(false) }
|
||||||
|
|
@ -283,7 +287,9 @@ fun PreferencesContent(
|
||||||
if (movementSounds) playOnClickSound(context)
|
if (movementSounds) playOnClickSound(context)
|
||||||
if (release != null && updateAvailable) {
|
if (release != null && updateAvailable) {
|
||||||
release?.let {
|
release?.let {
|
||||||
viewModel.navigationManager.navigateTo(Destination.UpdateApp)
|
viewModel.navigationManager.navigateTo(
|
||||||
|
Destination.UpdateApp,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
updateVM.init(preferences.updateUrl)
|
updateVM.init(preferences.updateUrl)
|
||||||
|
|
@ -313,7 +319,8 @@ fun PreferencesContent(
|
||||||
val summary =
|
val summary =
|
||||||
remember(cacheUsage) {
|
remember(cacheUsage) {
|
||||||
cacheUsage.let {
|
cacheUsage.let {
|
||||||
val diskMB = it.imageDiskUsed / AppPreference.MEGA_BIT
|
val diskMB =
|
||||||
|
it.imageDiskUsed / AppPreference.MEGA_BIT
|
||||||
val memoryUsedMB =
|
val memoryUsedMB =
|
||||||
it.imageMemoryUsed / AppPreference.MEGA_BIT
|
it.imageMemoryUsed / AppPreference.MEGA_BIT
|
||||||
val memoryMaxMB =
|
val memoryMaxMB =
|
||||||
|
|
@ -392,7 +399,10 @@ fun PreferencesContent(
|
||||||
seerrDialogMode =
|
seerrDialogMode =
|
||||||
when (val conn = seerrConnection) {
|
when (val conn = seerrConnection) {
|
||||||
is SeerrConnectionStatus.Error -> {
|
is SeerrConnectionStatus.Error -> {
|
||||||
SeerrDialogMode.Error(conn.serverUrl, conn.ex)
|
SeerrDialogMode.Error(
|
||||||
|
conn.serverUrl,
|
||||||
|
conn.ex,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SeerrConnectionStatus.NotConfigured -> {
|
SeerrConnectionStatus.NotConfigured -> {
|
||||||
|
|
@ -409,9 +419,19 @@ fun PreferencesContent(
|
||||||
modifier = Modifier,
|
modifier = Modifier,
|
||||||
summary =
|
summary =
|
||||||
when (seerrConnection) {
|
when (seerrConnection) {
|
||||||
is SeerrConnectionStatus.Error -> stringResource(R.string.voice_error_server)
|
is SeerrConnectionStatus.Error -> {
|
||||||
SeerrConnectionStatus.NotConfigured -> stringResource(R.string.add_server)
|
stringResource(R.string.voice_error_server)
|
||||||
is SeerrConnectionStatus.Success -> stringResource(R.string.enabled)
|
}
|
||||||
|
|
||||||
|
SeerrConnectionStatus.NotConfigured -> {
|
||||||
|
stringResource(
|
||||||
|
R.string.add_server,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is SeerrConnectionStatus.Success -> {
|
||||||
|
stringResource(R.string.enabled)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onLongClick = {},
|
onLongClick = {},
|
||||||
interactionSource = interactionSource,
|
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 -> {
|
else -> {
|
||||||
val value = pref.getter.invoke(preferences)
|
val value = pref.getter.invoke(preferences)
|
||||||
ComposablePreference(
|
ComposablePreference(
|
||||||
|
|
@ -597,6 +630,7 @@ fun PreferencesPage(
|
||||||
PreferenceScreenOption.ADVANCED,
|
PreferenceScreenOption.ADVANCED,
|
||||||
PreferenceScreenOption.EXO_PLAYER,
|
PreferenceScreenOption.EXO_PLAYER,
|
||||||
PreferenceScreenOption.MPV,
|
PreferenceScreenOption.MPV,
|
||||||
|
PreferenceScreenOption.SCREENSAVER,
|
||||||
-> {
|
-> {
|
||||||
PreferencesContent(
|
PreferencesContent(
|
||||||
initialPreferences,
|
initialPreferences,
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import com.github.damontecres.wholphin.preferences.resetSubtitles
|
||||||
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
import com.github.damontecres.wholphin.preferences.updateSubtitlePreferences
|
||||||
import com.github.damontecres.wholphin.services.BackdropService
|
import com.github.damontecres.wholphin.services.BackdropService
|
||||||
import com.github.damontecres.wholphin.services.NavigationManager
|
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.services.SeerrServerRepository
|
||||||
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
import com.github.damontecres.wholphin.ui.detail.DebugViewModel.Companion.sendAppLogs
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
|
@ -35,6 +36,7 @@ class PreferencesViewModel
|
||||||
val preferenceDataStore: DataStore<AppPreferences>,
|
val preferenceDataStore: DataStore<AppPreferences>,
|
||||||
val navigationManager: NavigationManager,
|
val navigationManager: NavigationManager,
|
||||||
val backdropService: BackdropService,
|
val backdropService: BackdropService,
|
||||||
|
val screensaverService: ScreensaverService,
|
||||||
private val rememberTabManager: RememberTabManager,
|
private val rememberTabManager: RememberTabManager,
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val seerrServerRepository: SeerrServerRepository,
|
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.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
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
|
||||||
|
|
@ -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.AppColors
|
||||||
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
import com.github.damontecres.wholphin.ui.components.ErrorMessage
|
||||||
import com.github.damontecres.wholphin.ui.components.LoadingPage
|
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.nav.Destination
|
||||||
import com.github.damontecres.wholphin.ui.playback.isDirectionalDpad
|
import com.github.damontecres.wholphin.ui.playback.isDirectionalDpad
|
||||||
import com.github.damontecres.wholphin.ui.playback.isDpad
|
import com.github.damontecres.wholphin.ui.playback.isDpad
|
||||||
|
|
@ -208,12 +205,6 @@ fun SlideshowPage(
|
||||||
LaunchedEffect(slideshowActive) {
|
LaunchedEffect(slideshowActive) {
|
||||||
player.repeatMode =
|
player.repeatMode =
|
||||||
if (slideshowState.enabled) Player.REPEAT_MODE_OFF else Player.REPEAT_MODE_ONE
|
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) }
|
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.preferences.AppPreference
|
||||||
import com.github.damontecres.wholphin.services.ImageUrlService
|
import com.github.damontecres.wholphin.services.ImageUrlService
|
||||||
import com.github.damontecres.wholphin.services.PlayerFactory
|
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.services.UserPreferencesService
|
||||||
import com.github.damontecres.wholphin.ui.PhotoItemFields
|
import com.github.damontecres.wholphin.ui.PhotoItemFields
|
||||||
import com.github.damontecres.wholphin.ui.launchIO
|
import com.github.damontecres.wholphin.ui.launchIO
|
||||||
|
|
@ -67,6 +68,7 @@ class SlideshowViewModel
|
||||||
private val serverRepository: ServerRepository,
|
private val serverRepository: ServerRepository,
|
||||||
private val imageUrlService: ImageUrlService,
|
private val imageUrlService: ImageUrlService,
|
||||||
private val userPreferencesService: UserPreferencesService,
|
private val userPreferencesService: UserPreferencesService,
|
||||||
|
private val screensaverService: ScreensaverService,
|
||||||
@Assisted val slideshowSettings: Destination.Slideshow,
|
@Assisted val slideshowSettings: Destination.Slideshow,
|
||||||
) : ViewModel(),
|
) : ViewModel(),
|
||||||
Player.Listener {
|
Player.Listener {
|
||||||
|
|
@ -110,6 +112,7 @@ class SlideshowViewModel
|
||||||
|
|
||||||
init {
|
init {
|
||||||
addCloseable {
|
addCloseable {
|
||||||
|
screensaverService.keepScreenOn(false)
|
||||||
player.removeListener(this@SlideshowViewModel)
|
player.removeListener(this@SlideshowViewModel)
|
||||||
player.release()
|
player.release()
|
||||||
}
|
}
|
||||||
|
|
@ -282,6 +285,7 @@ class SlideshowViewModel
|
||||||
private var slideshowJob: Job? = null
|
private var slideshowJob: Job? = null
|
||||||
|
|
||||||
fun startSlideshow() {
|
fun startSlideshow() {
|
||||||
|
screensaverService.keepScreenOn(true)
|
||||||
_slideshow.update {
|
_slideshow.update {
|
||||||
SlideshowState(enabled = true, paused = false)
|
SlideshowState(enabled = true, paused = false)
|
||||||
}
|
}
|
||||||
|
|
@ -295,6 +299,7 @@ class SlideshowViewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopSlideshow() {
|
fun stopSlideshow() {
|
||||||
|
screensaverService.keepScreenOn(false)
|
||||||
slideshowJob?.cancel()
|
slideshowJob?.cancel()
|
||||||
_slideshow.update {
|
_slideshow.update {
|
||||||
SlideshowState(enabled = false, paused = false)
|
SlideshowState(enabled = false, paused = false)
|
||||||
|
|
|
||||||
|
|
@ -144,6 +144,16 @@ enum BackdropStyle{
|
||||||
BACKDROP_NONE = 2;
|
BACKDROP_NONE = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ScreensaverPreferences{
|
||||||
|
int64 start_delay = 1;
|
||||||
|
int64 duration = 2;
|
||||||
|
bool animate = 3;
|
||||||
|
int32 max_age_filter = 4;
|
||||||
|
bool enabled = 5;
|
||||||
|
repeated string item_types = 6;
|
||||||
|
bool show_clock = 7;
|
||||||
|
}
|
||||||
|
|
||||||
message InterfacePreferences {
|
message InterfacePreferences {
|
||||||
ThemeSongVolume play_theme_songs = 1;
|
ThemeSongVolume play_theme_songs = 1;
|
||||||
bool remember_selected_tab = 2;
|
bool remember_selected_tab = 2;
|
||||||
|
|
@ -155,6 +165,7 @@ message InterfacePreferences {
|
||||||
LiveTvPreferences live_tv_preferences = 8;
|
LiveTvPreferences live_tv_preferences = 8;
|
||||||
BackdropStyle backdrop_style = 9;
|
BackdropStyle backdrop_style = 9;
|
||||||
SubtitlePreferences hdr_subtitles_preferences = 10;
|
SubtitlePreferences hdr_subtitles_preferences = 10;
|
||||||
|
ScreensaverPreferences screensaver_preference = 11;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdvancedPreferences {
|
message AdvancedPreferences {
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,11 @@
|
||||||
<item quantity="one">%d second</item>
|
<item quantity="one">%d second</item>
|
||||||
<item quantity="other">%d seconds</item>
|
<item quantity="other">%d seconds</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<plurals name="minutes">
|
||||||
|
<item quantity="zero">%s minutes</item>
|
||||||
|
<item quantity="one">%s minute</item>
|
||||||
|
<item quantity="other">%s minutes</item>
|
||||||
|
</plurals>
|
||||||
|
|
||||||
<plurals name="movies">
|
<plurals name="movies">
|
||||||
<item quantity="zero">Movies</item>
|
<item quantity="zero">Movies</item>
|
||||||
|
|
@ -522,6 +527,19 @@
|
||||||
<string name="display_presets">Display presets</string>
|
<string name="display_presets">Display presets</string>
|
||||||
<string name="display_presets_description">Built-in presets to quickly style all rows</string>
|
<string name="display_presets_description">Built-in presets to quickly style all rows</string>
|
||||||
<string name="customize_home_summary">Choose rows and images on the home page</string>
|
<string name="customize_home_summary">Choose rows and images on the home page</string>
|
||||||
|
<string name="start_after">Start after</string>
|
||||||
|
<string name="animate">Animate</string>
|
||||||
|
<string name="max_age_rating">Max age rating</string>
|
||||||
|
<string name="no_max">No max</string>
|
||||||
|
<string name="for_all_ages">For all ages</string>
|
||||||
|
<string name="up_to_age">Up to age %s</string>
|
||||||
|
<string name="screensaver">Screensaver</string>
|
||||||
|
<string name="screensaver_settings">Screensaver settings</string>
|
||||||
|
<string name="start_screensaver">Start screensaver</string>
|
||||||
|
<string name="duration">Duration</string>
|
||||||
|
<string name="photos">Photos</string>
|
||||||
|
<string name="include_types">Include types</string>
|
||||||
|
<string name="include_types_summary">What types of items to show images for</string>
|
||||||
<string name="content_scale_fit">Fit</string>
|
<string name="content_scale_fit">Fit</string>
|
||||||
<string name="content_scale_crop">Crop</string>
|
<string name="content_scale_crop">Crop</string>
|
||||||
<string name="content_scale_fill">Fill</string>
|
<string name="content_scale_fill">Fill</string>
|
||||||
|
|
@ -673,10 +691,17 @@
|
||||||
<string name="search_and_download_subtitles"><![CDATA[Search & download subtitles]]></string>
|
<string name="search_and_download_subtitles"><![CDATA[Search & download subtitles]]></string>
|
||||||
<string name="no_subtitles_found">No remote subtitles were found</string>
|
<string name="no_subtitles_found">No remote subtitles were found</string>
|
||||||
<string name="series_continueing">Present</string>
|
<string name="series_continueing">Present</string>
|
||||||
|
<string name="in_app_screensaver">Use in-app screensaver</string>
|
||||||
<string-array name="backdrop_style_options">
|
<string-array name="backdrop_style_options">
|
||||||
<item>@string/backdrop_style_dynamic</item>
|
<item>@string/backdrop_style_dynamic</item>
|
||||||
<item>@string/backdrop_style_image</item>
|
<item>@string/backdrop_style_image</item>
|
||||||
<item>@string/none</item>
|
<item>@string/none</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="screensaver_item_types">
|
||||||
|
<item>@string/movies</item>
|
||||||
|
<item>@string/tv_shows</item>
|
||||||
|
<item>@string/photos</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue