diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt index 7a3b71920..ba47c08ea 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/LyricsSheet.kt @@ -116,6 +116,10 @@ import com.theveloper.pixelplay.presentation.components.snapping.rememberLazyLis import com.theveloper.pixelplay.presentation.components.snapping.rememberSnapperFlingBehavior import com.theveloper.pixelplay.utils.LyricsUtils import com.theveloper.pixelplay.presentation.components.subcomps.LyricsMoreBottomSheet +import android.content.BroadcastReceiver +import android.content.Intent +import android.content.IntentFilter +import androidx.compose.ui.platform.LocalView import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import androidx.datastore.preferences.core.booleanPreferencesKey @@ -348,6 +352,67 @@ fun LyricsSheet( } val animatedLyricsBlurStrength by animatedLyricsBlurStrengthFlow.collectAsStateWithLifecycle(initialValue = 2.5f) + // Read keep-screen-on preference from DataStore + val keepScreenOnFlow = remember(context) { + context.dataStore.data.map { it[booleanPreferencesKey("keep_screen_on_lyrics")] ?: false } + } + var keepScreenOn by remember { mutableStateOf(false) } + // Sync DataStore → local state + LaunchedEffect(Unit) { + keepScreenOnFlow.collect { keepScreenOn = it } + } + val coroutineScope = rememberCoroutineScope() + + // Apply FLAG_KEEP_SCREEN_ON via the window when enabled + val view = LocalView.current + val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current + + DisposableEffect(keepScreenOn, lifecycleOwner) { + val observer = androidx.lifecycle.LifecycleEventObserver { _, event -> + if (event == androidx.lifecycle.Lifecycle.Event.ON_STOP && keepScreenOn) { + keepScreenOn = false + coroutineScope.launch { + context.dataStore.edit { prefs -> + prefs[booleanPreferencesKey("keep_screen_on_lyrics")] = false + } + } + } + } + + if (keepScreenOn) { + view.keepScreenOn = true + } + + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + view.keepScreenOn = false + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + + DisposableEffect(keepScreenOn, lifecycleOwner) { + val observer = androidx.lifecycle.LifecycleEventObserver { _, event -> + if (event == androidx.lifecycle.Lifecycle.Event.ON_STOP && keepScreenOn) { + keepScreenOn = false + coroutineScope.launch { + context.dataStore.edit { prefs -> + prefs[booleanPreferencesKey("keep_screen_on_lyrics")] = false + } + } + } + } + + if (keepScreenOn) { + view.keepScreenOn = true + } + + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + view.keepScreenOn = false + lifecycleOwner.lifecycle.removeObserver(observer) + } + } + val resolvedAutoscrollSpec = autoscrollAnimationSpec ?: if (useAnimatedLyrics) { spring( stiffness = Spring.StiffnessMediumLow, @@ -395,7 +460,25 @@ fun LyricsSheet( val swipeThresholdPx = with(LocalDensity.current) { swipeThreshold.toPx() } val overlayTranslation = remember { Animatable(0f) } val swipeProgress = remember { Animatable(0f) } - val coroutineScope = rememberCoroutineScope() + + // Reset keep-screen-on when the physical screen goes off (power button / OEM sleep gesture). + // ACTION_SCREEN_OFF is a guaranteed platform broadcast; no OEM can suppress it. + DisposableEffect(Unit) { + val receiver = object : BroadcastReceiver() { + override fun onReceive(ctx: android.content.Context, intent: Intent) { + if (intent.action == Intent.ACTION_SCREEN_OFF) { + keepScreenOn = false + coroutineScope.launch { + context.dataStore.edit { prefs -> + prefs[booleanPreferencesKey("keep_screen_on_lyrics")] = false + } + } + } + } + } + context.registerReceiver(receiver, IntentFilter(Intent.ACTION_SCREEN_OFF)) + onDispose { context.unregisterReceiver(receiver) } + } // Auto-hide controls logic LaunchedEffect(immersiveLyricsEnabled, lastInteractionTime, showSyncedLyrics, isImmersiveTemporarilyDisabled) { @@ -523,10 +606,15 @@ fun LyricsSheet( // Read backProgressProvider inside graphicsLayer (draw-phase) — no layout // pass is triggered per gesture frame, same pattern as SheetVisualState. // 0f = fully visible, 1f = fully dismissed. - // Effect: slides down 8 % of height (no fade). + // Effect: scale down to 92 % + slide down 8 % of height + fade to 72 % alpha. + // Matches Android predictive back spec for full-screen destinations and + // mirrors the scale+alpha treatment used across the rest of the app. .graphicsLayer { val p = backProgressProvider.value - translationY = androidx.compose.ui.util.lerp(0f, size.height * 0.08f, p) + val scale = lerp(1f, 0.92f, p) + scaleX = scale + scaleY = scale + translationY = lerp(0f, size.height * 0.08f, p) } .clip(RoundedCornerShape(32.dp)) .pointerInput(Unit) { @@ -938,6 +1026,15 @@ fun LyricsSheet( resetImmersiveTimer() onSetImmersiveTemporarilyDisabled(it) }, + keepScreenOn = keepScreenOn, + onKeepScreenOnChange = { enabled -> + keepScreenOn = enabled + coroutineScope.launch { + context.dataStore.edit { prefs -> + prefs[booleanPreferencesKey("keep_screen_on_lyrics")] = enabled + } + } + }, lyricsAlignment = lyricsAlignment, onLyricsAlignmentChange = { newAlignment -> coroutineScope.launch { @@ -1562,6 +1659,11 @@ fun LyricWordSpan( unhighlightedColor: Color, modifier: Modifier = Modifier ) { + val wordAnimSpec = if (useAnimatedLyrics) spring( + stiffness = Spring.StiffnessVeryLow, + dampingRatio = Spring.DampingRatioMediumBouncy + ) else tween(durationMillis = 200) + val color by animateColorAsState( targetValue = if (isHighlighted) highlightedColor else unhighlightedColor, animationSpec = if (useAnimatedLyrics) spring( @@ -1570,6 +1672,23 @@ fun LyricWordSpan( ) else tween(durationMillis = 200), label = "wordColor" ) + + // Scale: pop up to 1.10 on highlight, settle back to 1f. Only active when + // animated lyrics is on — layout is untouched because it's applied in graphicsLayer. + val scale by animateFloatAsState( + targetValue = if (useAnimatedLyrics && isHighlighted) 1.10f else 1f, + animationSpec = wordAnimSpec, + label = "wordScale" + ) + + // Alpha: unhighlighted words dim slightly so the active word pops without + // needing a hard color contrast. Only active when animated lyrics is on. + val alpha by animateFloatAsState( + targetValue = if (useAnimatedLyrics && !isHighlighted) 0.55f else 1f, + animationSpec = wordAnimSpec, + label = "wordAlpha" + ) + Box( modifier = modifier, contentAlignment = Alignment.Center @@ -1586,6 +1705,12 @@ fun LyricWordSpan( style = style, color = color, fontWeight = if (isHighlighted) FontWeight.Bold else FontWeight.Normal, + // Scale and alpha applied at draw phase — zero layout impact per frame. + modifier = Modifier.graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + } ) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt index bd2512416..64b66eeef 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/subcomps/LyricsMoreBottomSheet.kt @@ -21,6 +21,9 @@ import androidx.compose.material.icons.rounded.Abc import androidx.compose.material.icons.rounded.FormatAlignCenter import androidx.compose.material.icons.rounded.Tune import androidx.compose.material.icons.rounded.Translate +import androidx.compose.material.icons.rounded.BrightnessHigh +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -66,6 +69,8 @@ fun LyricsMoreBottomSheet( onToggleSyncControls: () -> Unit, isImmersiveTemporarilyDisabled: Boolean, onSetImmersiveTemporarilyDisabled: (Boolean) -> Unit, + keepScreenOn: Boolean, + onKeepScreenOnChange: (Boolean) -> Unit, lyricsAlignment: String, onLyricsAlignmentChange: (String) -> Unit, hasTranslatedLyrics: Boolean, @@ -292,15 +297,17 @@ fun LyricsMoreBottomSheet( val isRomanizationVisible = hasRomanizedLyrics val isTranslationVisible = hasTranslatedLyrics val isImmersiveVisible = showSyncedLyrics && immersiveLyricsEnabled + val isKeepScreenOnVisible = true - if (isSyncVisible || isRomanizationVisible || isTranslationVisible) { + if (isSyncVisible || isRomanizationVisible || isTranslationVisible || isKeepScreenOnVisible) { // Determine first and last items for rounding val isRomanizationFirst = isRomanizationVisible && !isSyncVisible val isTranslationFirst = isTranslationVisible && !isSyncVisible && !isRomanizationVisible - val isSyncLast = isSyncVisible && !isRomanizationVisible && !isTranslationVisible && !isImmersiveVisible - val isRomanizationLast = isRomanizationVisible && !isTranslationVisible && !isImmersiveVisible - val isTranslationLast = isTranslationVisible && !isImmersiveVisible + val isSyncLast = isSyncVisible && !isRomanizationVisible && !isTranslationVisible && !isImmersiveVisible && !isKeepScreenOnVisible + val isRomanizationLast = isRomanizationVisible && !isTranslationVisible && !isImmersiveVisible && !isKeepScreenOnVisible + val isTranslationLast = isTranslationVisible && !isImmersiveVisible && !isKeepScreenOnVisible + val isImmersiveLast = isImmersiveVisible && !isKeepScreenOnVisible Column( verticalArrangement = Arrangement.spacedBy(2.dp), @@ -461,6 +468,47 @@ fun LyricsMoreBottomSheet( ) ) }, + modifier = Modifier + .fillMaxWidth() + .clip( + RoundedCornerShape( + topStart = 8.dp, + topEnd = 8.dp, + bottomStart = if (isImmersiveLast) 24.dp else 8.dp, + bottomEnd = if (isImmersiveLast) 24.dp else 8.dp + ) + ) + .background(itemBackgroundColor), + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + headlineColor = contentColor, + leadingIconColor = contentColor + ) + ) + } + + // Keep Screen On Toggle + if (isKeepScreenOnVisible) { + ListItem( + headlineContent = { Text(stringResource(R.string.lyrics_more_keep_screen_on)) }, + leadingContent = { + Icon( + imageVector = Icons.Rounded.BrightnessHigh, + contentDescription = null + ) + }, + trailingContent = { + Switch( + checked = keepScreenOn, + onCheckedChange = onKeepScreenOnChange, + colors = SwitchDefaults.colors( + checkedThumbColor = onAccentColor, + checkedTrackColor = accentColor, + uncheckedThumbColor = contentColor, + uncheckedTrackColor = contentColor.copy(alpha = 0.3f) + ) + ) + }, modifier = Modifier .fillMaxWidth() .clip( @@ -471,7 +519,8 @@ fun LyricsMoreBottomSheet( bottomEnd = 24.dp ) ) - .background(itemBackgroundColor), + .background(itemBackgroundColor) + .clickable { onKeepScreenOnChange(!keepScreenOn) }, colors = ListItemDefaults.colors( containerColor = Color.Transparent, headlineColor = contentColor, diff --git a/app/src/main/res/values-de/strings_components.xml b/app/src/main/res/values-de/strings_components.xml index 54d6661cc..e109fa39e 100644 --- a/app/src/main/res/values-de/strings_components.xml +++ b/app/src/main/res/values-de/strings_components.xml @@ -23,6 +23,7 @@ Romanisierung anzeigen Übersetzungen anzeigen Immersive einmal deaktivieren + Bildschirm an lassen Lyrics linksbündig Lyrics zentriert Lyrics rechtsbündig diff --git a/app/src/main/res/values-es/strings_components.xml b/app/src/main/res/values-es/strings_components.xml index 86dcfd8cd..860e34c1c 100644 --- a/app/src/main/res/values-es/strings_components.xml +++ b/app/src/main/res/values-es/strings_components.xml @@ -23,6 +23,7 @@ Mostrar romanización Mostrar traducciones Desactivar inmersivo (una vez) + Keep screen on Alinear letras a la izquierda Alinear letras al centro Alinear letras a la derecha diff --git a/app/src/main/res/values-fr/strings_components.xml b/app/src/main/res/values-fr/strings_components.xml index 3850470c3..4c600584c 100644 --- a/app/src/main/res/values-fr/strings_components.xml +++ b/app/src/main/res/values-fr/strings_components.xml @@ -23,6 +23,7 @@ Afficher la romanisation Afficher les traductions Désactiver l\'immersion (une fois) + Keep screen on Aligner les paroles à gauche Aligner les paroles au centre Aligner les paroles à droite diff --git a/app/src/main/res/values-in/strings_components.xml b/app/src/main/res/values-in/strings_components.xml index 5d8e655f9..70f0c06c4 100644 --- a/app/src/main/res/values-in/strings_components.xml +++ b/app/src/main/res/values-in/strings_components.xml @@ -23,6 +23,7 @@ Tampilkan romanisasi Tampilkan terjemahan Nonaktifkan imersif (sekali) + Keep screen on Rata kiri lirik Rata tengah lirik Rata kanan lirik diff --git a/app/src/main/res/values-it/strings_components.xml b/app/src/main/res/values-it/strings_components.xml index fad80402c..41b2d1fdf 100644 --- a/app/src/main/res/values-it/strings_components.xml +++ b/app/src/main/res/values-it/strings_components.xml @@ -23,6 +23,7 @@ Mostra romanizzazione Mostra traduzioni Disabilita immersivo (una volta) + Keep screen on Allinea testo a sinistra Allinea testo al centro Allinea testo a destra diff --git a/app/src/main/res/values-ko/strings_components.xml b/app/src/main/res/values-ko/strings_components.xml index 0418da88c..bbf76cfba 100644 --- a/app/src/main/res/values-ko/strings_components.xml +++ b/app/src/main/res/values-ko/strings_components.xml @@ -23,6 +23,7 @@ 로마자 표기 표시 번역 표시 몰입 모드 해제 (1회) + Keep screen on 가사 왼쪽 정렬 가사 가운데 정렬 가사 오른쪽 정렬 diff --git a/app/src/main/res/values-nb/strings_components.xml b/app/src/main/res/values-nb/strings_components.xml index fbe10278c..3952f2e06 100644 --- a/app/src/main/res/values-nb/strings_components.xml +++ b/app/src/main/res/values-nb/strings_components.xml @@ -23,6 +23,7 @@ Vis romanisering Vis oversettelser Deaktiver immersiv modus (én gang) + Keep screen on Venstrejuster tekst Midtstill tekst Høyrejuster tekst diff --git a/app/src/main/res/values-ru/strings_components.xml b/app/src/main/res/values-ru/strings_components.xml index f60c007a7..8f9d051ee 100644 --- a/app/src/main/res/values-ru/strings_components.xml +++ b/app/src/main/res/values-ru/strings_components.xml @@ -23,6 +23,7 @@ Показать романизацию Показать перевод Выйти из иммерсивного (раз) + Keep screen on По левому краю По центру По правому краю diff --git a/app/src/main/res/values-zh-rCN/strings_components.xml b/app/src/main/res/values-zh-rCN/strings_components.xml index a23f7f816..150201e7d 100644 --- a/app/src/main/res/values-zh-rCN/strings_components.xml +++ b/app/src/main/res/values-zh-rCN/strings_components.xml @@ -23,6 +23,7 @@ 显示罗马音 显示翻译 关闭沉浸模式(仅本次) + Keep screen on 歌词左对齐 歌词居中对齐 歌词右对齐 diff --git a/app/src/main/res/values/strings_components.xml b/app/src/main/res/values/strings_components.xml index 898e50583..6105eff3b 100644 --- a/app/src/main/res/values/strings_components.xml +++ b/app/src/main/res/values/strings_components.xml @@ -23,6 +23,7 @@ Show romanization Show translations Disable immersive (once) + Keep screen on Align lyrics left Align lyrics center Align lyrics right