mirror of
https://github.com/JustinZeus/Wholphin.git
synced 2026-07-08 15:50:16 +02:00
Fix several issues with right-to-left locales (#1404)
## Description Fix several issues when the current locale is right-to-left - Fixes the backdrop dynamic colors - Fixes mirroring of playback controls, including seeking ### Related issues Fixes #1397 Fixes #1398 ### Testing Emulator w/ forced RTL in developer options ## Screenshots <img width="1280" height="720" alt="rtl_backdrop Large" src="https://github.com/user-attachments/assets/3e3b14ec-32cc-42c0-a224-d1d859a55acd" /> <img width="1280" height="720" alt="rtl_controls Large" src="https://github.com/user-attachments/assets/79983bb9-e1d0-4712-a5c6-da3d0721db96" /> ## AI or LLM usage None
This commit is contained in:
parent
656393fa1d
commit
9eff22e939
9 changed files with 214 additions and 117 deletions
|
|
@ -45,7 +45,9 @@ import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
|
|
@ -106,9 +108,11 @@ fun NowPlayingPage(
|
||||||
).value.appPreferences
|
).value.appPreferences
|
||||||
val musicPrefs = preferences.musicPreferences
|
val musicPrefs = preferences.musicPreferences
|
||||||
|
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
val keyHandler =
|
val keyHandler =
|
||||||
remember(preferences) {
|
remember(isLtr, preferences) {
|
||||||
PlaybackKeyHandler(
|
PlaybackKeyHandler(
|
||||||
|
isLtr = isLtr,
|
||||||
player = player,
|
player = player,
|
||||||
controlsEnabled = true,
|
controlsEnabled = true,
|
||||||
skipWithLeftRight = false,
|
skipWithLeftRight = false,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -75,6 +77,7 @@ fun Backdrop(
|
||||||
useExistingImageAsPlaceholder: Boolean = false,
|
useExistingImageAsPlaceholder: Boolean = false,
|
||||||
crossfadeDuration: Duration = 800.milliseconds,
|
crossfadeDuration: Duration = 800.milliseconds,
|
||||||
) {
|
) {
|
||||||
|
val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl
|
||||||
val baseBackgroundColor = MaterialTheme.colorScheme.background
|
val baseBackgroundColor = MaterialTheme.colorScheme.background
|
||||||
if (backdrop.hasColors &&
|
if (backdrop.hasColors &&
|
||||||
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
|
(backdropStyle == BackdropStyle.BACKDROP_DYNAMIC_COLOR || backdropStyle == BackdropStyle.UNRECOGNIZED)
|
||||||
|
|
@ -100,12 +103,14 @@ fun Backdrop(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.drawBehind {
|
.drawBehind {
|
||||||
drawRect(color = baseBackgroundColor)
|
drawRect(color = baseBackgroundColor)
|
||||||
|
val start = if (isRtl) size.width else 0f
|
||||||
|
val end = if (isRtl) 0f else size.width
|
||||||
// Top Left (Vibrant/Muted)
|
// Top Left (Vibrant/Muted)
|
||||||
drawRect(
|
drawRect(
|
||||||
brush =
|
brush =
|
||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(animSecondary, Color.Transparent),
|
colors = listOf(animSecondary, Color.Transparent),
|
||||||
center = Offset(0f, 0f),
|
center = Offset(start, 0f),
|
||||||
radius = size.width * 0.8f,
|
radius = size.width * 0.8f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -114,7 +119,7 @@ fun Backdrop(
|
||||||
brush =
|
brush =
|
||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(animPrimary, Color.Transparent),
|
colors = listOf(animPrimary, Color.Transparent),
|
||||||
center = Offset(size.width, size.height),
|
center = Offset(end, size.height),
|
||||||
radius = size.width * 0.8f,
|
radius = size.width * 0.8f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -127,7 +132,7 @@ fun Backdrop(
|
||||||
baseBackgroundColor,
|
baseBackgroundColor,
|
||||||
Color.Transparent,
|
Color.Transparent,
|
||||||
),
|
),
|
||||||
center = Offset(0f, size.height),
|
center = Offset(start, size.height),
|
||||||
radius = size.width * 0.8f,
|
radius = size.width * 0.8f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -136,7 +141,7 @@ fun Backdrop(
|
||||||
brush =
|
brush =
|
||||||
Brush.radialGradient(
|
Brush.radialGradient(
|
||||||
colors = listOf(animTertiary, Color.Transparent),
|
colors = listOf(animTertiary, Color.Transparent),
|
||||||
center = Offset(size.width, 0f),
|
center = Offset(end, 0f),
|
||||||
radius = size.width * 0.8f,
|
radius = size.width * 0.8f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -165,6 +170,7 @@ fun Backdrop(
|
||||||
.fillMaxWidth(.7f)
|
.fillMaxWidth(.7f)
|
||||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||||
.drawWithContent {
|
.drawWithContent {
|
||||||
|
val start = if (isRtl) size.width else 0f
|
||||||
drawContent()
|
drawContent()
|
||||||
if (drawerIsOpen) {
|
if (drawerIsOpen) {
|
||||||
drawRect(
|
drawRect(
|
||||||
|
|
@ -190,7 +196,7 @@ fun Backdrop(
|
||||||
brush =
|
brush =
|
||||||
Brush.horizontalGradient(
|
Brush.horizontalGradient(
|
||||||
colors = listOf(Color.Transparent, Color.Black),
|
colors = listOf(Color.Transparent, Color.Black),
|
||||||
startX = 0f,
|
startX = start,
|
||||||
endX = size.width * 0.6f,
|
endX = size.width * 0.6f,
|
||||||
),
|
),
|
||||||
blendMode = BlendMode.DstIn,
|
blendMode = BlendMode.DstIn,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,14 @@ fun isControllerMedia(event: KeyEvent) =
|
||||||
event.key == Key.ButtonL1 ||
|
event.key == Key.ButtonL1 ||
|
||||||
event.key == Key.ButtonL2
|
event.key == Key.ButtonL2
|
||||||
|
|
||||||
|
fun isDpadLeft(event: KeyEvent) =
|
||||||
|
event.key == Key.DirectionLeft ||
|
||||||
|
event.key == Key.SystemNavigationLeft
|
||||||
|
|
||||||
|
fun isDpadRight(event: KeyEvent) =
|
||||||
|
event.key == Key.DirectionRight ||
|
||||||
|
event.key == Key.SystemNavigationRight
|
||||||
|
|
||||||
fun isSkipBack(event: KeyEvent) =
|
fun isSkipBack(event: KeyEvent) =
|
||||||
event.key == Key.DirectionLeft ||
|
event.key == Key.DirectionLeft ||
|
||||||
event.key == Key.ButtonL1 ||
|
event.key == Key.ButtonL1 ||
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,10 @@ import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
|
@ -85,6 +87,10 @@ fun PlaybackDialog(
|
||||||
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
onPlaybackActionClick: (PlaybackAction) -> Unit,
|
||||||
onChangeSubtitleDelay: (Duration) -> Unit,
|
onChangeSubtitleDelay: (Duration) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
|
// TODO, shouldn't this work out of the box?
|
||||||
|
val leftGravity = remember(isLtr) { if (isLtr) Gravity.START else Gravity.END }
|
||||||
|
val rightGravity = remember(isLtr) { if (isLtr) Gravity.END else Gravity.START }
|
||||||
when (type) {
|
when (type) {
|
||||||
PlaybackDialogType.DEBUG -> {
|
PlaybackDialogType.DEBUG -> {
|
||||||
throw IllegalStateException("Should not open a dialog with " + PlaybackDialogType.DEBUG)
|
throw IllegalStateException("Should not open a dialog with " + PlaybackDialogType.DEBUG)
|
||||||
|
|
@ -113,7 +119,7 @@ fun PlaybackDialog(
|
||||||
onDismissRequest.invoke()
|
onDismissRequest.invoke()
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.SearchCaptions)
|
onPlaybackActionClick.invoke(PlaybackAction.SearchCaptions)
|
||||||
},
|
},
|
||||||
gravity = Gravity.END,
|
gravity = rightGravity,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -167,7 +173,7 @@ fun PlaybackDialog(
|
||||||
onClickPlaybackDialogType(choice.data)
|
onClickPlaybackDialogType(choice.data)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
gravity = Gravity.START,
|
gravity = leftGravity,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -182,7 +188,7 @@ fun PlaybackDialog(
|
||||||
onSelectChoice = { _, choice ->
|
onSelectChoice = { _, choice ->
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.ToggleAudio(choice.index))
|
onPlaybackActionClick.invoke(PlaybackAction.ToggleAudio(choice.index))
|
||||||
},
|
},
|
||||||
gravity = Gravity.END,
|
gravity = rightGravity,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -205,7 +211,7 @@ fun PlaybackDialog(
|
||||||
onSelectChoice = { _, value ->
|
onSelectChoice = { _, value ->
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.PlaybackSpeed(value.data))
|
onPlaybackActionClick.invoke(PlaybackAction.PlaybackSpeed(value.data))
|
||||||
},
|
},
|
||||||
gravity = Gravity.START,
|
gravity = leftGravity,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,7 +234,7 @@ fun PlaybackDialog(
|
||||||
onSelectChoice = { _, choice ->
|
onSelectChoice = { _, choice ->
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.Scale(choice.data))
|
onPlaybackActionClick.invoke(PlaybackAction.Scale(choice.data))
|
||||||
},
|
},
|
||||||
gravity = Gravity.START,
|
gravity = leftGravity,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import kotlin.time.Duration
|
||||||
* Handles [KeyEvent]s during playback on [PlaybackPage]
|
* Handles [KeyEvent]s during playback on [PlaybackPage]
|
||||||
*/
|
*/
|
||||||
class PlaybackKeyHandler(
|
class PlaybackKeyHandler(
|
||||||
|
private val isLtr: Boolean,
|
||||||
private val player: Player,
|
private val player: Player,
|
||||||
private val controlsEnabled: Boolean,
|
private val controlsEnabled: Boolean,
|
||||||
private val skipWithLeftRight: Boolean,
|
private val skipWithLeftRight: Boolean,
|
||||||
|
|
@ -47,11 +48,21 @@ class PlaybackKeyHandler(
|
||||||
if (isDirectionalDpad(it) || isEnterKey(it) || isControllerMedia(it)) {
|
if (isDirectionalDpad(it) || isEnterKey(it) || isControllerMedia(it)) {
|
||||||
if (!controllerViewState.controlsVisible) {
|
if (!controllerViewState.controlsVisible) {
|
||||||
if (skipWithLeftRight && isSkipBack(it)) {
|
if (skipWithLeftRight && isSkipBack(it)) {
|
||||||
updateSkipIndicator(-seekBack.inWholeMilliseconds)
|
if (isLtr) {
|
||||||
player.seekBack(seekBack)
|
updateSkipIndicator(-seekBack.inWholeMilliseconds)
|
||||||
|
player.seekBack(seekBack)
|
||||||
|
} else {
|
||||||
|
updateSkipIndicator(seekForward.inWholeMilliseconds)
|
||||||
|
player.seekForward(seekForward)
|
||||||
|
}
|
||||||
} else if (skipWithLeftRight && isSkipForward(it)) {
|
} else if (skipWithLeftRight && isSkipForward(it)) {
|
||||||
player.seekForward(seekForward)
|
if (isLtr) {
|
||||||
updateSkipIndicator(seekForward.inWholeMilliseconds)
|
player.seekForward(seekForward)
|
||||||
|
updateSkipIndicator(seekForward.inWholeMilliseconds)
|
||||||
|
} else {
|
||||||
|
player.seekBack(seekBack)
|
||||||
|
updateSkipIndicator(seekBack.inWholeMilliseconds)
|
||||||
|
}
|
||||||
} else if (oneClickPause && isEnterKey(it)) {
|
} else if (oneClickPause && isEnterKey(it)) {
|
||||||
val wasPlaying = player.isPlaying
|
val wasPlaying = player.isPlaying
|
||||||
Util.handlePlayPauseButtonAction(player)
|
Util.handlePlayPauseButtonAction(player)
|
||||||
|
|
@ -127,7 +138,7 @@ class PlaybackKeyHandler(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
val isBack = isSkipBack(event)
|
val isBack = if (isLtr) isSkipBack(event) else isSkipForward(event)
|
||||||
return when (event.type) {
|
return when (event.type) {
|
||||||
KeyEventType.KeyDown -> {
|
KeyEventType.KeyDown -> {
|
||||||
val repeatCount = event.nativeKeyEvent.repeatCount
|
val repeatCount = event.nativeKeyEvent.repeatCount
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,9 @@ import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.unit.IntSize
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
@ -234,25 +236,29 @@ fun PlaybackPageContent(
|
||||||
skipIndicatorDuration += delta
|
skipIndicatorDuration += delta
|
||||||
skipPosition = player.currentPosition
|
skipPosition = player.currentPosition
|
||||||
}
|
}
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
val keyHandler =
|
val keyHandler =
|
||||||
PlaybackKeyHandler(
|
remember(isLtr, preferences) {
|
||||||
player = player,
|
PlaybackKeyHandler(
|
||||||
controlsEnabled = state.nextUp == null,
|
isLtr = isLtr,
|
||||||
skipWithLeftRight = true,
|
player = player,
|
||||||
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
controlsEnabled = state.nextUp == null,
|
||||||
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
skipWithLeftRight = true,
|
||||||
getDurationMs = { player.duration.coerceAtLeast(0L) },
|
seekForward = preferences.appPreferences.playbackPreferences.skipForwardMs.milliseconds,
|
||||||
controllerViewState = controllerViewState,
|
seekBack = preferences.appPreferences.playbackPreferences.skipBackMs.milliseconds,
|
||||||
updateSkipIndicator = updateSkipIndicator,
|
getDurationMs = { player.duration.coerceAtLeast(0L) },
|
||||||
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
controllerViewState = controllerViewState,
|
||||||
onInteraction = viewModel::reportInteraction,
|
updateSkipIndicator = updateSkipIndicator,
|
||||||
oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause,
|
skipBackOnResume = preferences.appPreferences.playbackPreferences.skipBackOnResume,
|
||||||
onStop = {
|
onInteraction = viewModel::reportInteraction,
|
||||||
player.stop()
|
oneClickPause = preferences.appPreferences.playbackPreferences.oneClickPause,
|
||||||
viewModel.navigationManager.goBack()
|
onStop = {
|
||||||
},
|
player.stop()
|
||||||
onPlaybackDialogTypeClick = { playbackDialog = it },
|
viewModel.navigationManager.goBack()
|
||||||
)
|
},
|
||||||
|
onPlaybackDialogTypeClick = { playbackDialog = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val onPlaybackActionClick: (PlaybackAction) -> Unit = {
|
val onPlaybackActionClick: (PlaybackAction) -> Unit = {
|
||||||
when (it) {
|
when (it) {
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ import androidx.compose.ui.focus.focusProperties
|
||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.focus.focusRestorer
|
import androidx.compose.ui.focus.focusRestorer
|
||||||
import androidx.compose.ui.focus.onFocusChanged
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.Player
|
import androidx.media3.common.Player
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
|
|
@ -47,6 +49,7 @@ fun ChapterRowOverlay(
|
||||||
onChangeState: (OverlayViewState) -> Unit,
|
onChangeState: (OverlayViewState) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
val chapterInteractionSources =
|
val chapterInteractionSources =
|
||||||
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
|
remember(chapters.size) { List(chapters.size) { MutableInteractionSource() } }
|
||||||
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
val bringIntoViewRequester = remember { BringIntoViewRequester() }
|
||||||
|
|
@ -135,7 +138,10 @@ fun ChapterRowOverlay(
|
||||||
index == 0,
|
index == 0,
|
||||||
Modifier.focusProperties {
|
Modifier.focusProperties {
|
||||||
// Prevent scrolling left on first card to prevent moving down
|
// Prevent scrolling left on first card to prevent moving down
|
||||||
left = FocusRequester.Cancel
|
left =
|
||||||
|
if (isLtr) FocusRequester.Cancel else FocusRequester.Default
|
||||||
|
right =
|
||||||
|
if (isLtr) FocusRequester.Default else FocusRequester.Cancel
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -42,10 +42,12 @@ import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.isSpecified
|
import androidx.compose.ui.graphics.isSpecified
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.window.Dialog
|
import androidx.compose.ui.window.Dialog
|
||||||
|
|
@ -368,12 +370,13 @@ fun PlaybackButtons(
|
||||||
seekForward: Duration,
|
seekForward: Duration,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier.focusGroup(),
|
modifier = modifier.focusGroup(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
horizontalArrangement = Arrangement.spacedBy(buttonSpacing),
|
||||||
) {
|
) {
|
||||||
PlaybackButton(
|
PlaybackButton(
|
||||||
iconRes = R.drawable.baseline_skip_previous_24,
|
iconRes = if (isLtr) R.drawable.baseline_skip_previous_24 else R.drawable.baseline_skip_next_24,
|
||||||
onClick = {
|
onClick = {
|
||||||
onControllerInteraction.invoke()
|
onControllerInteraction.invoke()
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.Previous)
|
onPlaybackActionClick.invoke(PlaybackAction.Previous)
|
||||||
|
|
@ -382,7 +385,7 @@ fun PlaybackButtons(
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
)
|
)
|
||||||
PlaybackButton(
|
PlaybackButton(
|
||||||
iconRes = R.drawable.baseline_fast_rewind_24,
|
iconRes = if (isLtr) R.drawable.baseline_fast_rewind_24 else R.drawable.baseline_fast_forward_24,
|
||||||
onClick = {
|
onClick = {
|
||||||
onControllerInteraction.invoke()
|
onControllerInteraction.invoke()
|
||||||
player.seekBack(seekBack)
|
player.seekBack(seekBack)
|
||||||
|
|
@ -406,7 +409,7 @@ fun PlaybackButtons(
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
)
|
)
|
||||||
PlaybackButton(
|
PlaybackButton(
|
||||||
iconRes = R.drawable.baseline_fast_forward_24,
|
iconRes = if (isLtr) R.drawable.baseline_fast_forward_24 else R.drawable.baseline_fast_rewind_24,
|
||||||
onClick = {
|
onClick = {
|
||||||
onControllerInteraction.invoke()
|
onControllerInteraction.invoke()
|
||||||
player.seekForward(seekForward)
|
player.seekForward(seekForward)
|
||||||
|
|
@ -414,7 +417,7 @@ fun PlaybackButtons(
|
||||||
onControllerInteraction = onControllerInteraction,
|
onControllerInteraction = onControllerInteraction,
|
||||||
)
|
)
|
||||||
PlaybackButton(
|
PlaybackButton(
|
||||||
iconRes = R.drawable.baseline_skip_next_24,
|
iconRes = if (isLtr) R.drawable.baseline_skip_next_24 else R.drawable.baseline_skip_previous_24,
|
||||||
onClick = {
|
onClick = {
|
||||||
onControllerInteraction.invoke()
|
onControllerInteraction.invoke()
|
||||||
onPlaybackActionClick.invoke(PlaybackAction.Next)
|
onPlaybackActionClick.invoke(PlaybackAction.Next)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ package com.github.damontecres.wholphin.ui.playback.overlay
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
|
@ -44,11 +43,16 @@ import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.input.key.KeyEventType
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
import androidx.compose.ui.input.key.type
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.tv.material3.MaterialTheme
|
import androidx.tv.material3.MaterialTheme
|
||||||
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
import com.github.damontecres.wholphin.ui.playback.ControllerViewState
|
||||||
import com.github.damontecres.wholphin.ui.playback.calculateSeekAccelerationMultiplier
|
import com.github.damontecres.wholphin.ui.playback.calculateSeekAccelerationMultiplier
|
||||||
|
import com.github.damontecres.wholphin.ui.playback.isDpadLeft
|
||||||
|
import com.github.damontecres.wholphin.ui.playback.isDpadRight
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import timber.log.Timber
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -174,6 +178,7 @@ private fun SeekBarDisplay(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
) {
|
) {
|
||||||
|
val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr
|
||||||
val color = MaterialTheme.colorScheme.border
|
val color = MaterialTheme.colorScheme.border
|
||||||
val onSurface = MaterialTheme.colorScheme.onSurface
|
val onSurface = MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
|
|
@ -192,70 +197,75 @@ private fun SeekBarDisplay(
|
||||||
.height(animatedIndicatorHeight)
|
.height(animatedIndicatorHeight)
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.onPreviewKeyEvent { event ->
|
.onPreviewKeyEvent { event ->
|
||||||
when (event.nativeKeyEvent.keyCode) {
|
if (!isDpadLeft(event) && !isDpadRight(event)) {
|
||||||
KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_LEFT -> {
|
Timber.v("Ignoring %s", event)
|
||||||
when (event.type) {
|
return@onPreviewKeyEvent false
|
||||||
KeyEventType.KeyDown -> {
|
}
|
||||||
val repeatCount = event.nativeKeyEvent.repeatCount
|
val seekBack =
|
||||||
if (repeatCount > 0) {
|
if (isLtr) {
|
||||||
leftHandledByRepeat = true
|
isDpadLeft(event)
|
||||||
onLeft.invoke(
|
} else {
|
||||||
calculateSeekAccelerationMultiplier(
|
isDpadRight(event)
|
||||||
repeatCount = repeatCount,
|
}
|
||||||
durationMs = durationMs,
|
if (seekBack) {
|
||||||
),
|
when (event.type) {
|
||||||
)
|
KeyEventType.KeyDown -> {
|
||||||
} else {
|
val repeatCount = event.nativeKeyEvent.repeatCount
|
||||||
leftHandledByRepeat = false
|
if (repeatCount > 0) {
|
||||||
}
|
leftHandledByRepeat = true
|
||||||
}
|
onLeft.invoke(
|
||||||
|
calculateSeekAccelerationMultiplier(
|
||||||
KeyEventType.KeyUp -> {
|
repeatCount = repeatCount,
|
||||||
if (!leftHandledByRepeat) {
|
durationMs = durationMs,
|
||||||
onLeft.invoke(1)
|
),
|
||||||
}
|
)
|
||||||
|
} else {
|
||||||
leftHandledByRepeat = false
|
leftHandledByRepeat = false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
|
||||||
return@onPreviewKeyEvent false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return@onPreviewKeyEvent true
|
|
||||||
}
|
|
||||||
|
|
||||||
KeyEvent.KEYCODE_DPAD_RIGHT, KeyEvent.KEYCODE_SYSTEM_NAVIGATION_RIGHT -> {
|
KeyEventType.KeyUp -> {
|
||||||
when (event.type) {
|
if (!leftHandledByRepeat) {
|
||||||
KeyEventType.KeyDown -> {
|
onLeft.invoke(1)
|
||||||
val repeatCount = event.nativeKeyEvent.repeatCount
|
|
||||||
if (repeatCount > 0) {
|
|
||||||
rightHandledByRepeat = true
|
|
||||||
onRight.invoke(
|
|
||||||
calculateSeekAccelerationMultiplier(
|
|
||||||
repeatCount = repeatCount,
|
|
||||||
durationMs = durationMs,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
rightHandledByRepeat = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
leftHandledByRepeat = false
|
||||||
|
}
|
||||||
|
|
||||||
KeyEventType.KeyUp -> {
|
else -> {
|
||||||
if (!rightHandledByRepeat) {
|
return@onPreviewKeyEvent false
|
||||||
onRight.invoke(1)
|
}
|
||||||
}
|
}
|
||||||
|
return@onPreviewKeyEvent true
|
||||||
|
} else {
|
||||||
|
when (event.type) {
|
||||||
|
KeyEventType.KeyDown -> {
|
||||||
|
val repeatCount = event.nativeKeyEvent.repeatCount
|
||||||
|
if (repeatCount > 0) {
|
||||||
|
rightHandledByRepeat = true
|
||||||
|
onRight.invoke(
|
||||||
|
calculateSeekAccelerationMultiplier(
|
||||||
|
repeatCount = repeatCount,
|
||||||
|
durationMs = durationMs,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
rightHandledByRepeat = false
|
rightHandledByRepeat = false
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
|
||||||
return@onPreviewKeyEvent false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return@onPreviewKeyEvent true
|
|
||||||
|
KeyEventType.KeyUp -> {
|
||||||
|
if (!rightHandledByRepeat) {
|
||||||
|
onRight.invoke(1)
|
||||||
|
}
|
||||||
|
rightHandledByRepeat = false
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
return@onPreviewKeyEvent false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return@onPreviewKeyEvent true
|
||||||
}
|
}
|
||||||
false
|
|
||||||
}.focusable(enabled = enabled, interactionSource = interactionSource),
|
}.focusable(enabled = enabled, interactionSource = interactionSource),
|
||||||
onDraw = {
|
onDraw = {
|
||||||
val yOffset = size.height.div(2)
|
val yOffset = size.height.div(2)
|
||||||
|
|
@ -266,33 +276,70 @@ private fun SeekBarDisplay(
|
||||||
strokeWidth = size.height,
|
strokeWidth = size.height,
|
||||||
cap = StrokeCap.Round,
|
cap = StrokeCap.Round,
|
||||||
)
|
)
|
||||||
drawLine(
|
if (isLtr) {
|
||||||
color = onSurface.copy(alpha = .65f),
|
drawLine(
|
||||||
start = Offset(x = 0f, y = yOffset),
|
color = onSurface.copy(alpha = .65f),
|
||||||
end =
|
start = Offset(x = 0f, y = yOffset),
|
||||||
Offset(
|
end =
|
||||||
x = size.width.times(bufferedProgress),
|
Offset(
|
||||||
y = yOffset,
|
x = size.width.times(bufferedProgress),
|
||||||
),
|
y = yOffset,
|
||||||
strokeWidth = size.height,
|
),
|
||||||
cap = StrokeCap.Round,
|
strokeWidth = size.height,
|
||||||
)
|
cap = StrokeCap.Round,
|
||||||
drawLine(
|
)
|
||||||
color = color,
|
drawLine(
|
||||||
start = Offset(x = 0f, y = yOffset),
|
color = color,
|
||||||
end =
|
start = Offset(x = 0f, y = yOffset),
|
||||||
Offset(
|
end =
|
||||||
// x = size.width.times(if (isSelected) seekProgress else progress),
|
Offset(
|
||||||
x = size.width.times(progress),
|
x = size.width.times(progress),
|
||||||
y = yOffset,
|
y = yOffset,
|
||||||
),
|
),
|
||||||
strokeWidth = size.height,
|
strokeWidth = size.height,
|
||||||
cap = StrokeCap.Round,
|
cap = StrokeCap.Round,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
drawLine(
|
||||||
|
color = onSurface.copy(alpha = .65f),
|
||||||
|
start =
|
||||||
|
Offset(
|
||||||
|
x = size.width - size.width.times(bufferedProgress),
|
||||||
|
y = yOffset,
|
||||||
|
),
|
||||||
|
end =
|
||||||
|
Offset(
|
||||||
|
x = size.width,
|
||||||
|
y = yOffset,
|
||||||
|
),
|
||||||
|
strokeWidth = size.height,
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
)
|
||||||
|
drawLine(
|
||||||
|
color = color,
|
||||||
|
start = Offset(x = size.width - size.width.times(progress), y = yOffset),
|
||||||
|
end =
|
||||||
|
Offset(
|
||||||
|
x = size.width,
|
||||||
|
y = yOffset,
|
||||||
|
),
|
||||||
|
strokeWidth = size.height,
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
)
|
||||||
|
}
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = Color.White,
|
color = Color.White,
|
||||||
radius = size.height + 2,
|
radius = size.height + 2,
|
||||||
center = Offset(x = size.width.times(progress), y = yOffset),
|
center =
|
||||||
|
Offset(
|
||||||
|
x =
|
||||||
|
if (isLtr) {
|
||||||
|
size.width.times(progress)
|
||||||
|
} else {
|
||||||
|
size.width - size.width.times(progress)
|
||||||
|
},
|
||||||
|
y = yOffset,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue