-
Notifications
You must be signed in to change notification settings - Fork 162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Video player controller #3959
Video player controller #3959
Changes from all commits
98875a6
7a8b942
9c1ccdf
28ed092
c8519b3
6213402
9a6b398
5686b77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/* | ||
* Copyright 2024 New Vector Ltd. | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-only | ||
* Please see LICENSE in the repository root for full details. | ||
*/ | ||
|
||
package io.element.android.libraries.dateformatter.api | ||
|
||
import java.util.Locale | ||
|
||
/** | ||
* Convert milliseconds to human readable duration. | ||
* Hours in 1 digit or more. | ||
* Minutes in 2 digits when hours are available. | ||
* Seconds always on 2 digits. | ||
* Example: | ||
* - when the duration is longer than 1 hour: | ||
* - "10:23:34" | ||
* - "1:23:34" | ||
* - "1:03:04" | ||
* - when the duration is shorter: | ||
* - "4:56" | ||
* - "14:06" | ||
* - Less than one minute: | ||
* - "0:00" | ||
* - "0:01" | ||
* - "0:59" | ||
*/ | ||
fun Long.toHumanReadableDuration(): String { | ||
val inSeconds = this / 1_000 | ||
val hours = inSeconds / 3_600 | ||
val minutes = inSeconds % 3_600 / 60 | ||
val seconds = inSeconds % 60 | ||
return if (hours > 0) { | ||
String.format(Locale.US, "%d:%02d:%02d", hours, minutes, seconds) | ||
} else { | ||
String.format(Locale.US, "%d:%02d", minutes, seconds) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/* | ||
* Copyright 2024 New Vector Ltd. | ||
* | ||
* SPDX-License-Identifier: AGPL-3.0-only | ||
* Please see LICENSE in the repository root for full details. | ||
*/ | ||
|
||
package io.element.android.libraries.dateformatter.api | ||
|
||
import com.google.common.truth.Truth.assertThat | ||
import org.junit.Test | ||
|
||
class DurationFormatterTest { | ||
@Test | ||
fun `format seconds only`() { | ||
assertThat(buildDuration().toHumanReadableDuration()).isEqualTo("0:00") | ||
assertThat(buildDuration(seconds = 1).toHumanReadableDuration()).isEqualTo("0:01") | ||
assertThat(buildDuration(seconds = 59).toHumanReadableDuration()).isEqualTo("0:59") | ||
} | ||
|
||
@Test | ||
fun `format minutes and seconds`() { | ||
assertThat(buildDuration(minutes = 1).toHumanReadableDuration()).isEqualTo("1:00") | ||
assertThat(buildDuration(minutes = 1, seconds = 30).toHumanReadableDuration()).isEqualTo("1:30") | ||
assertThat(buildDuration(minutes = 59, seconds = 59).toHumanReadableDuration()).isEqualTo("59:59") | ||
} | ||
|
||
@Test | ||
fun `format hours, minutes and seconds`() { | ||
assertThat(buildDuration(hours = 1).toHumanReadableDuration()).isEqualTo("1:00:00") | ||
assertThat(buildDuration(hours = 1, minutes = 1, seconds = 1).toHumanReadableDuration()).isEqualTo("1:01:01") | ||
assertThat(buildDuration(hours = 24, minutes = 59, seconds = 59).toHumanReadableDuration()).isEqualTo("24:59:59") | ||
assertThat(buildDuration(hours = 25, minutes = 0, seconds = 0).toHumanReadableDuration()).isEqualTo("25:00:00") | ||
} | ||
|
||
private fun buildDuration( | ||
hours: Int = 0, | ||
minutes: Int = 0, | ||
seconds: Int = 0 | ||
): Long { | ||
return (hours * 60 * 60 + minutes * 60 + seconds) * 1000L | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,19 +5,32 @@ | |
* Please see LICENSE in the repository root for full details. | ||
*/ | ||
|
||
@file:OptIn(ExperimentalMaterial3Api::class) | ||
|
||
package io.element.android.libraries.designsystem.theme.components | ||
|
||
import androidx.compose.foundation.interaction.DragInteraction | ||
import androidx.compose.foundation.interaction.MutableInteractionSource | ||
import androidx.compose.foundation.interaction.PressInteraction | ||
import androidx.compose.foundation.layout.Column | ||
import androidx.compose.foundation.layout.height | ||
import androidx.compose.material3.ExperimentalMaterial3Api | ||
import androidx.compose.material3.SliderColors | ||
import androidx.compose.material3.SliderDefaults | ||
import androidx.compose.runtime.Composable | ||
import androidx.compose.runtime.LaunchedEffect | ||
import androidx.compose.runtime.getValue | ||
import androidx.compose.runtime.mutableFloatStateOf | ||
import androidx.compose.runtime.mutableStateOf | ||
import androidx.compose.runtime.remember | ||
import androidx.compose.runtime.setValue | ||
import androidx.compose.ui.Modifier | ||
import androidx.compose.ui.draw.drawWithContent | ||
import androidx.compose.ui.graphics.Color | ||
import androidx.compose.ui.tooling.preview.Preview | ||
import androidx.compose.ui.unit.DpSize | ||
import androidx.compose.ui.unit.dp | ||
import io.element.android.compound.theme.ElementTheme | ||
import io.element.android.libraries.designsystem.preview.ElementThemedPreview | ||
import io.element.android.libraries.designsystem.preview.PreviewGroup | ||
|
||
|
@@ -32,8 +45,20 @@ | |
steps: Int = 0, | ||
onValueChangeFinish: (() -> Unit)? = null, | ||
colors: SliderColors = SliderDefaults.colors(), | ||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } | ||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, | ||
useCustomLayout: Boolean = false, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This feels a bit weird, to be honest. I think we should either split this component into two (one for the new M3 look and feel and another for the old one) or rename/clarify a bit what There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, it's not ideal. I'll confirm with Aaron that the design is up to date for Android first. |
||
) { | ||
val thumbColor = ElementTheme.colors.iconOnSolidPrimary | ||
var isUserInteracting by remember { mutableStateOf(false) } | ||
LaunchedEffect(interactionSource) { | ||
interactionSource.interactions.collect { interaction -> | ||
isUserInteracting = when (interaction) { | ||
Check warning on line 55 in libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt Codecov / codecov/patchlibraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt#L55
|
||
is DragInteraction.Start, | ||
is PressInteraction.Press -> true | ||
else -> false | ||
Check warning on line 58 in libraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt Codecov / codecov/patchlibraries/designsystem/src/main/kotlin/io/element/android/libraries/designsystem/theme/components/Slider.kt#L58
|
||
} | ||
} | ||
} | ||
androidx.compose.material3.Slider( | ||
value = value, | ||
onValueChange = onValueChange, | ||
|
@@ -43,6 +68,54 @@ | |
steps = steps, | ||
onValueChangeFinished = onValueChangeFinish, | ||
colors = colors, | ||
thumb = { | ||
if (useCustomLayout) { | ||
SliderDefaults.Thumb( | ||
modifier = Modifier.drawWithContent { | ||
drawContent() | ||
if (isUserInteracting.not()) { | ||
drawCircle(thumbColor, radius = 8.dp.toPx()) | ||
} | ||
}, | ||
interactionSource = interactionSource, | ||
colors = colors.copy( | ||
thumbColor = ElementTheme.colors.iconPrimary, | ||
), | ||
enabled = enabled, | ||
thumbSize = DpSize( | ||
if (isUserInteracting) 44.dp else 22.dp, | ||
22.dp, | ||
), | ||
) | ||
} else { | ||
SliderDefaults.Thumb( | ||
interactionSource = interactionSource, | ||
colors = colors, | ||
enabled = enabled | ||
) | ||
} | ||
}, | ||
track = { sliderState -> | ||
if (useCustomLayout) { | ||
SliderDefaults.Track( | ||
modifier = Modifier.height(8.dp), | ||
colors = colors.copy( | ||
activeTrackColor = Color(0x66E0EDFF), | ||
inactiveTrackColor = Color(0x66E0EDFF), | ||
Comment on lines
+103
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't we have either semantic or core token colors for these? |
||
), | ||
enabled = enabled, | ||
sliderState = sliderState, | ||
thumbTrackGapSize = 0.dp, | ||
drawStopIndicator = { }, | ||
) | ||
} else { | ||
SliderDefaults.Track( | ||
colors = colors, | ||
enabled = enabled, | ||
sliderState = sliderState, | ||
) | ||
} | ||
}, | ||
interactionSource = interactionSource, | ||
) | ||
} | ||
|
@@ -55,5 +128,6 @@ | |
Slider(onValueChange = { value = it }, value = value, enabled = true) | ||
Slider(steps = 10, onValueChange = { value = it }, value = value, enabled = true) | ||
Slider(onValueChange = { value = it }, value = value, enabled = false) | ||
Slider(onValueChange = { value = it }, value = value, enabled = true, useCustomLayout = true) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was going to suggest only calculating the
hours
ifinSeconds > 3600
but it seems like an overkill to just avoid a single allocation and a division 😅 .