diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index bf5cefe95..d8da6056a 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -25,11 +25,6 @@ jobs: with: java-version: '17' distribution: 'temurin' - - name: Set up Gradle - uses: gradle/actions/setup-gradle@v4 - with: - cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - cache-read-only: true - name: Build project run: > ./gradlew diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 172f13cfa..83a2530cf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ android-gradle-plugin = "8.7.3" androidx-activity = "1.9.3" androidx-annotation = "1.9.1" androidx-compose = "2024.11.00" -androidx-compose-material-navigation = "1.7.0-beta01" # TODO Remove this once https://issuetracker.google.com/issues/347719428 is resolved androidx-core = "1.15.0" androidx-datastore = "1.1.1" androidx-fragment = "1.8.5" @@ -137,7 +136,6 @@ androidx-compose-material3 = { group = "androidx.compose.material3", name = "mat androidx-compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class" } androidx-compose-material-icons-core = { module = "androidx.compose.material:material-icons-core" } androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } -androidx-compose-material-navigation = { module = "androidx.compose.material:material-navigation", version.ref = "androidx-compose-material-navigation" } androidx-compose-runtime = { module = "androidx.compose.runtime:runtime" } androidx-compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable" } androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-test-ext-junit" } diff --git a/pillarbox-demo-shared/build.gradle.kts b/pillarbox-demo-shared/build.gradle.kts index 2ed86c8eb..937816ba4 100644 --- a/pillarbox-demo-shared/build.gradle.kts +++ b/pillarbox-demo-shared/build.gradle.kts @@ -28,6 +28,8 @@ dependencies { api(libs.androidx.compose.ui.text) implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.compose.ui.unit) + implementation(libs.androidx.core) + implementation(libs.androidx.core.ktx) api(libs.androidx.datastore.core) api(libs.androidx.datastore.preferences) api(libs.androidx.datastore.preferences.core) @@ -38,6 +40,7 @@ dependencies { api(libs.androidx.navigation.runtime) implementation(libs.androidx.paging.common) api(libs.kotlinx.coroutines.core) + api(libs.kotlinx.datetime) api(libs.kotlinx.serialization.core) implementation(libs.okhttp) api(libs.srg.data) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt index d941e125f..c621036ac 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt @@ -60,10 +60,11 @@ data class Playlist(val title: String, val items: List, val languageTa imageUri = "https://www.rts.ch/2022/08/18/12/38/13317144.image/16x9", languageTag = "fr-CH", ), + // urn:swi:video:48498670 DemoItem.URL( title = "Swiss wheelchair athlete wins top award", uri = "https://cdn.prod.swi-services.ch/video-projects/94f5f5d1-5d53-4336-afda-9198462c45d9/localised-videos/ENG/renditions/ENG.mp4", - description = "VOD - MP4 (urn:swi:video:48498670)", + description = "VOD - MP4", imageUri = "https://cdn.prod.swi-services.ch/video-delivery/images/94f5f5d1-5d53-4336-afda-9198462c45d9/_.1hAGinujJ.yERGrrGNzBGCNSxmhKZT/16x9", languageTag = "en-CH", ), diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt new file mode 100644 index 000000000..6e48ee580 --- /dev/null +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/TalkBack.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.shared.ui + +import android.view.accessibility.AccessibilityManager +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.getSystemService + +/** + * Remembers the current touch exploration state and provides it as a Composable state. + * + * @return A boolean indicating whether touch exploration is currently enabled. + */ +@Composable +fun rememberIsTouchExplorationEnabled(): Boolean { + val accessibilityManager = LocalContext.current.getSystemService() ?: return false + val (isTouchExplorationEnabled, setIsTouchExplorationEnabled) = remember { + mutableStateOf(accessibilityManager.isTouchExplorationEnabled) + } + + DisposableEffect(Unit) { + val callback = AccessibilityManager.TouchExplorationStateChangeListener(setIsTouchExplorationEnabled) + + accessibilityManager.addTouchExplorationStateChangeListener(callback) + + onDispose { + accessibilityManager.removeTouchExplorationStateChangeListener(callback) + } + } + + return isTouchExplorationEnabled +} diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt index 6bda61b55..45f71f773 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/components/PillarboxSlider.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.focusable import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.Interaction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.PressInteraction import androidx.compose.foundation.isSystemInDarkTheme @@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable @@ -33,11 +35,15 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.semantics.ProgressBarRangeInfo +import androidx.compose.ui.semantics.progressBarRangeInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import ch.srgssr.pillarbox.demo.shared.extension.onDpadEvent +import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTouchExplorationEnabled import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_inverseSurface import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_onSurface import ch.srgssr.pillarbox.demo.shared.ui.theme.md_theme_dark_primary @@ -96,7 +102,12 @@ fun PillarboxSlider( PillarboxSliderInternal( activeTrackWeight = value / range.last.toFloat(), compactMode = compactMode, - modifier = modifier, + modifier = modifier.semantics { + progressBarRangeInfo = ProgressBarRangeInfo( + current = value.toFloat(), + range = range.first.toFloat()..range.last.toFloat(), + ) + }, secondaryValueWeight = secondaryValue?.let { it / range.last.toFloat() }, enabled = enabled, thumbColorEnabled = thumbColorEnabled, @@ -166,7 +177,12 @@ fun PillarboxSlider( PillarboxSliderInternal( activeTrackWeight = value / range.endInclusive, compactMode = compactMode, - modifier = modifier, + modifier = modifier.semantics { + progressBarRangeInfo = ProgressBarRangeInfo( + current = value.toFloat(), + range = range.start..range.endInclusive, + ) + }, secondaryValueWeight = secondaryValue?.let { it / range.endInclusive }, enabled = enabled, thumbColorEnabled = thumbColorEnabled, @@ -208,8 +224,11 @@ private fun PillarboxSliderInternal( onSeekBack: () -> Unit, onSeekForward: () -> Unit, ) { + val isTouchExplorationEnabled = rememberIsTouchExplorationEnabled() + val compactMode = compactMode && !isTouchExplorationEnabled val seekBarHeight by animateDpAsState(targetValue = if (compactMode) 8.dp else 16.dp, label = "seek_bar_height") val thumbColor by animateColorAsState(targetValue = if (enabled) thumbColorEnabled else thumbColorDisabled, label = "thumb_color") + val verticalPadding by animateDpAsState(targetValue = if (isTouchExplorationEnabled) 48.dp - seekBarHeight else 0.dp, label = "padding_top") val animatedActiveTrackWeight by animateFloatAsState(targetValue = activeTrackWeight, label = "active_track_weight") val activeTrackColor by animateColorAsState( @@ -230,6 +249,8 @@ private fun PillarboxSliderInternal( Row( modifier = modifier + .semantics(mergeDescendants = true) {} + .padding(vertical = verticalPadding) .height(seekBarHeight) .then( if (interactionSource != null) { @@ -257,7 +278,7 @@ private fun PillarboxSliderInternal( Thumb( color = thumbColor, - enabled = enabled, + enabled = enabled && !isTouchExplorationEnabled, onSeekBack = onSeekBack, onSeekForward = onSeekForward, ) diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt index 46916e11d..72d5a5a55 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/MetricsOverlay.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shadow +import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.tooling.preview.Preview import androidx.media3.common.Format import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions @@ -31,7 +32,9 @@ fun MetricsOverlay( ) { val currentVideoFormat = playbackMetrics.videoFormat val currentAudioFormat = playbackMetrics.audioFormat - Column(modifier = modifier) { + Column( + modifier = modifier.clearAndSetSemantics {}, + ) { currentVideoFormat?.let { OverlayText( overlayOptions = overlayOptions, diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt index 474b7b7d8..435962157 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/player/metrics/StatsForNerdsViewModel.kt @@ -95,7 +95,7 @@ class StatsForNerdsViewModel(application: Application) : AndroidViewModel(applic getSessionInformation( labelRes = R.string.video_size, value = if (value.videoSize != VideoSize.UNKNOWN) { - "${value.videoSize.width}x${value.videoSize.height}" + "${value.videoSize.width}×${value.videoSize.height}" } else { null } diff --git a/pillarbox-demo-shared/src/main/res/values/strings.xml b/pillarbox-demo-shared/src/main/res/values/strings.xml index 7f5ab6d58..eb2e8ea4d 100644 --- a/pillarbox-demo-shared/src/main/res/values/strings.xml +++ b/pillarbox-demo-shared/src/main/res/values/strings.xml @@ -11,6 +11,10 @@ Search for content No results Enter something to search + Audio + Video + Controls visible + Controls hidden %1$d min Settings Audio tracks @@ -26,7 +30,7 @@ DRM loading Total load time Information - Session id + Session ID URI Playback duration Data volume diff --git a/pillarbox-demo-tv/build.gradle.kts b/pillarbox-demo-tv/build.gradle.kts index 892f6dba2..18e7caf88 100644 --- a/pillarbox-demo-tv/build.gradle.kts +++ b/pillarbox-demo-tv/build.gradle.kts @@ -48,6 +48,7 @@ dependencies { implementation(libs.coil.network.okhttp) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.datetime) implementation(libs.kotlinx.serialization.core) implementation(libs.okhttp) implementation(libs.srg.data) diff --git a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt index cb1e4ebb8..0a8ed1153 100644 --- a/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt +++ b/pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/ui/player/compose/settings/PlaybackSettingsDrawer.kt @@ -383,7 +383,7 @@ private fun NavigationDrawerScope.TracksSetting( is VideoTrack -> { val text = buildString { append(format.width) - append("x") + append("×") append(format.height) if (format.bitrate > Format.NO_VALUE) { diff --git a/pillarbox-demo/build.gradle.kts b/pillarbox-demo/build.gradle.kts index 873c26083..4acc0fe82 100644 --- a/pillarbox-demo/build.gradle.kts +++ b/pillarbox-demo/build.gradle.kts @@ -58,7 +58,6 @@ dependencies { implementation(libs.androidx.compose.foundation.layout) implementation(libs.androidx.compose.material.icons.core) implementation(libs.androidx.compose.material.icons.extended) - implementation(libs.androidx.compose.material.navigation) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material3.window.size) implementation(libs.androidx.compose.runtime) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt index 782044350..5dec8fa5c 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/components/ContentView.kt @@ -20,6 +20,7 @@ import ch.srg.dataProvider.integrationlayer.data.remote.Type import ch.srg.dataProvider.integrationlayer.data.remote.Vendor import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.Content +import ch.srgssr.pillarbox.demo.shared.ui.rememberIsTouchExplorationEnabled import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme import java.util.Date import kotlin.time.Duration.Companion.seconds @@ -78,9 +79,10 @@ private fun MediaView( languageTag: String? = null, onClick: () -> Unit ) { + val isTouchExplorationEnabled = rememberIsTouchExplorationEnabled() val mediaTypeIcon = when (content.mediaType) { - MediaType.AUDIO -> "🎧" - MediaType.VIDEO -> "🎬" + MediaType.AUDIO -> if (isTouchExplorationEnabled) "${stringResource(R.string.audio_content)} -" else "🎧" + MediaType.VIDEO -> if (isTouchExplorationEnabled) "${stringResource(R.string.video_content)} -" else "🎬" } val subtitlePrefix = if (content.showTitle != null) { "${content.showTitle} - " diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt index f5a908502..d3edfc26a 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/DemoPlayerView.kt @@ -20,15 +20,13 @@ import androidx.compose.foundation.layout.displayCutoutPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.navigation.ModalBottomSheetLayout -import androidx.compose.material.navigation.bottomSheet -import androidx.compose.material.navigation.rememberBottomSheetNavigator +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -39,9 +37,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.Player -import androidx.navigation.compose.NavHost -import androidx.navigation.compose.composable -import androidx.navigation.compose.rememberNavController import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettings import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettingsRepository import ch.srgssr.pillarbox.demo.shared.ui.settings.AppSettingsViewModel @@ -62,7 +57,7 @@ import ch.srgssr.pillarbox.ui.ScaleMode * @param pictureInPictureClick The picture in picture button action. If `null` no button. * @param displayPlaylist If it displays the playlist UI or not. */ -@OptIn(ExperimentalMaterial3WindowSizeClassApi::class) +@OptIn(ExperimentalMaterial3WindowSizeClassApi::class, ExperimentalMaterial3Api::class) @Composable fun DemoPlayerView( player: Player, @@ -100,43 +95,22 @@ fun DemoPlayerView( } } } else { - val bottomSheetNavigator = rememberBottomSheetNavigator() - val navController = rememberNavController(bottomSheetNavigator) + var showSettingsSheet by remember { mutableStateOf(false) } - LaunchedEffect(bottomSheetNavigator.navigatorSheetState.isVisible) { - if (!bottomSheetNavigator.navigatorSheetState.isVisible) { - navController.popBackStack(route = RoutePlayer, false) - } - } - - ModalBottomSheetLayout( - modifier = modifier, - bottomSheetNavigator = bottomSheetNavigator, - ) { - NavHost(navController, startDestination = RoutePlayer) { - composable(route = RoutePlayer) { - PlayerContent( - player = player, - modifier = Modifier.fillMaxSize(), - pictureInPicture = pictureInPicture, - pictureInPictureClick = pictureInPictureClick, - displayPlaylist = displayPlaylist, - ) { - navController.navigate(route = RouteSettings) { - launchSingleTop = true - } - } - } - - bottomSheet(route = RouteSettings) { - LaunchedEffect(pictureInPicture) { - if (pictureInPicture) { - navController.popBackStack() - } - } + PlayerContent( + player = player, + modifier = Modifier.fillMaxSize(), + pictureInPicture = pictureInPicture, + pictureInPictureClick = pictureInPictureClick, + displayPlaylist = displayPlaylist, + optionClicked = { showSettingsSheet = true }, + ) - PlaybackSettingsContent(player = player) - } + if (showSettingsSheet) { + ModalBottomSheet( + onDismissRequest = { showSettingsSheet = false }, + ) { + PlaybackSettingsContent(player = player) } } } @@ -152,14 +126,11 @@ private fun PlayerContent( pictureInPicture: Boolean = false, pictureInPictureClick: (() -> Unit)? = null, displayPlaylist: Boolean = false, - optionClicked: (() -> Unit)? = null + optionClicked: () -> Unit, ) { var fullScreenState by remember { mutableStateOf(false) } - val fullScreenToggle: (Boolean) -> Unit = { fullScreenEnabled -> - fullScreenState = fullScreenEnabled - } val appSettings by appSettingsViewModel.currentAppSettings.collectAsStateWithLifecycle() ShowSystemUi(isShowed = !fullScreenState) Column(modifier = modifier) { @@ -200,7 +171,7 @@ private fun PlayerContent( ) { PlayerBottomToolbar( modifier = Modifier.fillMaxWidth(), - fullScreenClicked = fullScreenToggle, + fullScreenClicked = { fullScreenState = !fullScreenState }, fullScreenEnabled = fullScreenState, pictureInPictureClicked = pictureInPictureClick, optionClicked = optionClicked @@ -217,6 +188,3 @@ private fun PlayerContent( } } } - -private const val RoutePlayer = "player" -private const val RouteSettings = "settings" diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt index 36e11b690..0112777d5 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/PlayerView.kt @@ -19,8 +19,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.zIndex import androidx.media3.common.Player +import ch.srgssr.pillarbox.demo.shared.R import ch.srgssr.pillarbox.demo.shared.ui.player.metrics.MetricsOverlay import ch.srgssr.pillarbox.demo.shared.ui.settings.MetricsOverlayOptions import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerControls @@ -103,9 +107,16 @@ fun PlayerView( visible = controlsVisible ) val currentCredit by player.getCurrentCreditAsState() + val controlsStateDescription = if (visibilityState.isVisible) { + stringResource(R.string.controls_visible) + } else { + stringResource(R.string.controls_hidden) + } ToggleableBox( - modifier = modifier, + modifier = modifier.semantics { + stateDescription = controlsStateDescription + }, toggleable = controlsToggleable, visibilityState = visibilityState, toggleableContent = { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt index ec324e5e0..558bdb80b 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/controls/PlayerBottomToolbar.kt @@ -12,7 +12,6 @@ import androidx.compose.material.icons.filled.PictureInPicture import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.IconToggleButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -32,9 +31,9 @@ import ch.srgssr.pillarbox.demo.shared.R fun PlayerBottomToolbar( fullScreenEnabled: Boolean, modifier: Modifier = Modifier, - fullScreenClicked: ((Boolean) -> Unit)? = null, - pictureInPictureClicked: (() -> Unit)? = null, - optionClicked: (() -> Unit)? = null + fullScreenClicked: () -> Unit, + pictureInPictureClicked: (() -> Unit)?, + optionClicked: () -> Unit, ) { Row(modifier = modifier) { pictureInPictureClicked?.let { @@ -46,33 +45,29 @@ fun PlayerBottomToolbar( ) } } - fullScreenClicked?.let { - IconToggleButton(checked = fullScreenEnabled, onCheckedChange = it) { - if (fullScreenEnabled) { - Icon( - tint = Color.White, - imageVector = Icons.Default.FullscreenExit, - contentDescription = "Exit full screen" - ) - } else { - Icon( - tint = Color.White, - imageVector = Icons.Default.Fullscreen, - contentDescription = "Open in full screen" - ) - } - } - } - optionClicked?.let { - IconButton( - onClick = it - ) { + + IconButton(onClick = fullScreenClicked) { + if (fullScreenEnabled) { + Icon( + tint = Color.White, + imageVector = Icons.Default.FullscreenExit, + contentDescription = "Exit fullscreen" + ) + } else { Icon( tint = Color.White, - imageVector = Icons.Default.Settings, - contentDescription = stringResource(R.string.settings) + imageVector = Icons.Default.Fullscreen, + contentDescription = "Enter fullscreen" ) } } + + IconButton(onClick = optionClicked) { + Icon( + tint = Color.White, + imageVector = Icons.Default.Settings, + contentDescription = stringResource(R.string.settings) + ) + } } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt index f7a4dba56..905fead78 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSettingsContent.kt @@ -9,7 +9,7 @@ import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -21,7 +21,12 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.lifecycle.viewmodel.compose.viewModel import androidx.media3.common.Player import androidx.navigation.compose.NavHost @@ -178,14 +183,19 @@ private fun SettingsHome( settings: List, settingsClicked: (SettingItem) -> Unit, ) { - LazyColumn { - items(items = settings) { setting -> + LazyColumn( + modifier = Modifier.semantics { + collectionInfo = CollectionInfo(rowCount = settings.size, columnCount = 1) + } + ) { + itemsIndexed(items = settings) { index, setting -> SettingsItem( modifier = Modifier.clickable( enabled = true, role = Role.Button, onClick = { settingsClicked(setting) } ), + index = index, title = setting.title, secondaryText = setting.subtitle, imageVector = setting.icon @@ -196,13 +206,21 @@ private fun SettingsHome( @Composable private fun SettingsItem( + index: Int, title: String, imageVector: ImageVector, modifier: Modifier = Modifier, secondaryText: String? = null ) { ListItem( - modifier = modifier, + modifier = modifier.semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, headlineContent = { Text(text = title) }, diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt index f0729792f..5b0902dc0 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/PlaybackSpeedSettings.kt @@ -15,6 +15,11 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import ch.srgssr.pillarbox.demo.shared.ui.player.settings.PlaybackSpeedSetting import ch.srgssr.pillarbox.demo.ui.theme.PillarboxTheme @@ -33,14 +38,27 @@ fun PlaybackSpeedSettings( modifier: Modifier = Modifier, onSpeedSelected: (PlaybackSpeedSetting) -> Unit, ) { - LazyColumn(modifier) { + LazyColumn( + modifier = modifier.semantics { + collectionInfo = CollectionInfo(rowCount = playbackSpeeds.size, columnCount = 1) + }, + ) { itemsIndexed(items = playbackSpeeds) { index, playbackSpeed -> SettingsOptionItem( title = playbackSpeed.speed, enabled = playbackSpeed.isSelected, - modifier = Modifier.toggleable(playbackSpeed.isSelected) { - onSpeedSelected(playbackSpeed) - } + modifier = Modifier + .toggleable(playbackSpeed.isSelected) { + onSpeedSelected(playbackSpeed) + } + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + } ) if (index < playbackSpeeds.lastIndex) { @@ -57,7 +75,7 @@ private fun SettingsOptionItem(title: String, enabled: Boolean, modifier: Modifi headlineContent = { Text(text = title) }, trailingContent = { if (enabled) { - Icon(imageVector = Icons.Default.Check, contentDescription = "enabled") + Icon(imageVector = Icons.Default.Check, contentDescription = null) } } ) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt index a98f857d6..3cf801330 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/player/settings/TrackSelectionSettings.kt @@ -9,7 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.HearingDisabled import androidx.compose.material3.HorizontalDivider @@ -22,6 +22,11 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.CollectionInfo +import androidx.compose.ui.semantics.CollectionItemInfo +import androidx.compose.ui.semantics.collectionInfo +import androidx.compose.ui.semantics.collectionItemInfo +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.media3.common.C import androidx.media3.common.Format @@ -56,12 +61,25 @@ fun TrackSelectionSettings( onTrackClick: (track: Track) -> Unit ) { val itemModifier = Modifier.fillMaxWidth() - LazyColumn(modifier = modifier) { + LazyColumn( + modifier = modifier.semantics { + // Adding 2 for the "Reset to default" and "Disabled" options + collectionInfo = CollectionInfo(rowCount = tracksSetting.tracks.size + 2, columnCount = 1) + }, + ) { item { ListItem( modifier = itemModifier .minimumInteractiveComponentSize() - .clickable { onResetClick() }, + .clickable { onResetClick() } + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = 0, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, headlineContent = { Text( text = stringResource(R.string.reset_to_default) @@ -72,7 +90,15 @@ fun TrackSelectionSettings( } item { SettingsOption( - modifier = itemModifier, + modifier = itemModifier + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = 1, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, selected = tracksSetting.disabled, onClick = onDisabledClick, content = { @@ -81,10 +107,18 @@ fun TrackSelectionSettings( ) HorizontalDivider() } - items(tracksSetting.tracks) { track -> + itemsIndexed(tracksSetting.tracks) { index, track -> val format = track.format SettingsOption( - modifier = itemModifier, + modifier = itemModifier + .semantics { + collectionItemInfo = CollectionItemInfo( + rowIndex = index + 2, + rowSpan = 1, + columnIndex = 1, + columnSpan = 1, + ) + }, selected = track.isSelected, enabled = track.isSupported && !format.isForced(), onClick = { @@ -116,7 +150,7 @@ fun TrackSelectionSettings( is VideoTrack -> { val text = buildString { append(format.width) - append("x") + append("×") append(format.height) if (format.bitrate > Format.NO_VALUE) { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt index 2d3c5b1d9..dfa8656e1 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/ResizablePlayerShowcase.kt @@ -33,6 +33,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontFamily import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.Player @@ -79,8 +81,8 @@ private fun AdaptivePlayer(player: Player, modifier: Modifier = Modifier) { val (heightPercent, setHeightPercent) = remember { mutableFloatStateOf(1f) } BoxWithConstraints(modifier = modifier) { - val playerWidth by animateDpAsState(targetValue = maxWidth * widthPercent, label = "player_width") - val playerHeight by animateDpAsState(targetValue = maxHeight * heightPercent, label = "player_height") + val playerWidth by animateDpAsState(targetValue = this.maxWidth * widthPercent, label = "player_width") + val playerHeight by animateDpAsState(targetValue = this.maxHeight * heightPercent, label = "player_height") Box( modifier = Modifier.size(width = playerWidth, height = playerHeight), @@ -108,12 +110,14 @@ private fun AdaptivePlayer(player: Player, modifier: Modifier = Modifier) { SliderWithLabel( label = "W:", value = widthPercent, + hint = "Width", onValueChange = setWidthPercent, ) SliderWithLabel( label = "H:", value = heightPercent, + hint = "Height", onValueChange = setHeightPercent, ) @@ -138,15 +142,21 @@ private fun SliderWithLabel( modifier: Modifier = Modifier, label: String, value: Float, + hint: String, onValueChange: (Float) -> Unit ) { Row( - modifier = modifier.systemGestureExclusion(), + modifier = modifier + .semantics(mergeDescendants = true) {} + .systemGestureExclusion(), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.small), verticalAlignment = Alignment.CenterVertically, ) { Text( text = label, + modifier = Modifier.semantics { + contentDescription = hint + }, fontFamily = FontFamily.Monospace, ) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt index b0ace9eb8..5ee60dad1 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/SmoothSeekingShowcase.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch @@ -27,6 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics import androidx.lifecycle.compose.LifecycleStartEffect import androidx.media3.common.Player import ch.srgssr.pillarbox.core.business.PillarboxExoPlayer @@ -96,19 +98,21 @@ fun SmoothSeekingShowcase() { } Row( modifier = Modifier + .semantics(mergeDescendants = true) {} .fillMaxWidth() - .padding(horizontal = MaterialTheme.paddings.baseline), - horizontalArrangement = Arrangement.spacedBy(MaterialTheme.paddings.baseline), + .toggleable(smoothSeekingEnabled) { + smoothSeekingEnabled = it + } + .padding(MaterialTheme.paddings.baseline), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { + Text(text = stringResource(R.string.smooth_seeking_example)) + Switch( checked = smoothSeekingEnabled, - onCheckedChange = { enabled -> - smoothSeekingEnabled = enabled - } + onCheckedChange = null, ) - - Text(text = stringResource(id = R.string.smooth_seeking_example)) } } LifecycleStartEffect(Unit) { diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt index c97e9d1c2..3e35bf01a 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/TrackingToggleShowcase.kt @@ -5,11 +5,13 @@ package ch.srgssr.pillarbox.demo.ui.showcases.misc import android.content.res.Configuration +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -23,9 +25,11 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.semantics.semantics import ch.srgssr.pillarbox.demo.shared.data.DemoItem import ch.srgssr.pillarbox.demo.shared.di.PlayerModule import ch.srgssr.pillarbox.demo.ui.player.PlayerView +import ch.srgssr.pillarbox.demo.ui.theme.paddings /** * Tracking toggle sample @@ -67,11 +71,23 @@ fun TrackingToggleShowcase() { modifier = playerModifier ) - Row(modifier = Modifier.wrapContentSize(), verticalAlignment = Alignment.CenterVertically) { - Text(text = "Toggle tracking", color = MaterialTheme.colorScheme.onBackground) - Switch(checked = trackingEnabled, onCheckedChange = { - trackingEnabled = it - }) + Row( + modifier = Modifier + .semantics(mergeDescendants = true) {} + .fillMaxWidth() + .toggleable(trackingEnabled) { + trackingEnabled = it + } + .padding(MaterialTheme.paddings.baseline), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text(text = "Toggle tracking") + + Switch( + checked = trackingEnabled, + onCheckedChange = null, + ) } } }