Skip to content

Commit 7b88899

Browse files
authored
feat: Screensavers for AndroidTV (#617)
1 parent f1caf4d commit 7b88899

22 files changed

Lines changed: 577 additions & 89 deletions

.vscode/tasks.json

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,18 @@
118118
{
119119
"label": "Generate Pigeon Files",
120120
"type": "shell",
121-
"command": "powershell",
121+
"command": "bash",
122122
"args": [
123-
"-Command",
124-
"Get-ChildItem -Path pigeons/*.dart | ForEach-Object { dart run pigeon --input $_.FullName }"
123+
"-c",
124+
"for f in pigeons/*.dart; do dart run pigeon --input \"$f\"; done"
125125
],
126+
"windows": {
127+
"command": "powershell",
128+
"args": [
129+
"-Command",
130+
"Get-ChildItem -Path pigeons/*.dart | ForEach-Object { dart run pigeon --input $_.FullName }"
131+
]
132+
},
126133
"group": {
127134
"kind": "build",
128135
"isDefault": true

android/app/src/main/kotlin/nl/jknaapen/fladder/VideoPlayerActivity.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
1313
import androidx.compose.ui.platform.LocalContext
1414
import androidx.media3.common.util.UnstableApi
1515
import nl.jknaapen.fladder.composables.controls.CustomVideoControls
16+
import nl.jknaapen.fladder.composables.overlays.screensavers.ScreenSaver
1617
import nl.jknaapen.fladder.objects.VideoPlayerObject
1718
import nl.jknaapen.fladder.player.ExoPlayer
1819
import nl.jknaapen.fladder.utility.ScaledContent
@@ -51,9 +52,11 @@ class VideoPlayerActivity : ComponentActivity() {
5152
fun VideoPlayerScreen(
5253
) {
5354
val leanBackEnabled = leanBackEnabled(LocalContext.current)
54-
ExoPlayer { player ->
55-
ScaledContent(if (leanBackEnabled) 0.6f else 1f) {
56-
CustomVideoControls(player)
55+
ScreenSaver {
56+
ExoPlayer { player ->
57+
ScaledContent(if (leanBackEnabled) 0.6f else 1f) {
58+
CustomVideoControls(player)
59+
}
5760
}
5861
}
5962
}

android/app/src/main/kotlin/nl/jknaapen/fladder/api/PlayerSettingsHelper.g.kt

Lines changed: 39 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,20 @@ private object PlayerSettingsHelperPigeonUtils {
6565

6666
}
6767

68+
enum class Screensaver(val raw: Int) {
69+
DISABLED(0),
70+
DVD(1),
71+
LOGO(2),
72+
TIME(3),
73+
BLACK(4);
74+
75+
companion object {
76+
fun ofRaw(raw: Int): Screensaver? {
77+
return values().firstOrNull { it.raw == raw }
78+
}
79+
}
80+
}
81+
6882
enum class VideoPlayerFit(val raw: Int) {
6983
FILL(0),
7084
CONTAIN(1),
@@ -142,7 +156,8 @@ data class PlayerSettings (
142156
val autoNextType: AutoNextType,
143157
val acceptedOrientations: List<PlayerOrientations>,
144158
val fillScreen: Boolean,
145-
val videoFit: VideoPlayerFit
159+
val videoFit: VideoPlayerFit,
160+
val screensaver: Screensaver
146161
)
147162
{
148163
companion object {
@@ -156,7 +171,8 @@ data class PlayerSettings (
156171
val acceptedOrientations = pigeonVar_list[6] as List<PlayerOrientations>
157172
val fillScreen = pigeonVar_list[7] as Boolean
158173
val videoFit = pigeonVar_list[8] as VideoPlayerFit
159-
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations, fillScreen, videoFit)
174+
val screensaver = pigeonVar_list[9] as Screensaver
175+
return PlayerSettings(enableTunneling, skipTypes, themeColor, skipForward, skipBackward, autoNextType, acceptedOrientations, fillScreen, videoFit, screensaver)
160176
}
161177
}
162178
fun toList(): List<Any?> {
@@ -170,6 +186,7 @@ data class PlayerSettings (
170186
acceptedOrientations,
171187
fillScreen,
172188
videoFit,
189+
screensaver,
173190
)
174191
}
175192
override fun equals(other: Any?): Boolean {
@@ -188,30 +205,35 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
188205
return when (type) {
189206
129.toByte() -> {
190207
return (readValue(buffer) as Long?)?.let {
191-
VideoPlayerFit.ofRaw(it.toInt())
208+
Screensaver.ofRaw(it.toInt())
192209
}
193210
}
194211
130.toByte() -> {
195212
return (readValue(buffer) as Long?)?.let {
196-
PlayerOrientations.ofRaw(it.toInt())
213+
VideoPlayerFit.ofRaw(it.toInt())
197214
}
198215
}
199216
131.toByte() -> {
200217
return (readValue(buffer) as Long?)?.let {
201-
AutoNextType.ofRaw(it.toInt())
218+
PlayerOrientations.ofRaw(it.toInt())
202219
}
203220
}
204221
132.toByte() -> {
205222
return (readValue(buffer) as Long?)?.let {
206-
SegmentType.ofRaw(it.toInt())
223+
AutoNextType.ofRaw(it.toInt())
207224
}
208225
}
209226
133.toByte() -> {
210227
return (readValue(buffer) as Long?)?.let {
211-
SegmentSkip.ofRaw(it.toInt())
228+
SegmentType.ofRaw(it.toInt())
212229
}
213230
}
214231
134.toByte() -> {
232+
return (readValue(buffer) as Long?)?.let {
233+
SegmentSkip.ofRaw(it.toInt())
234+
}
235+
}
236+
135.toByte() -> {
215237
return (readValue(buffer) as? List<Any?>)?.let {
216238
PlayerSettings.fromList(it)
217239
}
@@ -221,28 +243,32 @@ private open class PlayerSettingsHelperPigeonCodec : StandardMessageCodec() {
221243
}
222244
override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
223245
when (value) {
224-
is VideoPlayerFit -> {
246+
is Screensaver -> {
225247
stream.write(129)
226248
writeValue(stream, value.raw)
227249
}
228-
is PlayerOrientations -> {
250+
is VideoPlayerFit -> {
229251
stream.write(130)
230252
writeValue(stream, value.raw)
231253
}
232-
is AutoNextType -> {
254+
is PlayerOrientations -> {
233255
stream.write(131)
234256
writeValue(stream, value.raw)
235257
}
236-
is SegmentType -> {
258+
is AutoNextType -> {
237259
stream.write(132)
238260
writeValue(stream, value.raw)
239261
}
240-
is SegmentSkip -> {
262+
is SegmentType -> {
241263
stream.write(133)
242264
writeValue(stream, value.raw)
243265
}
244-
is PlayerSettings -> {
266+
is SegmentSkip -> {
245267
stream.write(134)
268+
writeValue(stream, value.raw)
269+
}
270+
is PlayerSettings -> {
271+
stream.write(135)
246272
writeValue(stream, value.toList())
247273
}
248274
else -> super.writeValue(stream, value)

android/app/src/main/kotlin/nl/jknaapen/fladder/composables/controls/VideoPlayerControls.kt

Lines changed: 8 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,16 @@ import io.github.rabehx.iconsax.outline.Refresh
7272
import kotlinx.coroutines.delay
7373
import nl.jknaapen.fladder.composables.dialogs.AudioPicker
7474
import nl.jknaapen.fladder.composables.dialogs.ChapterSelectionSheet
75-
import nl.jknaapen.fladder.composables.dialogs.SubtitlePicker
7675
import nl.jknaapen.fladder.composables.dialogs.PlaybackSpeedPicker
77-
import nl.jknaapen.fladder.objects.Localized
76+
import nl.jknaapen.fladder.composables.dialogs.SubtitlePicker
77+
import nl.jknaapen.fladder.composables.shared.CurrentTime
7878
import nl.jknaapen.fladder.objects.PlayerSettingsObject
79-
import nl.jknaapen.fladder.objects.Translate
8079
import nl.jknaapen.fladder.objects.VideoPlayerObject
8180
import nl.jknaapen.fladder.utility.ImmersiveSystemBars
8281
import nl.jknaapen.fladder.utility.defaultSelected
8382
import nl.jknaapen.fladder.utility.leanBackEnabled
8483
import nl.jknaapen.fladder.utility.visible
85-
import java.time.ZoneId
86-
import kotlin.time.Clock
8784
import kotlin.time.Duration.Companion.seconds
88-
import kotlin.time.ExperimentalTime
89-
import kotlin.time.toJavaInstant
9085

9186

9287
@RequiresApi(Build.VERSION_CODES.O)
@@ -179,13 +174,15 @@ fun CustomVideoControls(
179174
activity?.finish()
180175
return@onKeyEvent true
181176
}
177+
182178
Key.MediaPlay -> {
183179
player?.play()
184180
return@onKeyEvent true
185181
}
182+
186183
Key.MediaPlayPause -> {
187-
player?.let{
188-
if (it.isPlaying){
184+
player?.let {
185+
if (it.isPlaying) {
189186
it.pause()
190187
updateLastInteraction()
191188
} else {
@@ -194,11 +191,13 @@ fun CustomVideoControls(
194191
}
195192
return@onKeyEvent true
196193
}
194+
197195
Key.MediaPause, Key.P -> {
198196
player?.pause()
199197
updateLastInteraction()
200198
return@onKeyEvent true
201199
}
200+
202201
Key.Back, Key.Escape, Key.ButtonB, Key.Backspace -> {
203202
if (showControls) {
204203
hideControls()
@@ -593,35 +592,3 @@ internal fun RowScope.RightButtons(
593592
}
594593
}
595594
}
596-
597-
@RequiresApi(Build.VERSION_CODES.O)
598-
@kotlin.OptIn(ExperimentalTime::class)
599-
@Composable
600-
private fun CurrentTime() {
601-
val zone = ZoneId.systemDefault()
602-
603-
var currentTime by remember { mutableStateOf(Clock.System.now()) }
604-
605-
LaunchedEffect(Unit) {
606-
while (true) {
607-
currentTime = Clock.System.now()
608-
val delayMs = 60_000L - (currentTime.toEpochMilliseconds() % 60_000L)
609-
delay(delayMs)
610-
}
611-
}
612-
613-
val endZoned = currentTime.toJavaInstant().atZone(zone)
614-
615-
Translate(
616-
{
617-
Localized.hoursAndMinutes(endZoned.toOffsetDateTime().toString(), it)
618-
},
619-
key = currentTime,
620-
) { time ->
621-
Text(
622-
text = time,
623-
style = MaterialTheme.typography.titleLarge,
624-
color = Color.White
625-
)
626-
}
627-
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package nl.jknaapen.fladder.composables.overlays.screensavers
2+
3+
import Screensaver
4+
import android.os.Build
5+
import androidx.annotation.RequiresApi
6+
import androidx.compose.animation.AnimatedVisibility
7+
import androidx.compose.animation.fadeIn
8+
import androidx.compose.animation.fadeOut
9+
import androidx.compose.foundation.background
10+
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.fillMaxSize
12+
import androidx.compose.runtime.Composable
13+
import androidx.compose.runtime.LaunchedEffect
14+
import androidx.compose.runtime.collectAsState
15+
import androidx.compose.runtime.derivedStateOf
16+
import androidx.compose.runtime.getValue
17+
import androidx.compose.runtime.mutableLongStateOf
18+
import androidx.compose.runtime.mutableStateOf
19+
import androidx.compose.runtime.remember
20+
import androidx.compose.runtime.setValue
21+
import androidx.compose.ui.Modifier
22+
import androidx.compose.ui.graphics.Color
23+
import androidx.compose.ui.input.key.onKeyEvent
24+
import androidx.compose.ui.platform.LocalContext
25+
import kotlinx.coroutines.delay
26+
import nl.jknaapen.fladder.objects.PlayerSettingsObject
27+
import nl.jknaapen.fladder.objects.VideoPlayerObject
28+
import nl.jknaapen.fladder.utility.leanBackEnabled
29+
import kotlin.time.Duration.Companion.seconds
30+
31+
@RequiresApi(Build.VERSION_CODES.O)
32+
@Composable
33+
internal fun ScreenSaver(
34+
content: @Composable () -> Unit,
35+
) {
36+
if (!leanBackEnabled(LocalContext.current)) return
37+
38+
val selectedType by PlayerSettingsObject.screenSaver.collectAsState(Screensaver.LOGO)
39+
val isPlaying by VideoPlayerObject.playing.collectAsState(false)
40+
val isBuffering by VideoPlayerObject.buffering.collectAsState(true)
41+
42+
val lastInteraction = remember { mutableLongStateOf(System.currentTimeMillis()) }
43+
44+
val playerInactive by remember(isPlaying, isBuffering) {
45+
derivedStateOf {
46+
!isPlaying && !isBuffering
47+
}
48+
}
49+
50+
var screenSaverActive by remember { mutableStateOf(false) }
51+
52+
fun updateLastInteraction() {
53+
screenSaverActive = false
54+
lastInteraction.longValue = System.currentTimeMillis()
55+
}
56+
57+
LaunchedEffect(playerInactive, selectedType, lastInteraction.longValue) {
58+
if (selectedType == Screensaver.DISABLED) {
59+
screenSaverActive = false
60+
return@LaunchedEffect
61+
}
62+
63+
if (playerInactive) {
64+
delay(5.seconds)
65+
screenSaverActive = true
66+
} else {
67+
screenSaverActive = false
68+
}
69+
}
70+
71+
Box(
72+
modifier = Modifier.onKeyEvent { _ ->
73+
updateLastInteraction()
74+
if (screenSaverActive) {
75+
return@onKeyEvent true
76+
}
77+
return@onKeyEvent false
78+
}
79+
) {
80+
content()
81+
AnimatedVisibility(
82+
visible = screenSaverActive,
83+
enter = fadeIn(),
84+
exit = fadeOut(),
85+
) {
86+
when (selectedType) {
87+
Screensaver.DVD -> ScreensaverDvd()
88+
Screensaver.LOGO -> ScreensaverLogo()
89+
Screensaver.TIME -> ScreensaverTime()
90+
Screensaver.BLACK -> BlackScreensaver()
91+
Screensaver.DISABLED -> {}
92+
}
93+
}
94+
}
95+
}
96+
97+
@Composable
98+
private fun BlackScreensaver() {
99+
Box(
100+
modifier = Modifier
101+
.fillMaxSize()
102+
.background(color = Color.Black)
103+
)
104+
}

0 commit comments

Comments
 (0)