From ac14a7d429161a66b9d700afde23b7606215805e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 14 Oct 2026 13:48:02 +0000 Subject: [PATCH 1/2] Move UI hosting and menu infrastructure to android-ui-common Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 1 + ui-common/build.gradle.kts | 2 + .../com/theoplayer/android/ui/MenuScope.kt | 38 +++ .../theoplayer/android/ui/PlayerContainer.kt | 81 ++++++ .../android/ui/THEOplayerViewLifecycle.kt | 124 ++++++++++ .../android/ui/UIControllerScope.kt | 72 ++++++ .../java/com/theoplayer/android/ui/Menu.kt | 35 --- .../com/theoplayer/android/ui/UIController.kt | 233 ------------------ 8 files changed, 318 insertions(+), 268 deletions(-) create mode 100644 ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt create mode 100644 ui-common/src/main/java/com/theoplayer/android/ui/PlayerContainer.kt create mode 100644 ui-common/src/main/java/com/theoplayer/android/ui/THEOplayerViewLifecycle.kt create mode 100644 ui-common/src/main/java/com/theoplayer/android/ui/UIControllerScope.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3c3563ee..4c6f2d45 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ core-pip = "1.0.0-alpha02" androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } androidx-lifecycle-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle-compose" } androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activity-compose" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" } androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3" } diff --git a/ui-common/build.gradle.kts b/ui-common/build.gradle.kts index befb04ad..0104fa31 100644 --- a/ui-common/build.gradle.kts +++ b/ui-common/build.gradle.kts @@ -66,7 +66,9 @@ dependencies { implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ktx) + implementation(libs.androidx.lifecycle.compose) implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.ui.ui) testImplementation(libs.junit4) diff --git a/ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt b/ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt new file mode 100644 index 00000000..b4026823 --- /dev/null +++ b/ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt @@ -0,0 +1,38 @@ +package com.theoplayer.android.ui + +import androidx.compose.runtime.Composable + +/** + * The content for a new menu, typically this should draw a single [Menu]. + */ +typealias MenuContent = @Composable MenuScope.() -> Unit; + +/** + * Scope for the contents of a [Menu]. + */ +interface MenuScope { + /** + * Open a new submenu. + * + * When the submenu is closed, the original menu is shown again. + * + * Only the top-most menu is composed. While the submenu is open, the original menu + * (or the player controls, if no menu was open yet) leaves the composition, and any state + * remembered there is disposed. So instead of passing state from the caller into [menu], + * remember the state inside [menu]: + * ```kotlin + * // Don't: audioTrackState is disposed while the menu is open + * val audioTrackState = rememberAudioTrackState(player) + * openMenu { AudioTrackMenu(audioTrackState) } + * + * // Do + * openMenu { AudioTrackMenu(rememberAudioTrackState(player)) } + * ``` + */ + fun openMenu(menu: MenuContent) + + /** + * Close the currently open menu. + */ + fun closeCurrentMenu() +} diff --git a/ui-common/src/main/java/com/theoplayer/android/ui/PlayerContainer.kt b/ui-common/src/main/java/com/theoplayer/android/ui/PlayerContainer.kt new file mode 100644 index 00000000..2da1f0ad --- /dev/null +++ b/ui-common/src/main/java/com/theoplayer/android/ui/PlayerContainer.kt @@ -0,0 +1,81 @@ +package com.theoplayer.android.ui + +import android.view.ViewGroup +import androidx.compose.foundation.layout.Box +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.theoplayer.android.api.THEOplayerView + +/** + * Hosts [theoplayerView] and draws [ui] inside its UI container. + */ +@InternalUiApi +@Composable +fun PlayerContainer( + modifier: Modifier = Modifier, + theoplayerView: THEOplayerView?, + ui: @Composable () -> Unit +) { + if (theoplayerView == null) { + Box( + modifier = modifier + ) { + ui() + } + } else { + val lifecycle = LocalLifecycleOwner.current.lifecycle + var uiContainer by remember { mutableStateOf(null) } + var composeView by remember { mutableStateOf(null) } + + AndroidView( + modifier = modifier, + factory = { context -> + uiContainer = + theoplayerView.findViewById(com.theoplayer.android.R.id.theo_ui_container) + // Wrap our UI inside a ComposeView + composeView = ComposeView(context).apply { + // When entering fullscreen, we remove the view from its original location + // and add it to the activity's root view. + // When it is temporarily removed (and detached from the window), + // we do *not* want to lose the composition. Therefore, don't use the default + // DisposeOnDetachedFromWindow or DisposeOnDetachedFromWindowOrReleasedFromPool + // strategies! + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnLifecycleDestroyed(lifecycle) + ) + setContent { + ui() + } + } + // Host the THEOplayer view inside our AndroidView + (theoplayerView.parent as? ViewGroup)?.removeView(theoplayerView) + theoplayerView + }) + + // Install inside THEOplayerView's UI container + DisposableEffect(uiContainer, composeView) { + val container = uiContainer + val view = composeView + if (container != null && view != null) { + container.addView(view) + theoplayerView.postInvalidate() + } + onDispose { + if (container != null && view != null) { + container.removeView(view) + view.disposeComposition() + theoplayerView.postInvalidate() + } + } + } + } +} diff --git a/ui-common/src/main/java/com/theoplayer/android/ui/THEOplayerViewLifecycle.kt b/ui-common/src/main/java/com/theoplayer/android/ui/THEOplayerViewLifecycle.kt new file mode 100644 index 00000000..1102afb7 --- /dev/null +++ b/ui-common/src/main/java/com/theoplayer/android/ui/THEOplayerViewLifecycle.kt @@ -0,0 +1,124 @@ +package com.theoplayer.android.ui + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.ContextWrapper +import android.view.View +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalInspectionMode +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +import com.theoplayer.android.api.THEOplayerConfig +import com.theoplayer.android.api.THEOplayerView + +/** + * Creates and remembers a [THEOplayerView] owned by the composition. + * + * This couples the lifecycle of the view to the current activity: it automatically calls + * [THEOplayerView.onPause] and [THEOplayerView.onResume] whenever the current activity is + * [paused][Activity.onPause] or [resumed][Activity.onResume] (resuming an ad that was + * interrupted), and it destroys the view when it leaves the composition. + * + * In previews, the SDK does not initialize the view ([View.isInEditMode]), so do not + * access its [THEOplayerView.player] there. [UIController] and [DefaultUI] handle this + * automatically. + * + * @param config the player configuration + */ +@Composable +fun rememberTHEOplayerView(config: THEOplayerConfig? = null): THEOplayerView { + val context = LocalContext.current + val inspectionMode = LocalInspectionMode.current + val theoplayerView = remember { + THEOplayerView(if (inspectionMode) PreviewContext(context) else context, config) + } + if (!theoplayerView.isInEditMode) { + setupTHEOplayerView(theoplayerView, destroyOnLifecycleDestroy = false) + DisposableEffect(theoplayerView) { + onDispose { + theoplayerView.onDestroy() + } + } + } + return theoplayerView +} + +/** + * A context for creating a [THEOplayerView] in a preview. + * + * The [THEOplayerView] constructor casts the application context to [Application], + * but layoutlib's application context is not an [Application]. + */ +private class PreviewContext(base: Context) : ContextWrapper(base) { + private val application = PreviewApplication(base.applicationContext) + + override fun getApplicationContext(): Context = application +} + +private class PreviewApplication(base: Context) : Application() { + init { + attachBaseContext(base) + } +} + +/** + * Remembers a THEOplayer view for use by a UI module. + */ +@InternalUiApi +@Composable +fun rememberTHEOplayerViewInternal(config: THEOplayerConfig?): THEOplayerView? { + return if (LocalInspectionMode.current) null else rememberTHEOplayerView(config) +} + +/** + * Couples a THEOplayer view to the current lifecycle owner. + */ +@InternalUiApi +@Composable +fun setupTHEOplayerView( + theoplayerView: THEOplayerView, + destroyOnLifecycleDestroy: Boolean = true +): THEOplayerView { + var wasPlayingAd by remember { mutableStateOf(false) } + + val lifecycle = LocalLifecycleOwner.current.lifecycle + DisposableEffect(lifecycle, theoplayerView) { + val lifecycleObserver = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_RESUME -> { + theoplayerView.onResume() + if (wasPlayingAd) { + theoplayerView.player.play() + wasPlayingAd = false + } + } + + Lifecycle.Event.ON_PAUSE -> { + wasPlayingAd = theoplayerView.player.ads.isPlaying + theoplayerView.onPause() + } + + Lifecycle.Event.ON_DESTROY -> { + if (destroyOnLifecycleDestroy) { + theoplayerView.onDestroy() + } + } + else -> {} + } + } + lifecycle.addObserver(lifecycleObserver) + onDispose { + lifecycle.removeObserver(lifecycleObserver) + } + } + + return theoplayerView +} diff --git a/ui-common/src/main/java/com/theoplayer/android/ui/UIControllerScope.kt b/ui-common/src/main/java/com/theoplayer/android/ui/UIControllerScope.kt new file mode 100644 index 00000000..5ac5b51b --- /dev/null +++ b/ui-common/src/main/java/com/theoplayer/android/ui/UIControllerScope.kt @@ -0,0 +1,72 @@ +package com.theoplayer.android.ui + +import androidx.compose.runtime.mutableStateListOf +import com.theoplayer.android.api.THEOplayerView + +/** + * Scope for the contents of a [UIController]. + */ +interface UIControllerScope : MenuScope { + /** + * The player hosted in this [UIController]. + * + * If the UIController was created from a [THEOplayerView], the [Player] is created on + * first access (and only then adds its listeners). Prefer [theoplayerView] with the + * experimental state holders instead. + */ + val player: Player + + /** + * The THEOplayer view hosted in this [UIController], or `null` in inspection mode. + */ + val theoplayerView: THEOplayerView? + get() = player.theoplayerView +} + +/** + * Default implementation of [UIControllerScope], managing the menu stack. + */ +@InternalUiApi +class UIControllerScopeImpl( + override val theoplayerView: THEOplayerView?, + private val providedPlayer: Player? +) : UIControllerScope { + private var menuStack = mutableStateListOf() + + val currentMenu: MenuContent? + get() = menuStack.lastOrNull() + var lastWasClosed: Boolean = false + private set + + var activityInPipMode: Boolean = false + set(value) { + field = value + if (lazyPlayer.isInitialized()) { + lazyPlayer.value.activityInPipMode = value + } + } + + private val lazyPlayer = lazy { + PlayerImpl(theoplayerView).also { it.activityInPipMode = activityInPipMode } + } + + override val player: Player + get() = providedPlayer ?: lazyPlayer.value + + fun dispose() { + if (lazyPlayer.isInitialized()) { + lazyPlayer.value.dispose() + } + } + + override fun openMenu(menu: MenuContent) { + menuStack.add(menu) + lastWasClosed = false + } + + override fun closeCurrentMenu() { + menuStack.removeLastOrNull()?.also { + lastWasClosed = true + } + } +} diff --git a/ui/src/main/java/com/theoplayer/android/ui/Menu.kt b/ui/src/main/java/com/theoplayer/android/ui/Menu.kt index 9981dbd4..fd2e412a 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Menu.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Menu.kt @@ -17,11 +17,6 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -/** - * The content for a new menu, typically this should draw a single [Menu]. - */ -typealias MenuContent = @Composable MenuScope.() -> Unit; - /** * A menu that can be opened on top of the player. * @@ -71,33 +66,3 @@ fun MenuScope.Menu( } } } - -/** - * Scope for the contents of a [Menu]. - */ -interface MenuScope { - /** - * Open a new submenu. - * - * When the submenu is closed, the original menu is shown again. - * - * Only the top-most menu is composed. While the submenu is open, the original menu - * (or the player controls, if no menu was open yet) leaves the composition, and any state - * remembered there is disposed. So instead of passing state from the caller into [menu], - * remember the state inside [menu]: - * ```kotlin - * // Don't: audioTrackState is disposed while the menu is open - * val audioTrackState = rememberAudioTrackState(player) - * openMenu { AudioTrackMenu(audioTrackState) } - * - * // Do - * openMenu { AudioTrackMenu(rememberAudioTrackState(player)) } - * ``` - */ - fun openMenu(menu: MenuContent) - - /** - * Close the currently open menu. - */ - fun closeCurrentMenu() -} \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index 49015270..20b647d9 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -1,10 +1,6 @@ package com.theoplayer.android.ui import android.app.Activity -import android.app.Application -import android.content.Context -import android.content.ContextWrapper -import android.view.ViewGroup import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition @@ -21,7 +17,6 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -33,7 +28,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.SideEffect import androidx.compose.runtime.remember @@ -42,14 +36,8 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalInspectionMode -import androidx.compose.ui.platform.ViewCompositionStrategy -import androidx.compose.ui.viewinterop.AndroidView -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver -import androidx.lifecycle.compose.LocalLifecycleOwner import com.theoplayer.android.api.THEOplayerConfig import com.theoplayer.android.api.THEOplayerView import com.theoplayer.android.api.cast.chromecast.PlayerCastState @@ -382,70 +370,6 @@ internal fun UIControllerImpl( } } -/** - * Scope for the contents of a [UIController]. - */ -interface UIControllerScope : MenuScope { - /** - * The player hosted in this [UIController]. - * - * If the UIController was created from a [THEOplayerView], the [Player] is created on - * first access (and only then adds its listeners). Prefer [theoplayerView] with the - * experimental state holders instead. - */ - val player: Player - - /** - * The THEOplayer view hosted in this [UIController], or `null` in inspection mode. - */ - val theoplayerView: THEOplayerView? - get() = player.theoplayerView -} - -private class UIControllerScopeImpl( - override val theoplayerView: THEOplayerView?, - private val providedPlayer: Player? -) : UIControllerScope { - private var menuStack = mutableStateListOf() - - val currentMenu: MenuContent? - get() = menuStack.lastOrNull() - var lastWasClosed: Boolean = false - private set - - var activityInPipMode: Boolean = false - set(value) { - field = value - if (lazyPlayer.isInitialized()) { - lazyPlayer.value.activityInPipMode = value - } - } - - private val lazyPlayer = lazy { - PlayerImpl(theoplayerView).also { it.activityInPipMode = activityInPipMode } - } - - override val player: Player - get() = providedPlayer ?: lazyPlayer.value - - fun dispose() { - if (lazyPlayer.isInitialized()) { - lazyPlayer.value.dispose() - } - } - - override fun openMenu(menu: MenuContent) { - menuStack.add(menu) - lastWasClosed = false - } - - override fun closeCurrentMenu() { - menuStack.removeLastOrNull()?.also { - lastWasClosed = true - } - } -} - internal val LocalUIControllerErrorState = staticCompositionLocalOf { null } private sealed class UIState { @@ -455,67 +379,6 @@ private sealed class UIState { object Hidden } -@Composable -private fun PlayerContainer( - modifier: Modifier = Modifier, - theoplayerView: THEOplayerView?, - ui: @Composable () -> Unit -) { - if (theoplayerView == null) { - Box( - modifier = modifier - ) { - ui() - } - } else { - val lifecycle = LocalLifecycleOwner.current.lifecycle - var uiContainer by remember { mutableStateOf(null) } - var composeView by remember { mutableStateOf(null) } - - AndroidView( - modifier = modifier, - factory = { context -> - uiContainer = - theoplayerView.findViewById(com.theoplayer.android.R.id.theo_ui_container) - // Wrap our UI inside a ComposeView - composeView = ComposeView(context).apply { - // When entering fullscreen, we remove the view from its original location - // and add it to the activity's root view. - // When it is temporarily removed (and detached from the window), - // we do *not* want to lose the composition. Therefore, don't use the default - // DisposeOnDetachedFromWindow or DisposeOnDetachedFromWindowOrReleasedFromPool - // strategies! - setViewCompositionStrategy( - ViewCompositionStrategy.DisposeOnLifecycleDestroyed(lifecycle) - ) - setContent { - ui() - } - } - // Host the THEOplayer view inside our AndroidView - (theoplayerView.parent as? ViewGroup)?.removeView(theoplayerView) - theoplayerView - }) - - // Install inside THEOplayerView's UI container - DisposableEffect(uiContainer, composeView) { - val container = uiContainer - val view = composeView - if (container != null && view != null) { - container.addView(view) - theoplayerView.postInvalidate() - } - onDispose { - if (container != null && view != null) { - container.removeView(view) - view.disposeComposition() - theoplayerView.postInvalidate() - } - } - } - } -} - @Composable private fun UIControllerScope.PlayerControls( controlsVisible: Boolean, @@ -593,61 +456,6 @@ fun rememberPlayer(config: THEOplayerConfig? = null): Player { return rememberPlayerInternal(theoplayerView) } -/** - * Creates and remembers a [THEOplayerView] owned by the composition. - * - * This couples the lifecycle of the view to the current activity: it automatically calls - * [THEOplayerView.onPause] and [THEOplayerView.onResume] whenever the current activity is - * [paused][Activity.onPause] or [resumed][Activity.onResume] (resuming an ad that was - * interrupted), and it destroys the view when it leaves the composition. - * - * In previews, the SDK does not initialize the view ([View.isInEditMode]), so do not - * access its [THEOplayerView.player] there. [UIController] and [DefaultUI] handle this - * automatically. - * - * @param config the player configuration - */ -@Composable -fun rememberTHEOplayerView(config: THEOplayerConfig? = null): THEOplayerView { - val context = LocalContext.current - val inspectionMode = LocalInspectionMode.current - val theoplayerView = remember { - THEOplayerView(if (inspectionMode) PreviewContext(context) else context, config) - } - if (!theoplayerView.isInEditMode) { - setupTHEOplayerView(theoplayerView, destroyOnLifecycleDestroy = false) - DisposableEffect(theoplayerView) { - onDispose { - theoplayerView.onDestroy() - } - } - } - return theoplayerView -} - -/** - * A context for creating a [THEOplayerView] in a preview. - * - * The [THEOplayerView] constructor casts the application context to [Application], - * but layoutlib's application context is not an [Application]. - */ -private class PreviewContext(base: Context) : ContextWrapper(base) { - private val application = PreviewApplication(base.applicationContext) - - override fun getApplicationContext(): Context = application -} - -private class PreviewApplication(base: Context) : Application() { - init { - attachBaseContext(base) - } -} - -@Composable -internal fun rememberTHEOplayerViewInternal(config: THEOplayerConfig?): THEOplayerView? { - return if (LocalInspectionMode.current) null else rememberTHEOplayerView(config) -} - /** * Create a [Player] wrapping an existing [THEOplayerView]. * @@ -702,47 +510,6 @@ internal fun rememberPlayerInternal(theoplayerView: THEOplayerView?): Player { return player } -@Composable -internal fun setupTHEOplayerView( - theoplayerView: THEOplayerView, - destroyOnLifecycleDestroy: Boolean = true -): THEOplayerView { - var wasPlayingAd by remember { mutableStateOf(false) } - - val lifecycle = LocalLifecycleOwner.current.lifecycle - DisposableEffect(lifecycle, theoplayerView) { - val lifecycleObserver = LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_RESUME -> { - theoplayerView.onResume() - if (wasPlayingAd) { - theoplayerView.player.play() - wasPlayingAd = false - } - } - - Lifecycle.Event.ON_PAUSE -> { - wasPlayingAd = theoplayerView.player.ads.isPlaying - theoplayerView.onPause() - } - - Lifecycle.Event.ON_DESTROY -> { - if (destroyOnLifecycleDestroy) { - theoplayerView.onDestroy() - } - } - else -> {} - } - } - lifecycle.addObserver(lifecycleObserver) - onDispose { - lifecycle.removeObserver(lifecycleObserver) - } - } - - return theoplayerView -} - internal fun Modifier.playerAspectRatio(videoSizeState: VideoSizeState): Modifier { return this.constrainedAspectRatio( if (videoSizeState.videoWidth != 0 && videoSizeState.videoHeight != 0) { From 81ee1e9c54d7e6594942c5ab8fb44e044f5060c6 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 14 Oct 2026 13:57:29 +0000 Subject: [PATCH 2/2] Remove trailing semicolon Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt b/ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt index b4026823..2dc3e798 100644 --- a/ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt +++ b/ui-common/src/main/java/com/theoplayer/android/ui/MenuScope.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.Composable /** * The content for a new menu, typically this should draw a single [Menu]. */ -typealias MenuContent = @Composable MenuScope.() -> Unit; +typealias MenuContent = @Composable MenuScope.() -> Unit /** * Scope for the contents of a [Menu].