From a0c62ac5df1d4c4e018c44abd0f346c3f8461f09 Mon Sep 17 00:00:00 2001 From: oxy-macmini Date: Sun, 9 Mar 2025 22:06:19 +0800 Subject: [PATCH] refactor: separate TV code. --- .../com/m3u/feature/channel/ChannelMask.kt | 66 +- .../com/m3u/feature/channel/ChannelScreen.kt | 9 +- .../channel/components/DlnaDeviceItem.kt | 2 +- .../components/DlnaDevicesBottomSheet.kt | 2 +- .../channel/components/FormatsBottomSheet.kt | 2 +- .../components/MaskGestureValuePanel.kt | 2 +- .../channel/components/MaskValueButton.kt | 28 +- .../feature/channel/components/PlayerMask.kt | 13 +- .../feature/channel/components/PlayerPanel.kt | 124 +--- .../channel/components/ProgrammeGuide.kt | 142 ++-- .../channel/components/VerticalGestureArea.kt | 1 - .../m3u/feature/crash/components/FileItem.kt | 2 +- .../crash/screen/detail/DetailScreen.kt | 2 +- .../m3u/feature/favorite/FavoriteScreen.kt | 68 +- .../favorite/components/FavoriteGallery.kt | 93 --- .../favorite/components/FavoriteItem.kt | 55 -- .../com/m3u/feature/foryou/ForyouScreen.kt | 4 +- .../feature/foryou/components/PlaylistItem.kt | 117 +-- .../components/recommend/RecommendGallery.kt | 82 +-- .../components/recommend/RecommendItem.kt | 54 +- .../PlaylistConfigurationScreen.kt | 2 +- .../m3u/feature/playlist/PlaylistScreen.kt | 324 +++++++-- .../feature/playlist/TvPlaylistActivity.kt | 80 -- ...oneChannelGallery.kt => ChannelGallery.kt} | 4 +- ...martphoneChannelItem.kt => ChannelItem.kt} | 4 +- .../components/ImmersiveBackground.kt | 159 ---- .../playlist/components/PlaylistTabRow.kt | 30 +- .../playlist/components/TvChannelGallery.kt | 80 -- .../playlist/components/TvChannelItem.kt | 148 ---- .../internal/SmartphonePlaylistScreenImpl.kt | 318 -------- .../playlist/internal/TvPlaylistScreenImpl.kt | 307 -------- .../playlist/navigation/PlaylistNavigation.kt | 23 +- .../com/m3u/feature/setting/SettingScreen.kt | 26 +- .../setting/components/CanvasBottomSheet.kt | 2 +- .../setting/components/DataSourceSelection.kt | 2 +- .../setting/components/EpgPlaylistItem.kt | 14 +- .../setting/components/LocalStorageButton.kt | 2 +- .../setting/fragments/AppearanceFragment.kt | 38 +- .../setting/fragments/OptionalFragment.kt | 94 ++- .../fragments/SubscriptionsFragment.kt | 38 +- gradle/libs.versions.toml | 6 + i18n/src/main/res/values/feat_foryou.xml | 2 +- material/build.gradle.kts | 2 - .../m3u/material/components/Backgrounds.kt | 16 +- .../com/m3u/material/components/Buttons.kt | 263 ------- .../java/com/m3u/material/components/Icons.kt | 34 - .../m3u/material/components/Preferences.kt | 177 ++--- .../com/m3u/material/components/TextFields.kt | 338 +++------ .../m3u/material/components/ThemeSelection.kt | 167 ++--- .../material/components/mask/MaskButton.kt | 29 +- .../components/mask/MaskCircleButton.kt | 65 +- .../com/m3u/material/components/tv/Dialog.kt | 685 ------------------ .../src/main/java/com/m3u/material/ktx/Tv.kt | 67 -- .../main/java/com/m3u/material/model/Theme.kt | 58 +- settings.gradle.kts | 1 + .../main/java/com/m3u/smartphone/ui/App.kt | 7 +- .../java/com/m3u/smartphone/ui/AppNavHost.kt | 7 +- .../java/com/m3u/smartphone/ui/Scaffold.kt | 185 ++--- .../com/m3u/smartphone/ui/StarBackground.kt | 4 +- .../ui/internal/TelevisionScaffoldImpl.kt | 139 ---- .../ui/sheet/RemoteDirectionController.kt | 2 +- .../ui/sheet/VirtualNumberKeyboard.kt | 2 +- tv/.gitignore | 1 + tv/build.gradle.kts | 104 +++ tv/proguard-rules.pro | 21 + tv/src/main/AndroidManifest.xml | 31 + tv/src/main/java/com/m3u/tv/MainActivity.kt | 45 ++ tv/src/main/java/com/m3u/tv/ui/theme/Color.kt | 11 + tv/src/main/java/com/m3u/tv/ui/theme/Theme.kt | 34 + tv/src/main/java/com/m3u/tv/ui/theme/Type.kt | 36 + tv/src/main/res/mipmap-hdpi/ic_launcher.webp | Bin 0 -> 1404 bytes tv/src/main/res/mipmap-mdpi/ic_launcher.webp | Bin 0 -> 982 bytes tv/src/main/res/mipmap-xhdpi/ic_launcher.webp | Bin 0 -> 1900 bytes .../main/res/mipmap-xxhdpi/ic_launcher.webp | Bin 0 -> 2884 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.webp | Bin 0 -> 3844 bytes tv/src/main/res/values/strings.xml | 3 + tv/src/main/res/values/themes.xml | 4 + .../java/com/m3u/ui/EpisodesBottomSheet.kt | 12 +- ui/src/main/java/com/m3u/ui/MediaSheet.kt | 12 +- ui/src/main/java/com/m3u/ui/SnackHost.kt | 2 +- .../main/java/com/m3u/ui/SortBottomSheet.kt | 77 +- 81 files changed, 1217 insertions(+), 3995 deletions(-) delete mode 100644 feature/playlist/src/main/java/com/m3u/feature/playlist/TvPlaylistActivity.kt rename feature/playlist/src/main/java/com/m3u/feature/playlist/components/{SmartphoneChannelGallery.kt => ChannelGallery.kt} (98%) rename feature/playlist/src/main/java/com/m3u/feature/playlist/components/{SmartphoneChannelItem.kt => ChannelItem.kt} (99%) delete mode 100644 feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt delete mode 100644 feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelGallery.kt delete mode 100644 feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelItem.kt delete mode 100644 feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt delete mode 100644 feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt delete mode 100644 material/src/main/java/com/m3u/material/components/Buttons.kt delete mode 100644 material/src/main/java/com/m3u/material/components/Icons.kt delete mode 100644 material/src/main/java/com/m3u/material/components/tv/Dialog.kt delete mode 100644 material/src/main/java/com/m3u/material/ktx/Tv.kt delete mode 100644 smartphone/src/main/java/com/m3u/smartphone/ui/internal/TelevisionScaffoldImpl.kt create mode 100644 tv/.gitignore create mode 100644 tv/build.gradle.kts create mode 100644 tv/proguard-rules.pro create mode 100644 tv/src/main/AndroidManifest.xml create mode 100644 tv/src/main/java/com/m3u/tv/MainActivity.kt create mode 100644 tv/src/main/java/com/m3u/tv/ui/theme/Color.kt create mode 100644 tv/src/main/java/com/m3u/tv/ui/theme/Theme.kt create mode 100644 tv/src/main/java/com/m3u/tv/ui/theme/Type.kt create mode 100644 tv/src/main/res/mipmap-hdpi/ic_launcher.webp create mode 100644 tv/src/main/res/mipmap-mdpi/ic_launcher.webp create mode 100644 tv/src/main/res/mipmap-xhdpi/ic_launcher.webp create mode 100644 tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp create mode 100644 tv/src/main/res/mipmap-xxxhdpi/ic_launcher.webp create mode 100644 tv/src/main/res/values/strings.xml create mode 100644 tv/src/main/res/values/themes.xml diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt index f7e4007e..6c889fea 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelMask.kt @@ -1,10 +1,7 @@ package com.m3u.feature.channel import android.content.pm.ActivityInfo -import android.view.KeyEvent -import androidx.activity.compose.BackHandler import androidx.activity.compose.LocalOnBackPressedDispatcherOwner -import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Spring import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState @@ -15,7 +12,6 @@ import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.focusable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsDraggedAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -31,14 +27,11 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.automirrored.rounded.VolumeDown import androidx.compose.material.icons.automirrored.rounded.VolumeOff import androidx.compose.material.icons.automirrored.rounded.VolumeUp import androidx.compose.material.icons.rounded.Archive import androidx.compose.material.icons.rounded.Cast -import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.HighQuality -import androidx.compose.material.icons.rounded.LightMode import androidx.compose.material.icons.rounded.Pause import androidx.compose.material.icons.rounded.PictureInPicture import androidx.compose.material.icons.rounded.PlayArrow @@ -68,10 +61,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip -import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.semantics @@ -95,7 +86,6 @@ import com.m3u.material.components.mask.MaskPanel import com.m3u.material.components.mask.MaskState import com.m3u.material.effects.currentBackStackEntry import com.m3u.material.ktx.thenIf -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalSpacing import com.m3u.ui.FontFamilies import com.m3u.ui.Image @@ -141,7 +131,6 @@ internal fun ChannelMask( val helper = LocalHelper.current val spacing = LocalSpacing.current val configuration = LocalConfiguration.current - val tv = tv() val coroutineScope = rememberCoroutineScope() val onBackPressedDispatcher = checkNotNull( @@ -236,12 +225,6 @@ internal fun ChannelMask( } } - if (tv) { - BackHandler(maskState.visible && !maskState.locked) { - maskState.sleep() - } - } - Box( modifier = modifier.fillMaxSize() ) { @@ -314,7 +297,7 @@ internal fun ChannelMask( ) } - if (!tv && preferences.screencast) { + if (preferences.screencast) { MaskButton( state = maskState, icon = Icons.Rounded.Cast, @@ -322,7 +305,7 @@ internal fun ChannelMask( contentDescription = stringResource(string.feat_channel_tooltip_cast) ) } - if (!tv && playerState.videoSize.isNotEmpty) { + if (playerState.videoSize.isNotEmpty) { MaskButton( state = maskState, icon = Icons.Rounded.PictureInPicture, @@ -453,7 +436,6 @@ internal fun ChannelMask( ) } } - if (!tv) { val autoRotating by ChannelMaskUtils.IsAutoRotatingEnabled LaunchedEffect(autoRotating) { if (autoRotating) { @@ -474,7 +456,6 @@ internal fun ChannelMask( contentDescription = stringResource(string.feat_channel_tooltip_screen_rotating) ) } - } }, slider = { when { @@ -506,45 +487,10 @@ internal fun ChannelMask( overflow = TextOverflow.Ellipsis, modifier = Modifier.basicMarquee() ) - var isSliderHasFocus by remember { mutableStateOf(false) } - val tvSliderModifier = Modifier - .onFocusChanged { - isSliderHasFocus = it.hasFocus - if (it.hasFocus) { - maskState.wake() - } - } - .focusable() - .onKeyEvent { event -> - when (event.nativeKeyEvent.keyCode) { - KeyEvent.KEYCODE_DPAD_LEFT -> { - bufferedPosition = (bufferedPosition - ?: contentPosition - .coerceAtLeast(0L)) - 15000L - maskState.wake() - true - } - - KeyEvent.KEYCODE_DPAD_RIGHT -> { - bufferedPosition = (bufferedPosition - ?: contentPosition - .coerceAtLeast(0L)) + 15000L - maskState.wake() - true - } - - else -> false - } - } val sliderThumbWidthDp by animateDpAsState( - targetValue = if (isSliderHasFocus) 8.dp - else 4.dp, + targetValue = 4.dp, label = "slider-thumb-width-dp" ) - val sliderColors = SliderDefaults.colors( - thumbColor = if (!isSliderHasFocus) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.primary.copy(0.56f) - ) val sliderInteractionSource = remember { MutableInteractionSource() } Slider( value = animContentPosition, @@ -555,17 +501,13 @@ internal fun ChannelMask( bufferedPosition = it.roundToLong() maskState.wake() }, - colors = sliderColors, thumb = { SliderDefaults.Thumb( interactionSource = sliderInteractionSource, - colors = sliderColors, thumbSize = DpSize(sliderThumbWidthDp, 44.dp) ) }, - modifier = Modifier - .weight(1f) - .thenIf(tv) { tvSliderModifier } + modifier = Modifier.weight(1f) ) } } diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt index 889c9f0a..af8fc195 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/ChannelScreen.kt @@ -58,7 +58,6 @@ import com.m3u.material.components.mask.rememberMaskState import com.m3u.material.components.mask.toggle import com.m3u.material.components.rememberPullPanelLayoutState import com.m3u.material.ktx.checkPermissionOrRationale -import com.m3u.material.ktx.tv import com.m3u.ui.Player import com.m3u.ui.helper.LocalHelper import com.m3u.ui.helper.OnPipModeChanged @@ -67,6 +66,7 @@ import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlin.time.Duration.Companion.milliseconds +import androidx.core.net.toUri @Composable fun ChannelRoute( @@ -269,7 +269,7 @@ fun ChannelRoute( val channelUrl = channel?.url ?: return@DlnaDevicesBottomSheet context.startActivity( Intent(Intent.ACTION_VIEW).apply { - setDataAndType(Uri.parse(channelUrl), "video/*") + setDataAndType(channelUrl.toUri(), "video/*") }.let { Intent.createChooser(it, openInExternalPlayerString.title()) } ) }, @@ -325,7 +325,6 @@ private fun ChannelPlayer( val currentVolume by rememberUpdatedState(volume) val currentSpeed by rememberUpdatedState(speed) val preferences = hiltPreferences() - val tv = tv() Background( color = Color.Black, @@ -354,7 +353,7 @@ private fun ChannelPlayer( modifier = Modifier .fillMaxHeight() .fillMaxWidth(0.18f), - enabled = !tv && preferences.brightnessGesture + enabled = preferences.brightnessGesture ) VerticalGestureArea( @@ -371,7 +370,7 @@ private fun ChannelPlayer( .align(Alignment.TopEnd) .fillMaxHeight() .fillMaxWidth(0.18f), - enabled = !tv && preferences.volumeGesture + enabled = preferences.volumeGesture ) val shouldShowPlaceholder = diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDeviceItem.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDeviceItem.kt index d34ea368..f804829a 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDeviceItem.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDeviceItem.kt @@ -7,7 +7,7 @@ import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import net.mm2d.upnp.Device @Composable diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDevicesBottomSheet.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDevicesBottomSheet.kt index 8703047d..94c19e85 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDevicesBottomSheet.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/DlnaDevicesBottomSheet.kt @@ -28,7 +28,7 @@ import androidx.compose.ui.unit.dp import com.m3u.core.util.basic.title import com.m3u.i18n.R.string import com.m3u.material.components.CircularProgressIndicator -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.components.mask.MaskState import com.m3u.material.model.LocalSpacing import com.m3u.ui.UnstableBadge diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/FormatsBottomSheet.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/FormatsBottomSheet.kt index 24ddae04..42fb6e36 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/FormatsBottomSheet.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/FormatsBottomSheet.kt @@ -35,7 +35,7 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.C import androidx.media3.common.Format import com.m3u.i18n.R.string -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.components.mask.MaskState import com.m3u.material.model.LocalSpacing import kotlinx.coroutines.launch diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskGestureValuePanel.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskGestureValuePanel.kt index 6917c57e..a7266085 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskGestureValuePanel.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskGestureValuePanel.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -16,7 +17,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.tv.material3.MaterialTheme import com.m3u.material.model.LocalSpacing import com.m3u.ui.MonoText diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt index 104ab862..c50c9ea1 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/MaskValueButton.kt @@ -2,6 +2,7 @@ package com.m3u.feature.channel.components import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text @@ -12,13 +13,10 @@ import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import com.m3u.material.components.IconButton +import androidx.compose.material3.IconButton import com.m3u.material.components.mask.MaskState -import com.m3u.material.ktx.thenIf -import com.m3u.material.ktx.tv import com.m3u.ui.FontFamilies @Composable @@ -33,8 +31,6 @@ fun MaskTextButton( tint: Color = Color.Unspecified, enabled: Boolean = true ) { - val tv = tv() - TooltipBox( state = tooltipState, positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), @@ -48,13 +44,6 @@ fun MaskTextButton( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, modifier = modifier - .thenIf(tv) { - Modifier.onFocusEvent { - if (it.isFocused) { - state.wake() - } - } - } ) { if (text != null) { Text( @@ -65,15 +54,18 @@ fun MaskTextButton( ) } IconButton( - icon = icon, - enabled = enabled, - contentDescription = contentDescription, onClick = { state.wake() onClick() }, - tint = tint - ) + enabled = enabled + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = tint + ) + } } } } \ No newline at end of file diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt index b0644d1c..7cebfac1 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerMask.kt @@ -1,23 +1,16 @@ package com.m3u.feature.channel.components import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.animateContentSize import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.safeContentPadding import androidx.compose.foundation.layout.safeDrawing -import androidx.compose.foundation.layout.safeDrawingPadding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -25,13 +18,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.coerceAtLeast import androidx.compose.ui.unit.dp import com.m3u.material.components.mask.Mask import com.m3u.material.components.mask.MaskState -import com.m3u.material.ktx.plus -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalSpacing @Composable @@ -46,7 +36,6 @@ internal fun PlayerMask( val configuration = LocalConfiguration.current val spacing = LocalSpacing.current - val tv = tv() Mask( state = state, color = Color.Black.copy(alpha = 0.54f), @@ -59,7 +48,7 @@ internal fun PlayerMask( .padding(horizontal = spacing.medium) .align(Alignment.TopCenter), horizontalArrangement = Arrangement.spacedBy( - if (!tv) spacing.none else spacing.medium, + spacing.none, Alignment.End ), verticalAlignment = Alignment.Top, diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerPanel.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerPanel.kt index 1977ad67..64b494ea 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerPanel.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/PlayerPanel.kt @@ -1,6 +1,5 @@ package com.m3u.feature.channel.components -import android.view.KeyEvent import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut @@ -34,6 +33,7 @@ import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.NotificationsActive import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface @@ -48,20 +48,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusDirection import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.paging.compose.LazyPagingItems -import androidx.tv.material3.surfaceColorAtElevation import coil.compose.SubcomposeAsyncImage import com.m3u.core.util.collections.indexOf import com.m3u.data.database.model.Channel @@ -71,12 +66,11 @@ import com.m3u.data.database.model.ProgrammeRange import com.m3u.data.service.MediaCommand import com.m3u.material.components.Background import com.m3u.material.components.CircularProgressIndicator -import com.m3u.material.components.IconButton +import androidx.compose.material3.IconButton import com.m3u.material.effects.BackStackEntry import com.m3u.material.effects.BackStackHandler import com.m3u.material.ktx.Edge import com.m3u.material.ktx.blurEdges -import com.m3u.material.ktx.tv import com.m3u.material.ktx.thenIf import com.m3u.material.model.LocalSpacing import com.m3u.material.shape.AbsoluteSmoothCornerShape @@ -89,10 +83,6 @@ import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime -import androidx.tv.material3.Card as TvCard -import androidx.tv.material3.CardDefaults as TvCardDefaults -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText @Composable internal fun PlayerPanel( @@ -225,15 +215,18 @@ internal fun PlayerPanel( if (isReminderShowing) { val inReminder = currentProgramme.id in programmeReminderIds IconButton( - icon = if (!inReminder) Icons.Outlined.Notifications - else Icons.Rounded.NotificationsActive, - contentDescription = null, onClick = { if (inReminder) onCancelRemindProgramme(currentProgramme) else onRemindProgramme(currentProgramme) }, modifier = Modifier.align(Alignment.CenterVertically) - ) + ) { + Icon( + imageVector = if (!inReminder) Icons.Outlined.Notifications + else Icons.Rounded.NotificationsActive, + contentDescription = null + ) + } } } @@ -345,7 +338,6 @@ fun PlayerPanelImpl( } } -@OptIn(ExperimentalComposeUiApi::class) @Composable private fun ChannelGallery( value: ChannelGalleryValue, @@ -355,7 +347,6 @@ private fun ChannelGallery( ) { val spacing = LocalSpacing.current val lazyListState = rememberLazyListState() - val tv = tv() ScrollToCurrentEffect( value = value, @@ -395,25 +386,11 @@ private fun ChannelGallery( content = content ) } else { - val focusManager = LocalFocusManager.current LazyColumn( state = lazyListState, verticalArrangement = Arrangement.spacedBy(spacing.medium), contentPadding = PaddingValues(spacing.medium), - modifier = modifier - .fillMaxWidth() - .thenIf(tv) { - Modifier.onKeyEvent { - when (it.nativeKeyEvent.keyCode) { - KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_RIGHT -> { - focusManager.moveFocus(FocusDirection.Exit) - true - } - - else -> false - } - } - }, + modifier = modifier.fillMaxWidth(), content = content ) } @@ -441,62 +418,33 @@ private fun ChannelGalleryItem( val spacing = LocalSpacing.current val helper = LocalHelper.current val coroutineScope = rememberCoroutineScope() - val tv = tv() - if (!tv) { - Card( - colors = CardDefaults.cardColors( - containerColor = if (!isPlaying) - MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.medium) - else MaterialTheme.colorScheme.onSurface, - contentColor = if (!isPlaying) MaterialTheme.colorScheme.onSurface - else MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.small) - ), - shape = AbsoluteRoundedCornerShape(spacing.medium), - elevation = CardDefaults.cardElevation(spacing.none), - onClick = { - if (isPlaying) return@Card - coroutineScope.launch { - helper.play( - MediaCommand.Common(channel.id) - ) - } - }, - modifier = modifier - ) { - Text( - text = channel.title, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold.takeIf { isPlaying }, - modifier = Modifier.padding(spacing.medium) - ) - } - } else { - TvCard( - colors = TvCardDefaults.colors( - containerColor = if (!isPlaying) - TvMaterialTheme.colorScheme.surfaceColorAtElevation(spacing.medium) - else TvMaterialTheme.colorScheme.onSurface, - contentColor = if (!isPlaying) TvMaterialTheme.colorScheme.onSurface - else TvMaterialTheme.colorScheme.surfaceColorAtElevation(spacing.small) - ), - onClick = { - if (isPlaying) return@TvCard - coroutineScope.launch { - helper.play( - MediaCommand.Common(channel.id) - ) - } - }, - modifier = modifier - ) { - TvText( - text = channel.title, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.SemiBold.takeIf { isPlaying }, - modifier = Modifier.padding(spacing.medium) - ) - } + Card( + colors = CardDefaults.cardColors( + containerColor = if (!isPlaying) + MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.medium) + else MaterialTheme.colorScheme.onSurface, + contentColor = if (!isPlaying) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.small) + ), + shape = AbsoluteRoundedCornerShape(spacing.medium), + elevation = CardDefaults.cardElevation(spacing.none), + onClick = { + if (isPlaying) return@Card + coroutineScope.launch { + helper.play( + MediaCommand.Common(channel.id) + ) + } + }, + modifier = modifier + ) { + Text( + text = channel.title, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.SemiBold.takeIf { isPlaying }, + modifier = Modifier.padding(spacing.medium) + ) } } diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/ProgrammeGuide.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/ProgrammeGuide.kt index 0c7e7074..6ce08e9f 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/ProgrammeGuide.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/ProgrammeGuide.kt @@ -62,11 +62,9 @@ import com.m3u.core.architecture.preferences.hiltPreferences import com.m3u.data.database.model.Programme import com.m3u.data.database.model.ProgrammeRange import com.m3u.data.database.model.ProgrammeRange.Companion.HOUR_LENGTH -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.ktx.Edge import com.m3u.material.ktx.blurEdges -import com.m3u.material.ktx.tv -import com.m3u.material.ktx.thenIf import com.m3u.material.model.LocalSpacing import com.m3u.ui.FontFamilies import com.m3u.ui.util.TimeUtils.formatEOrSh @@ -85,9 +83,6 @@ import kotlinx.datetime.toLocalDateTime import kotlin.math.absoluteValue import kotlin.time.Duration import kotlin.time.Duration.Companion.seconds -import androidx.tv.material3.ClickableSurfaceDefaults as TvClickableSurfaceDefaults -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Surface as TvSurface private enum class Zoom(val time: Float) { DEFAULT(1f), ZOOM_1_5(1.5f), ZOOM_2(2f), ZOOM_5(5f) @@ -108,7 +103,6 @@ internal fun ProgramGuide( onProgrammePressed: (Programme) -> Unit ) { val spacing = LocalSpacing.current - val tv = tv() val currentMilliseconds by produceCurrentMillisecondState() val coroutineScope = rememberCoroutineScope() @@ -160,7 +154,7 @@ internal fun ProgramGuide( MaterialTheme.colorScheme.surface, listOf(Edge.Top, Edge.Bottom) ) - .thenIf(!tv) { zoomGestureModifier } + .then(zoomGestureModifier) .then(modifier) ) { // programmes @@ -223,17 +217,14 @@ internal fun ProgramGuide( ) }) {} } - - if (!tv) { - Controls( - animateToCurrentTimeline = { - coroutineScope.launch { animateToCurrentTimeline() } - }, - modifier = Modifier - .padding(spacing.medium) - .align(Alignment.BottomEnd) - ) - } + Controls( + animateToCurrentTimeline = { + coroutineScope.launch { animateToCurrentTimeline() } + }, + modifier = Modifier + .padding(spacing.medium) + .align(Alignment.BottomEnd) + ) } } @@ -303,7 +294,6 @@ private fun ProgrammeCell( val currentOnPressed by rememberUpdatedState(onPressed) val spacing = LocalSpacing.current val preferences = hiltPreferences() - val tv = tv() val clockMode = preferences.twelveHourClock val content = @Composable { Column( @@ -341,71 +331,61 @@ private fun ProgrammeCell( ) } } - if (!tv) { - val hapticFeedback = LocalHapticFeedback.current - var isPressed by remember { mutableStateOf(false) } - val scale by animateFloatAsState( - targetValue = if (isPressed) 0.95f else 1f, - label = "programme-cell-scale", - animationSpec = spring( - dampingRatio = Spring.DampingRatioHighBouncy, - stiffness = Spring.StiffnessMediumLow - ) + val hapticFeedback = LocalHapticFeedback.current + var isPressed by remember { mutableStateOf(false) } + val scale by animateFloatAsState( + targetValue = if (isPressed) 0.95f else 1f, + label = "programme-cell-scale", + animationSpec = spring( + dampingRatio = Spring.DampingRatioHighBouncy, + stiffness = Spring.StiffnessMediumLow ) - val currentColor by animateColorAsState( - targetValue = if(inReminder) MaterialTheme.colorScheme.tertiary - else MaterialTheme.colorScheme.tertiaryContainer, - label = "programme-cell-color" - ) - val currentContentColor by animateColorAsState( - targetValue = if(inReminder) MaterialTheme.colorScheme.onTertiary - else MaterialTheme.colorScheme.onTertiaryContainer, - label = "programme-cell-color" - ) - Surface( - color = currentColor, - contentColor = currentContentColor, - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), - shape = AbsoluteRoundedCornerShape(4.dp), - modifier = Modifier - .pointerInput(Unit) { - awaitEachGesture { - val down = awaitFirstDown() - try { - withTimeout(viewConfiguration.longPressTimeoutMillis) { - waitForUpOrCancellation() - } - } catch (_: PointerEventTimeoutCancellationException) { - down.consume() - currentOnPressed() - hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - isPressed = true - do { - val event = awaitPointerEvent() - event.changes.fastForEach { it.consume() } - } while (event.changes.fastAny { it.pressed }) - isPressed = false - } finally { - isPressed = false + ) + val currentColor by animateColorAsState( + targetValue = if (inReminder) MaterialTheme.colorScheme.tertiary + else MaterialTheme.colorScheme.tertiaryContainer, + label = "programme-cell-color" + ) + val currentContentColor by animateColorAsState( + targetValue = if (inReminder) MaterialTheme.colorScheme.onTertiary + else MaterialTheme.colorScheme.onTertiaryContainer, + label = "programme-cell-color" + ) + Surface( + color = currentColor, + contentColor = currentContentColor, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline), + shape = AbsoluteRoundedCornerShape(4.dp), + modifier = Modifier + .pointerInput(Unit) { + awaitEachGesture { + val down = awaitFirstDown() + try { + withTimeout(viewConfiguration.longPressTimeoutMillis) { + waitForUpOrCancellation() } + } catch (_: PointerEventTimeoutCancellationException) { + down.consume() + currentOnPressed() + hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) + isPressed = true + do { + val event = awaitPointerEvent() + event.changes.fastForEach { it.consume() } + } while (event.changes.fastAny { it.pressed }) + isPressed = false + } finally { + isPressed = false } } - .graphicsLayer { - scaleX = scale - scaleY = scale - } - .then(modifier), - content = content - ) - } else { - TvSurface( - onClick = onPressed, - colors = TvClickableSurfaceDefaults.colors(TvMaterialTheme.colorScheme.tertiaryContainer), - shape = TvClickableSurfaceDefaults.shape(AbsoluteRoundedCornerShape(4.dp)), - modifier = modifier, - content = { content() } - ) - } + } + .graphicsLayer { + scaleX = scale + scaleY = scale + } + .then(modifier), + content = content + ) } @Composable diff --git a/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt b/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt index 10e0418f..f9e6b465 100644 --- a/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt +++ b/feature/channel/src/main/java/com/m3u/feature/channel/components/VerticalGestureArea.kt @@ -11,7 +11,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Modifier import com.m3u.feature.channel.ChannelMaskUtils.detectVerticalGesture -import com.m3u.material.ktx.tv import com.m3u.material.ktx.thenIf @Composable diff --git a/feature/crash/src/main/java/com/m3u/feature/crash/components/FileItem.kt b/feature/crash/src/main/java/com/m3u/feature/crash/components/FileItem.kt index 092801fc..42f97aa3 100644 --- a/feature/crash/src/main/java/com/m3u/feature/crash/components/FileItem.kt +++ b/feature/crash/src/main/java/com/m3u/feature/crash/components/FileItem.kt @@ -2,7 +2,7 @@ package com.m3u.feature.crash.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Adb -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text diff --git a/feature/crash/src/main/java/com/m3u/feature/crash/screen/detail/DetailScreen.kt b/feature/crash/src/main/java/com/m3u/feature/crash/screen/detail/DetailScreen.kt index 38eb2a02..d5bbf76c 100644 --- a/feature/crash/src/main/java/com/m3u/feature/crash/screen/detail/DetailScreen.kt +++ b/feature/crash/src/main/java/com/m3u/feature/crash/screen/detail/DetailScreen.kt @@ -19,7 +19,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.hilt.navigation.compose.hiltViewModel import com.m3u.material.components.Background -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.model.LocalSpacing import com.m3u.ui.MonoText diff --git a/feature/favorite/src/main/java/com/m3u/feature/favorite/FavoriteScreen.kt b/feature/favorite/src/main/java/com/m3u/feature/favorite/FavoriteScreen.kt index 78050b1c..17f8f46c 100644 --- a/feature/favorite/src/main/java/com/m3u/feature/favorite/FavoriteScreen.kt +++ b/feature/favorite/src/main/java/com/m3u/feature/favorite/FavoriteScreen.kt @@ -33,7 +33,6 @@ import com.m3u.data.service.MediaCommand import com.m3u.feature.favorite.components.FavouriteGallery import com.m3u.i18n.R import com.m3u.material.ktx.interceptVolumeEvent -import com.m3u.material.ktx.tv import com.m3u.material.ktx.thenIf import com.m3u.material.model.LocalHazeState import com.m3u.ui.EpisodesBottomSheet @@ -41,7 +40,6 @@ import com.m3u.ui.MediaSheet import com.m3u.ui.MediaSheetValue import com.m3u.ui.Sort import com.m3u.ui.SortBottomSheet -import com.m3u.ui.TvSortFullScreenDialog import com.m3u.ui.helper.Action import com.m3u.ui.helper.LocalHelper import com.m3u.ui.helper.Metadata @@ -56,8 +54,6 @@ fun FavouriteRoute( modifier: Modifier = Modifier, viewModel: FavouriteViewModel = hiltViewModel() ) { - val tv = tv() - val title = stringResource(R.string.ui_title_favourite) val helper = LocalHelper.current @@ -119,13 +115,9 @@ fun FavouriteRoute( } }, onLongClickChannel = { mediaSheetValue = MediaSheetValue.FavouriteScreen(it) }, - onClickRandomTips = { - viewModel.playRandomly() - navigateToChannel() - }, modifier = Modifier .fillMaxSize() - .thenIf(!tv && preferences.godMode) { + .thenIf(preferences.godMode) { Modifier.interceptVolumeEvent { event -> preferences.rowCount = when (event) { KeyEvent.KEYCODE_VOLUME_UP -> @@ -159,39 +151,29 @@ fun FavouriteRoute( onRefresh = { series?.let { viewModel.seriesReplay.value += 1 } }, onDismissRequest = { viewModel.series.value = null } ) - if (!tv) { - SortBottomSheet( - visible = isSortSheetVisible, - sort = sort, - sorts = sorts, - sheetState = sheetState, - onChanged = { viewModel.sort(it) }, - onDismissRequest = { isSortSheetVisible = false } - ) - MediaSheet( - value = mediaSheetValue, - onFavouriteChannel = { channel -> - viewModel.favourite(channel.id) - mediaSheetValue = MediaSheetValue.FavouriteScreen() - }, - onCreateShortcut = { channel -> - viewModel.createShortcut(context, channel.id) - mediaSheetValue = MediaSheetValue.FavouriteScreen() - }, - onDismissRequest = { - mediaSheetValue = MediaSheetValue.FavouriteScreen() - mediaSheetValue = MediaSheetValue.FavouriteScreen() - } - ) - } else { - TvSortFullScreenDialog( - visible = (mediaSheetValue as? MediaSheetValue.FavouriteScreen)?.channel != null, - sort = sort, - sorts = sorts, - onChanged = { viewModel.sort(it) }, - onDismissRequest = { mediaSheetValue = MediaSheetValue.FavouriteScreen() } - ) - } + SortBottomSheet( + visible = isSortSheetVisible, + sort = sort, + sorts = sorts, + sheetState = sheetState, + onChanged = { viewModel.sort(it) }, + onDismissRequest = { isSortSheetVisible = false } + ) + MediaSheet( + value = mediaSheetValue, + onFavouriteChannel = { channel -> + viewModel.favourite(channel.id) + mediaSheetValue = MediaSheetValue.FavouriteScreen() + }, + onCreateShortcut = { channel -> + viewModel.createShortcut(context, channel.id) + mediaSheetValue = MediaSheetValue.FavouriteScreen() + }, + onDismissRequest = { + mediaSheetValue = MediaSheetValue.FavouriteScreen() + mediaSheetValue = MediaSheetValue.FavouriteScreen() + } + ) } @Composable @@ -203,7 +185,6 @@ private fun FavoriteScreen( recently: Boolean, onClickChannel: (Channel) -> Unit, onLongClickChannel: (Channel) -> Unit, - onClickRandomTips: () -> Unit, modifier: Modifier = Modifier ) { val configuration = LocalConfiguration.current @@ -220,7 +201,6 @@ private fun FavoriteScreen( rowCount = actualRowCount, onClick = onClickChannel, onLongClick = onLongClickChannel, - onClickRandomTips = onClickRandomTips, modifier = modifier.haze( LocalHazeState.current, HazeDefaults.style(MaterialTheme.colorScheme.surface) diff --git a/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt b/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt index 66307827..c299ea1a 100644 --- a/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt +++ b/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteGallery.kt @@ -1,7 +1,5 @@ package com.m3u.feature.favorite.components -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues @@ -14,35 +12,15 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.items import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState -import androidx.compose.foundation.shape.AbsoluteRoundedCornerShape import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import com.m3u.core.util.basic.title import com.m3u.core.wrapper.Resource import com.m3u.data.database.model.Channel -import com.m3u.i18n.R.string import com.m3u.material.components.VerticalDraggableScrollbar -import com.m3u.material.ktx.tv import com.m3u.material.ktx.plus import com.m3u.material.model.LocalSpacing -import com.m3u.ui.createPremiumBrush -import androidx.tv.material3.Card as TvCard -import androidx.tv.material3.CardDefaults as TvCardDefaults -import androidx.tv.material3.Glow as TvGlow -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText @Composable internal fun FavouriteGallery( @@ -53,7 +31,6 @@ internal fun FavouriteGallery( rowCount: Int, onClick: (Channel) -> Unit, onLongClick: (Channel) -> Unit, - onClickRandomTips: () -> Unit, modifier: Modifier = Modifier ) { val spacing = LocalSpacing.current @@ -116,73 +93,3 @@ internal fun FavouriteGallery( } } } - -@Composable -private fun RandomTips( - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - val spacing = LocalSpacing.current - val tv = tv() - val title = stringResource(string.feat_favorite_play_randomly) - if (!tv) { - ListItem( - headlineContent = { - Text( - text = title.title(), - style = MaterialTheme.typography.titleSmall, - overflow = TextOverflow.Ellipsis, - maxLines = 1, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onPrimary - ) - }, - colors = ListItemDefaults.colors(Color.Transparent), - modifier = Modifier - .clip(AbsoluteRoundedCornerShape(spacing.medium)) - .clickable(onClick = onClick) - .background( - Brush.createPremiumBrush( - MaterialTheme.colorScheme.primary, - MaterialTheme.colorScheme.tertiary - ) - ) - .then(modifier) - ) - } else { - TvCard( - onClick = onClick, - glow = TvCardDefaults.glow( - TvGlow( - elevationColor = Color.Transparent, - elevation = spacing.small - ) - ), - scale = TvCardDefaults.scale( - scale = 0.95f, - focusedScale = 1f - ), - ) { - Box( - modifier = Modifier - .fillMaxWidth() - .background( - Brush.createPremiumBrush( - TvMaterialTheme.colorScheme.primary, - TvMaterialTheme.colorScheme.tertiary - ) - ) - .padding(spacing.medium) - .then(modifier) - ) { - TvText( - text = title.title(), - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - color = TvMaterialTheme.colorScheme.onPrimary - ) - } - } - } -} diff --git a/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteItem.kt b/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteItem.kt index 80e1b9c8..905a9599 100644 --- a/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteItem.kt +++ b/feature/favorite/src/main/java/com/m3u/feature/favorite/components/FavoriteItem.kt @@ -18,7 +18,6 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import com.m3u.data.database.model.Channel import com.m3u.i18n.R.string -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalSpacing import com.m3u.material.shape.AbsoluteSmoothCornerShape import kotlinx.datetime.Clock @@ -27,9 +26,6 @@ import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.hours import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds -import androidx.tv.material3.ListItem as TvListItem -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText @Composable internal fun FavoriteItem( @@ -39,35 +35,6 @@ internal fun FavoriteItem( onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier -) { - val tv = tv() - if (!tv) { - SmartphoneFavoriteItemImpl( - channel = channel, - recently = recently, - zapping = zapping, - onClick = onClick, - onLongClick = onLongClick, - modifier = modifier - ) - } else { - TvFavouriteItemImpl( - channel = channel, - onClick = onClick, - onLongClick = onLongClick, - modifier = modifier - ) - } -} - -@Composable -private fun SmartphoneFavoriteItemImpl( - channel: Channel, - recently: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit, - modifier: Modifier = Modifier, - zapping: Boolean = false ) { val spacing = LocalSpacing.current @@ -123,25 +90,3 @@ private fun SmartphoneFavoriteItemImpl( ) } } - -@Composable -private fun TvFavouriteItemImpl( - channel: Channel, - onClick: () -> Unit, - onLongClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TvListItem( - selected = false, - onClick = onClick, - onLongClick = onLongClick, - headlineContent = { - TvText( - text = channel.title, - style = TvMaterialTheme.typography.bodyMedium, - maxLines = 1 - ) - }, - modifier = modifier - ) -} diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt index 6726d689..51f94efe 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/ForyouScreen.kt @@ -46,7 +46,6 @@ import com.m3u.feature.foryou.components.recommend.RecommendGallery import com.m3u.i18n.R.string import com.m3u.material.ktx.composableOf import com.m3u.material.ktx.interceptVolumeEvent -import com.m3u.material.ktx.tv import com.m3u.material.ktx.thenIf import com.m3u.ui.EpisodesBottomSheet import com.m3u.ui.MediaSheet @@ -72,7 +71,6 @@ fun ForyouRoute( val preferences = hiltPreferences() val coroutineScope = rememberCoroutineScope() - val tv = tv() val title = stringResource(string.ui_title_foryou) val playlistCounts by viewModel.playlistCounts.collectAsStateWithLifecycle() @@ -128,7 +126,7 @@ fun ForyouRoute( onUnsubscribePlaylist = viewModel::onUnsubscribePlaylist, modifier = Modifier .fillMaxSize() - .thenIf(!tv && preferences.godMode) { + .thenIf(preferences.godMode) { Modifier.interceptVolumeEvent { event -> preferences.rowCount = when (event) { KeyEvent.KEYCODE_VOLUME_UP -> (preferences.rowCount - 1).coerceAtLeast(1) diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/PlaylistItem.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/PlaylistItem.kt index 1c2b39ed..4f24a8c9 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/PlaylistItem.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/PlaylistItem.kt @@ -1,12 +1,10 @@ package com.m3u.feature.foryou.components -import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.DriveFileMove import androidx.compose.material3.CardDefaults @@ -19,7 +17,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight @@ -30,15 +27,11 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.m3u.material.components.CircularProgressIndicator -import com.m3u.material.components.Icon -import com.m3u.material.ktx.tv +import androidx.compose.material3.Icon import com.m3u.material.model.LocalSpacing import com.m3u.material.shape.AbsoluteSmoothCornerShape import com.m3u.ui.Badge import com.m3u.ui.FontFamilies -import androidx.tv.material3.ListItem as TvListItem -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText @Composable internal fun PlaylistItem( @@ -50,42 +43,6 @@ internal fun PlaylistItem( onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier -) { - val tv = tv() - if (!tv) { - SmartphonePlaylistItemImpl( - label = label, - type = type, - count = count, - local = local, - subscribing = subscribingOrRefreshing, - onClick = onClick, - onLongClick = onLongClick, - modifier = modifier - ) - } else { - TvPlaylistItemImpl( - label = label, - type = type, - count = count, - subscribing = subscribingOrRefreshing, - onClick = onClick, - onLongClick = onLongClick, - modifier = modifier - ) - } -} - -@Composable -private fun SmartphonePlaylistItemImpl( - label: String, - type: String?, - count: Int, - local: Boolean, - subscribing: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit, - modifier: Modifier = Modifier ) { val spacing = LocalSpacing.current OutlinedCard( @@ -129,7 +86,7 @@ private fun SmartphonePlaylistItemImpl( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(spacing.extraSmall) ) { - if (subscribing) { + if (subscribingOrRefreshing) { CircularProgressIndicator( color = LocalContentColor.current, size = 8.dp @@ -181,73 +138,3 @@ private fun SmartphonePlaylistItemImpl( ) } } - -@Composable -private fun TvPlaylistItemImpl( - label: String, - type: String?, - count: Int, - subscribing: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit, - modifier: Modifier = Modifier -) { - val spacing = LocalSpacing.current - val theme = TvMaterialTheme.colorScheme - TvListItem( - selected = false, - onClick = onClick, - onLongClick = onLongClick, - headlineContent = { - TvText( - text = label, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - supportingContent = { - TvText( - text = type?.uppercase().orEmpty(), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = TvMaterialTheme.typography.bodyMedium, - fontFamily = FontFamilies.LexendExa - ) - }, - trailingContent = { - Row( - modifier = Modifier - .clip(AbsoluteSmoothCornerShape(spacing.small, 65)) - .background(theme.primary) - .padding(horizontal = spacing.small), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(spacing.extraSmall) - ) { - if (subscribing) { - CircularProgressIndicator( - color = theme.onPrimary - ) - } - TvText( - color = theme.onPrimary, - text = count.toString(), - style = TvMaterialTheme.typography.bodyMedium.copy( - lineHeightStyle = LineHeightStyle( - alignment = LineHeightStyle.Alignment.Center, - trim = LineHeightStyle.Trim.None - ) - ), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding( - bottom = 2.dp, - ), - softWrap = false, - textAlign = TextAlign.Center, - fontFamily = FontFamilies.LexendExa - ) - } - }, - modifier = modifier - ) -} diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt index 25b1b5c9..68f07987 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendGallery.kt @@ -1,11 +1,5 @@ package com.m3u.feature.foryou.components.recommend -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInHorizontally -import androidx.compose.animation.slideOutHorizontally -import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -23,11 +17,9 @@ import com.m3u.core.wrapper.eventOf import com.m3u.data.database.model.Channel import com.m3u.data.database.model.Playlist import com.m3u.material.components.HorizontalPagerIndicator -import com.m3u.material.ktx.tv import com.m3u.material.ktx.pageOffset import com.m3u.material.model.LocalSpacing import com.m3u.ui.Events -import androidx.tv.material3.Carousel as TvCarousel @Composable internal fun RecommendGallery( @@ -40,8 +32,6 @@ internal fun RecommendGallery( val spacing = LocalSpacing.current val uriHandler = LocalUriHandler.current - val tv = tv() - val onClick = { spec: Recommend.Spec -> when (spec) { is Recommend.UnseenSpec -> { @@ -59,59 +49,35 @@ internal fun RecommendGallery( } } - if (!tv) { - val state = rememberPagerState { specs.size } - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(spacing.medium) - ) { - DisposableEffect(state.currentPage) { - onSpecChanged(specs[state.currentPage]) - onDispose { - onSpecChanged(null) - } + val state = rememberPagerState { specs.size } + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(spacing.medium) + ) { + DisposableEffect(state.currentPage) { + onSpecChanged(specs[state.currentPage]) + onDispose { + onSpecChanged(null) } - HorizontalPager( - state = state, - contentPadding = PaddingValues(horizontal = spacing.medium), - modifier = Modifier.height(128.dp) - ) { page -> - val spec = specs[page] - val pageOffset = state.pageOffset(page) - RecommendItem( - spec = spec, - pageOffset = pageOffset, - onClick = { onClick(spec) } - ) - } - HorizontalPagerIndicator( - pagerState = state, - modifier = Modifier - .align(Alignment.End) - .padding(horizontal = spacing.medium), - ) } - } else { - TvCarousel( - itemCount = specs.size, - contentTransformEndToStart = - fadeIn(tween(1000)) togetherWith fadeOut(tween(1000)), - contentTransformStartToEnd = - fadeIn(tween(1000)) togetherWith fadeOut(tween(1000)), - modifier = Modifier - .padding(spacing.medium) - .then(modifier) - ) { index -> - val spec = specs[index] + HorizontalPager( + state = state, + contentPadding = PaddingValues(horizontal = spacing.medium), + modifier = Modifier.height(128.dp) + ) { page -> + val spec = specs[page] + val pageOffset = state.pageOffset(page) RecommendItem( spec = spec, - pageOffset = 0f, - onClick = { onClick(spec) }, - modifier = Modifier.animateEnterExit( - enter = slideInHorizontally(animationSpec = tween(1000)) { it / 2 }, - exit = slideOutHorizontally(animationSpec = tween(1000)) - ) + pageOffset = pageOffset, + onClick = { onClick(spec) } ) } + HorizontalPagerIndicator( + pagerState = state, + modifier = Modifier + .align(Alignment.End) + .padding(horizontal = spacing.medium), + ) } } diff --git a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt index d8ed4942..cbdbd9db 100644 --- a/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt +++ b/feature/foryou/src/main/java/com/m3u/feature/foryou/components/recommend/RecommendItem.kt @@ -43,7 +43,6 @@ import com.m3u.core.architecture.preferences.hiltPreferences import com.m3u.core.util.basic.title import com.m3u.i18n.R.string import com.m3u.material.brush.RecommendCardContainerBrush -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalSpacing import com.m3u.material.shape.AbsoluteSmoothCornerShape import com.m3u.ui.FontFamilies @@ -51,10 +50,6 @@ import com.m3u.ui.createPremiumBrush import kotlinx.datetime.Clock import kotlinx.datetime.Instant import kotlin.time.Duration.Companion.days -import androidx.tv.material3.Card as TvCard -import androidx.tv.material3.CardDefaults as TvCardDefaults -import androidx.tv.material3.CardScale as TvCardScale -import androidx.tv.material3.LocalContentColor as TvLocalContentColor @Composable internal fun RecommendItem( @@ -80,37 +75,25 @@ private fun RecommendItemLayout( content: @Composable () -> Unit ) { val spacing = LocalSpacing.current - val tv = tv() - if (!tv) { - Card( - shape = AbsoluteSmoothCornerShape(spacing.medium, 65), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant), - onClick = onClick, - modifier = Modifier - .graphicsLayer { - lerp( - start = 0.65f, - stop = 1f, - fraction = 1f - pageOffset.coerceIn(0f, 1f) - ).also { scale -> - scaleX = scale - scaleY = scale - } + Card( + shape = AbsoluteSmoothCornerShape(spacing.medium, 65), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.onSurfaceVariant), + onClick = onClick, + modifier = Modifier + .graphicsLayer { + lerp( + start = 0.65f, + stop = 1f, + fraction = 1f - pageOffset.coerceIn(0f, 1f) + ).also { scale -> + scaleX = scale + scaleY = scale } - .fillMaxHeight() - .then(modifier), - content = { content() } - ) - } else { - TvCard( - scale = TvCardScale.None, - shape = TvCardDefaults.shape(AbsoluteSmoothCornerShape(spacing.medium, 65)), - onClick = onClick, - modifier = modifier, - content = { content() } - ) - } - + } + .fillMaxHeight() + .then(modifier), + content = { content() } + ) } @Composable @@ -186,7 +169,6 @@ fun UnseenContent(spec: Recommend.UnseenSpec) { ) CompositionLocalProvider( LocalContentColor provides Color.White, - TvLocalContentColor provides Color.White, ) { info() } diff --git a/feature/playlist-configuration/src/main/java/com/m3u/feature/playlist/configuration/PlaylistConfigurationScreen.kt b/feature/playlist-configuration/src/main/java/com/m3u/feature/playlist/configuration/PlaylistConfigurationScreen.kt index ed5b27e3..c2fb4085 100644 --- a/feature/playlist-configuration/src/main/java/com/m3u/feature/playlist/configuration/PlaylistConfigurationScreen.kt +++ b/feature/playlist-configuration/src/main/java/com/m3u/feature/playlist/configuration/PlaylistConfigurationScreen.kt @@ -56,7 +56,7 @@ import com.m3u.feature.playlist.configuration.components.SyncProgrammesButton import com.m3u.feature.playlist.configuration.components.XtreamPanel import com.m3u.i18n.R.string import com.m3u.material.components.Background -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.components.PlaceholderField import com.m3u.material.ktx.checkPermissionOrRationale import com.m3u.material.model.LocalHazeState diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt index 22ac2928..3e1da978 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt @@ -4,6 +4,8 @@ package com.m3u.feature.playlist import android.Manifest import android.content.Intent +import android.content.res.Configuration.ORIENTATION_LANDSCAPE +import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.content.res.Configuration.UI_MODE_TYPE_APPLIANCE import android.content.res.Configuration.UI_MODE_TYPE_CAR import android.content.res.Configuration.UI_MODE_TYPE_DESK @@ -15,13 +17,30 @@ import android.os.Build import android.provider.Settings import android.view.KeyEvent import androidx.activity.compose.BackHandler +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +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.PaddingValues +import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.material.BackdropScaffold +import androidx.compose.material.BackdropValue import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Sort import androidx.compose.material.icons.rounded.KeyboardDoubleArrowUp +import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.rememberBackdropScaffoldState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.InternalComposeApi @@ -32,14 +51,29 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LifecycleResumeEffect @@ -58,29 +92,37 @@ import com.m3u.data.database.model.isSeries import com.m3u.data.database.model.isVod import com.m3u.data.database.model.type import com.m3u.data.service.MediaCommand -import com.m3u.feature.playlist.internal.SmartphonePlaylistScreenImpl -import com.m3u.feature.playlist.internal.TvPlaylistScreenImpl +import com.m3u.feature.playlist.components.PlaylistTabRow +import com.m3u.feature.playlist.components.ChannelGallery import com.m3u.i18n.R.string +import com.m3u.material.components.TextField import com.m3u.material.ktx.checkPermissionOrRationale -import com.m3u.material.ktx.createScheme import com.m3u.material.ktx.interceptVolumeEvent -import com.m3u.material.ktx.tv +import com.m3u.material.ktx.isAtTop +import com.m3u.material.ktx.only +import com.m3u.material.ktx.split import com.m3u.material.ktx.thenIf +import com.m3u.material.model.LocalHazeState import com.m3u.material.model.LocalSpacing -import com.m3u.material.model.asTvScheme import com.m3u.ui.Destination import com.m3u.ui.EpisodesBottomSheet +import com.m3u.ui.EventHandler +import com.m3u.ui.MediaSheet +import com.m3u.ui.MediaSheetValue import com.m3u.ui.Sort +import com.m3u.ui.SortBottomSheet +import com.m3u.ui.helper.Action import com.m3u.ui.helper.Fob import com.m3u.ui.helper.LocalHelper import com.m3u.ui.helper.Metadata +import dev.chrisbanes.haze.HazeDefaults +import dev.chrisbanes.haze.haze import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlin.time.Duration.Companion.milliseconds -import androidx.tv.material3.MaterialTheme as TvMaterialTheme @Composable internal fun PlaylistRoute( @@ -93,11 +135,9 @@ internal fun PlaylistRoute( val preferences = hiltPreferences() val helper = LocalHelper.current val coroutineScope = rememberCoroutineScope() - val colorScheme = TvMaterialTheme.colorScheme + val colorScheme = MaterialTheme.colorScheme val lifecycleOwner = LocalLifecycleOwner.current - val tv = tv() - val zapping by viewModel.zapping.collectAsStateWithLifecycle() val playlistUrl by viewModel.playlistUrl.collectAsStateWithLifecycle() val playlist by viewModel.playlist.collectAsStateWithLifecycle() @@ -120,7 +160,8 @@ internal fun PlaylistRoute( val query by viewModel.query.collectAsStateWithLifecycle() val scrollUp by viewModel.scrollUp.collectAsStateWithLifecycle() - val writeExternalPermission = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) + val writeExternalPermission = + rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) val postNotificationPermission = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) null else rememberPermissionState(Manifest.permission.POST_NOTIFICATIONS) @@ -241,13 +282,12 @@ internal fun PlaylistRoute( } }, createShortcut = { id -> viewModel.createShortcut(context, id) }, - createTvRecommend = { id -> viewModel.createTvRecommend(helper.activityContext, id) }, isVodPlaylist = isVodPlaylist, isSeriesPlaylist = isSeriesPlaylist, getProgrammeCurrently = { channelId -> viewModel.getProgrammeCurrently(channelId) }, modifier = Modifier .fillMaxSize() - .thenIf(!tv && preferences.godMode) { + .thenIf(preferences.godMode) { Modifier.interceptVolumeEvent { event -> preferences.rowCount = when (event) { KeyEvent.KEYCODE_VOLUME_UP -> @@ -311,7 +351,6 @@ private fun PlaylistScreen( hide: (channelId: Int) -> Unit, savePicture: (channelId: Int) -> Unit, createShortcut: (channelId: Int) -> Unit, - createTvRecommend: (channelId: Int) -> Unit, contentPadding: PaddingValues, isVodPlaylist: Boolean, isSeriesPlaylist: Boolean, @@ -343,61 +382,216 @@ private fun PlaylistScreen( onDispose { Metadata.fob = null } } - val tv = tv() - if (!tv) { - SmartphonePlaylistScreenImpl( - categoryWithChannels = categoryWithChannels, - pinnedCategories = pinnedCategories, - onPinOrUnpinCategory = onPinOrUnpinCategory, - onHideCategory = onHideCategory, - zapping = zapping, - query = query, - onQuery = onQuery, - rowCount = rowCount, - scrollUp = scrollUp, - contentPadding = contentPadding, - onPlayChannel = onPlayChannel, - isAtTopState = isAtTopState, - refreshing = refreshing, - onRefresh = onRefresh, - sorts = sorts, - sort = sort, - onSort = onSort, - favourite = favourite, - onHide = hide, - onSaveCover = savePicture, - onCreateShortcut = createShortcut, - isVodOrSeriesPlaylist = isVodPlaylist || isSeriesPlaylist, - getProgrammeCurrently = getProgrammeCurrently, - modifier = modifier - ) - } else { - val preferences = hiltPreferences() - TvMaterialTheme( - colorScheme = remember(preferences.argb) { - createScheme(preferences.argb, true).asTvScheme() + val spacing = LocalSpacing.current + val configuration = LocalConfiguration.current + val focusManager = LocalFocusManager.current + + val scaffoldState = rememberBackdropScaffoldState( + initialValue = BackdropValue.Concealed + ) + val connection = remember { + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + return if (scaffoldState.isRevealed) available + else Offset.Zero } - ) { - TvPlaylistScreenImpl( - title = title, - categoryWithChannels = categoryWithChannels, - query = query, - onQuery = onQuery, - onPlayChannel = onPlayChannel, - onRefresh = onRefresh, - sorts = sorts, - sort = sort, - onSort = onSort, - favorite = favourite, - hide = hide, - savePicture = savePicture, - createTvRecommend = createTvRecommend, - isVodOrSeriesPlaylist = isVodPlaylist || isSeriesPlaylist, - getProgrammeCurrently = getProgrammeCurrently, - modifier = modifier - ) } } + val currentColor = MaterialTheme.colorScheme.background + val currentContentColor = MaterialTheme.colorScheme.onBackground + + val sheetState = rememberModalBottomSheetState() + + var mediaSheetValue: MediaSheetValue.PlaylistScreen by remember { mutableStateOf(MediaSheetValue.PlaylistScreen()) } + var isSortSheetVisible by rememberSaveable { mutableStateOf(false) } + + LifecycleResumeEffect(refreshing) { + Metadata.actions = buildList { + Action( + icon = Icons.AutoMirrored.Rounded.Sort, + contentDescription = "sort", + onClick = { isSortSheetVisible = true } + ).also { add(it) } + Action( + icon = Icons.Rounded.Refresh, + enabled = !refreshing, + contentDescription = "refresh", + onClick = onRefresh + ).also { add(it) } + } + onPauseOrDispose { + Metadata.actions = emptyList() + } + } + + val categories = remember(categoryWithChannels) { categoryWithChannels.map { it.category } } + var category by remember(categories) { mutableStateOf(categories.firstOrNull().orEmpty()) } + + val (inner, outer) = contentPadding split WindowInsetsSides.Bottom + + BackdropScaffold( + scaffoldState = scaffoldState, + gesturesEnabled = isAtTopState.value, + appBar = {}, + frontLayerShape = RectangleShape, + peekHeight = 0.dp, + backLayerContent = { + val coroutineScope = rememberCoroutineScope() + val focusRequester = remember { FocusRequester() } + LaunchedEffect(scaffoldState.currentValue) { + if (scaffoldState.isConcealed) { + focusManager.clearFocus() + } else { + focusRequester.requestFocus() + } + } + BackHandler(scaffoldState.isRevealed || query.isNotEmpty()) { + if (scaffoldState.isRevealed) { + coroutineScope.launch { + scaffoldState.conceal() + } + } + if (query.isNotEmpty()) { + onQuery("") + } + } + Box( + modifier = Modifier + .padding(spacing.medium) + .fillMaxWidth() + ) { + TextField( + text = query, + onValueChange = onQuery, + fontWeight = FontWeight.Bold, + placeholder = stringResource(string.feat_playlist_query_placeholder).uppercase(), + modifier = Modifier + .focusRequester(focusRequester) + .heightIn(max = 48.dp) + ) + } + }, + frontLayerContent = { + val state = rememberLazyStaggeredGridState() + LaunchedEffect(Unit) { + snapshotFlow { state.isAtTop } + .onEach { isAtTopState.value = it } + .launchIn(this) + } + EventHandler(scrollUp) { + state.scrollToItem(0) + } + val orientation = configuration.orientation + val actualRowCount = remember(orientation, rowCount) { + when (orientation) { + ORIENTATION_LANDSCAPE -> rowCount + 2 + ORIENTATION_PORTRAIT -> rowCount + else -> rowCount + } + } + var isExpanded by remember(sort == Sort.MIXED) { + mutableStateOf(false) + } + BackHandler(isExpanded) { isExpanded = false } + + val tabs = @Composable { + PlaylistTabRow( + selectedCategory = category, + categories = categories, + isExpanded = isExpanded, + bottomContentPadding = contentPadding only WindowInsetsSides.Bottom, + onExpanded = { isExpanded = !isExpanded }, + onCategoryChanged = { category = it }, + pinnedCategories = pinnedCategories, + onPinOrUnpinCategory = onPinOrUnpinCategory, + onHideCategory = onHideCategory + ) + } + + val gallery = @Composable { + val channel = remember(categoryWithChannels, category) { + categoryWithChannels.find { it.category == category } + } + ChannelGallery( + state = state, + rowCount = actualRowCount, + categoryWithChannels = channel, + zapping = zapping, + recently = sort == Sort.RECENTLY, + isVodOrSeriesPlaylist = isVodPlaylist || isSeriesPlaylist, + onClick = onPlayChannel, + contentPadding = inner, + onLongClick = { + mediaSheetValue = MediaSheetValue.PlaylistScreen(it) + }, + getProgrammeCurrently = getProgrammeCurrently, + modifier = Modifier.haze( + LocalHazeState.current, + HazeDefaults.style(MaterialTheme.colorScheme.surface) + ) + ) + } + Column( + Modifier.background(MaterialTheme.colorScheme.surfaceContainerHighest) + ) { + if (!isExpanded) { + AnimatedVisibility( + visible = categories.size > 1, + enter = fadeIn(animationSpec = tween(400)) + ) { + tabs() + } + gallery() + } else { + AnimatedVisibility( + visible = categories.size > 1, + enter = fadeIn(animationSpec = tween(400)) + ) { + tabs() + } + } + } + }, + backLayerBackgroundColor = Color.Transparent, + backLayerContentColor = currentContentColor, + frontLayerScrimColor = currentColor.copy(alpha = 0.45f), + frontLayerBackgroundColor = Color.Transparent, + modifier = modifier + .padding(outer) + .nestedScroll( + connection = connection + ) + ) + + SortBottomSheet( + visible = isSortSheetVisible, + sort = sort, + sorts = sorts, + sheetState = sheetState, + onChanged = onSort, + onDismissRequest = { isSortSheetVisible = false } + ) + + MediaSheet( + value = mediaSheetValue, + onFavouriteChannel = { channel -> + favourite(channel.id) + mediaSheetValue = MediaSheetValue.PlaylistScreen() + }, + onHideChannel = { channel -> + hide(channel.id) + mediaSheetValue = MediaSheetValue.PlaylistScreen() + }, + onSaveChannelCover = { channel -> + savePicture(channel.id) + mediaSheetValue = MediaSheetValue.PlaylistScreen() + }, + onCreateShortcut = { channel -> + createShortcut(channel.id) + mediaSheetValue = MediaSheetValue.PlaylistScreen() + }, + onDismissRequest = { mediaSheetValue = MediaSheetValue.PlaylistScreen() } + ) } @Composable diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/TvPlaylistActivity.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/TvPlaylistActivity.kt deleted file mode 100644 index 4e615441..00000000 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/TvPlaylistActivity.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.m3u.feature.playlist - -import android.app.ActivityOptions -import android.content.ComponentName -import android.content.Intent -import android.content.res.Configuration -import android.os.Bundle -import androidx.activity.compose.setContent -import androidx.activity.enableEdgeToEdge -import androidx.activity.viewModels -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.lifecycleScope -import com.m3u.core.Contracts -import com.m3u.ui.Events.enableDPadReaction -import com.m3u.ui.Toolkit -import com.m3u.ui.helper.Helper -import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.launch - -@AndroidEntryPoint -class TvPlaylistActivity : AppCompatActivity() { - private val helper: Helper = Helper(this) - private val viewModel: PlaylistViewModel by viewModels() - - override fun onCreate(savedInstanceState: Bundle?) { - enableEdgeToEdge() - enableDPadReaction() - super.onCreate(savedInstanceState) - handleIntent(intent) - setContent { - Toolkit(helper) { - PlaylistRoute( - viewModel = viewModel, - navigateToChannel = ::navigateToChannel - ) - } - } - } - - private fun handleIntent(intent: Intent) { - val intentAction = intent.action - if (intentAction == Intent.ACTION_VIEW) { - val intentData = intent.data - val pathSegments = intentData?.pathSegments ?: emptyList() - when (pathSegments.firstOrNull()) { - "discover" -> { - val channelId = pathSegments[1].toIntOrNull() ?: return - viewModel.setup(channelId) { - lifecycleScope.launch { - helper.play(it) - navigateToChannel() - } - } - } - } - } - } - - private fun navigateToChannel() { - val options = ActivityOptions.makeCustomAnimation( - this, - 0, - 0 - ) - startActivity( - Intent().apply { - component = ComponentName.createRelative( - this@TvPlaylistActivity, - Contracts.PLAYER_ACTIVITY - ) - }, - options.toBundle() - ) - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - helper.applyConfiguration() - } -} \ No newline at end of file diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelGallery.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ChannelGallery.kt similarity index 98% rename from feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelGallery.kt rename to feature/playlist/src/main/java/com/m3u/feature/playlist/components/ChannelGallery.kt index 836fde1d..2b90e284 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelGallery.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ChannelGallery.kt @@ -29,7 +29,7 @@ import com.m3u.material.ktx.plus import com.m3u.material.model.LocalSpacing @Composable -internal fun SmartphoneChannelGallery( +internal fun ChannelGallery( state: LazyStaggeredGridState, rowCount: Int, categoryWithChannels: PlaylistViewModel.CategoryWithChannels?, @@ -80,7 +80,7 @@ internal fun SmartphoneChannelGallery( ) { value = currentGetProgrammeCurrently(channel.id) } - SmartphoneChannelItem( + ChannelItem( channel = channel, programme = programme, recently = recently, diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelItem.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ChannelItem.kt similarity index 99% rename from feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelItem.kt rename to feature/playlist/src/main/java/com/m3u/feature/playlist/components/ChannelItem.kt index fbe0e3ec..83b7034f 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelItem.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ChannelItem.kt @@ -45,7 +45,7 @@ import com.m3u.data.database.model.Programme import com.m3u.data.database.model.Channel import com.m3u.i18n.R.string import com.m3u.material.components.CircularProgressIndicator -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.model.LocalSpacing import com.m3u.material.shape.AbsoluteSmoothCornerShape import com.m3u.ui.util.TimeUtils.formatEOrSh @@ -59,7 +59,7 @@ import kotlin.time.Duration.Companion.minutes import kotlin.time.Duration.Companion.seconds @Composable -internal fun SmartphoneChannelItem( +internal fun ChannelItem( channel: Channel, recently: Boolean, zapping: Boolean, diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt deleted file mode 100644 index 157bf233..00000000 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt +++ /dev/null @@ -1,159 +0,0 @@ -package com.m3u.feature.playlist.components - -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 -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Sort -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material.icons.rounded.Search -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.produceState -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.Dp -import coil.compose.AsyncImage -import coil.request.ImageRequest -import com.m3u.core.architecture.preferences.hiltPreferences -import com.m3u.data.database.model.Programme -import com.m3u.data.database.model.Channel -import com.m3u.material.brush.ImmersiveBackgroundBrush -import com.m3u.material.components.IconButton -import com.m3u.material.model.LocalSpacing -import com.m3u.ui.SnackHost -import androidx.tv.material3.LocalContentColor as TvLocalContentColor -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText - -@Composable -internal fun ImmersiveBackground( - title: String, - channel: Channel?, - maxBrowserHeight: Dp, - onRefresh: () -> Unit, - openSearchDrawer: () -> Unit, - openSortDrawer: () -> Unit, - getProgrammeCurrently: suspend (channelId: Int) -> Programme?, - modifier: Modifier = Modifier -) { - val context = LocalContext.current - val spacing = LocalSpacing.current - val preferences = hiltPreferences() - - val noPictureMode = preferences.noPictureMode - - val currentGetProgrammeCurrently by rememberUpdatedState(getProgrammeCurrently) - - Box(modifier) { - if (channel != null) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.TopEnd - ) { - if (!noPictureMode) { - val request = remember(channel.cover) { - ImageRequest.Builder(context) - .data(channel.cover.orEmpty()) - .crossfade(1600) - .build() - } - AsyncImage( - model = request, - contentScale = ContentScale.Crop, - contentDescription = channel.title, - modifier = Modifier - .fillMaxWidth(0.78f) - .aspectRatio(16 / 9f) - .drawWithCache { - onDrawWithContent { - drawContent() - drawRect(brush = ImmersiveBackgroundBrush(size)) - } - } - ) - } - - Column( - Modifier - .align(Alignment.BottomCenter) - .padding(spacing.medium) - .fillMaxWidth() - ) { - TvText( - text = channel.title, - style = TvMaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.ExtraBold, - maxLines = 1 - ) - - val programme: Programme? by produceState( - initialValue = null, - key1 = channel.id - ) { - value = currentGetProgrammeCurrently(channel.id) - } - - programme?.let { - TvText( - text = it.readText(), - style = TvMaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.SemiBold, - color = TvLocalContentColor.current.copy(0.67f), - maxLines = 1 - ) - } - Spacer( - modifier = Modifier.heightIn(min = maxBrowserHeight) - ) - } - } - } - Column( - modifier = Modifier.padding(spacing.medium), - verticalArrangement = Arrangement.spacedBy(spacing.medium) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(spacing.small), - ) { - TvText( - text = title, - style = TvMaterialTheme.typography.headlineLarge, - fontWeight = FontWeight.ExtraBold, - maxLines = 1 - ) - Spacer(modifier = Modifier.weight(1f)) - IconButton( - icon = Icons.Rounded.Search, - contentDescription = "search", - onClick = openSearchDrawer - ) - IconButton( - icon = Icons.AutoMirrored.Rounded.Sort, - contentDescription = "sort", - onClick = openSortDrawer - ) - IconButton( - icon = Icons.Rounded.Refresh, - contentDescription = "refresh", - onClick = onRefresh - ) - } - SnackHost() - } - } -} \ No newline at end of file diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/PlaylistTabRow.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/PlaylistTabRow.kt index cd65ddb1..98219663 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/PlaylistTabRow.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/PlaylistTabRow.kt @@ -28,6 +28,7 @@ import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -50,7 +51,7 @@ import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import com.m3u.material.components.IconButton +import androidx.compose.material3.IconButton import com.m3u.material.effects.BackStackEntry import com.m3u.material.effects.BackStackHandler import com.m3u.material.ktx.Edge @@ -99,28 +100,37 @@ internal fun PlaylistTabRow( horizontalArrangement = Arrangement.End ) { IconButton( - icon = Icons.Rounded.PushPin, - contentDescription = "pin", onClick = { name.let(onPinOrUnpinCategory) focusCategory = null } - ) + ) { + Icon( + imageVector = Icons.Rounded.PushPin, + contentDescription = "pin" + ) + } IconButton( - icon = Icons.Rounded.VisibilityOff, - contentDescription = "hide", onClick = { name.let(onHideCategory) focusCategory = null } - ) + ) { + Icon( + imageVector = Icons.Rounded.VisibilityOff, + contentDescription = "hide" + ) + } } } else { IconButton( - icon = Icons.Rounded.Menu, - contentDescription = "", onClick = onExpanded - ) + ) { + Icon( + imageVector = Icons.Rounded.Menu, + contentDescription = "" + ) + } } } } diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelGallery.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelGallery.kt deleted file mode 100644 index 9e814e42..00000000 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelGallery.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.m3u.feature.playlist.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusChanged -import androidx.compose.ui.unit.Dp -import androidx.paging.compose.collectAsLazyPagingItems -import androidx.tv.material3.MaterialTheme -import androidx.tv.material3.Text -import com.m3u.data.database.model.Channel -import com.m3u.feature.playlist.PlaylistViewModel -import com.m3u.material.model.LocalSpacing - -@Composable -internal fun TvChannelGallery( - categoryWithChannels: List, - maxBrowserHeight: Dp, - isSpecifiedSort: Boolean, - isVodOrSeriesPlaylist: Boolean, - onClick: (Channel) -> Unit, - onLongClick: (Channel) -> Unit, - onFocus: (Channel) -> Unit, - modifier: Modifier = Modifier, -) { - val spacing = LocalSpacing.current - val multiCategories = categoryWithChannels.size > 1 - - LazyColumn( - verticalArrangement = Arrangement.spacedBy(spacing.medium), - contentPadding = PaddingValues(vertical = spacing.medium), - modifier = Modifier - .heightIn(max = maxBrowserHeight) - .fillMaxWidth() - .then(modifier) - ) { - items(categoryWithChannels) { (category, flow) -> - val channels = flow.collectAsLazyPagingItems() - if (multiCategories && channels.itemCount > 0) { - Text( - text = category, - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier.padding(spacing.medium) - ) - } - LazyRow( - horizontalArrangement = Arrangement.spacedBy(spacing.medium), - contentPadding = PaddingValues(horizontal = spacing.medium), - modifier = Modifier.fillMaxWidth() - ) { - items(channels.itemCount) { index -> - val channel = channels[index] - if (channel != null) { - TvChannelItem( - channel = channel, - isVodOrSeriesPlaylist = isVodOrSeriesPlaylist, - isGridLayout = false, - onClick = { onClick(channel) }, - onLongClick = { onLongClick(channel) }, - modifier = Modifier.onFocusChanged { - if (it.hasFocus) { - onFocus(channel) - } - } - ) - } else { - // TODO: placeholder - } - } - } - } - } -} diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelItem.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelItem.kt deleted file mode 100644 index 084543d8..00000000 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/TvChannelItem.kt +++ /dev/null @@ -1,148 +0,0 @@ -package com.m3u.feature.playlist.components - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -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.layout.widthIn -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.BrokenImage -import androidx.compose.runtime.Composable -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.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import androidx.tv.material3.Border as TvBorder -import androidx.tv.material3.Card as TvCard -import androidx.tv.material3.CardDefaults as TvCardDefaults -import androidx.tv.material3.Glow as TvGlow -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText -import coil.compose.SubcomposeAsyncImage -import coil.request.ImageRequest -import com.m3u.core.architecture.preferences.hiltPreferences -import com.m3u.data.database.model.Channel -import com.m3u.material.components.CircularProgressIndicator -import com.m3u.material.components.Icon -import com.m3u.material.ktx.thenIf -import com.m3u.material.model.LocalSpacing -import coil.size.Size as CoilSize - -@Composable -internal fun TvChannelItem( - channel: Channel, - isVodOrSeriesPlaylist: Boolean, - isGridLayout: Boolean, - onClick: () -> Unit, - onLongClick: () -> Unit, - modifier: Modifier = Modifier, -) { - val context = LocalContext.current - val preferences = hiltPreferences() - val spacing = LocalSpacing.current - - val noPictureMode = preferences.noPictureMode - val isCoverExisted = !channel.cover.isNullOrEmpty() - - TvCard( - onClick = onClick, - onLongClick = onLongClick, - glow = TvCardDefaults.glow( - TvGlow( - elevationColor = Color.Transparent, - elevation = spacing.small - ) - ), - scale = TvCardDefaults.scale( - scale = 0.95f, - focusedScale = 1.1f - ), - border = TvCardDefaults.border( - if (channel.favourite) TvBorder( - BorderStroke(3.dp, TvMaterialTheme.colorScheme.border), - ) - else TvBorder.None - ), - modifier = Modifier - .thenIf(!noPictureMode) { - if (isGridLayout) Modifier.width(128.dp) - else Modifier.height(128.dp) - } - .then(modifier) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxSize() - ) { - if (!isCoverExisted || noPictureMode) { - TvText( - text = channel.title, - textAlign = TextAlign.Center, - modifier = Modifier - .widthIn(86.dp) - .padding(spacing.medium), - maxLines = 1 - ) - } else { - SubcomposeAsyncImage( - model = remember(channel.cover) { - ImageRequest.Builder(context) - .data(channel.cover) - .size(CoilSize.ORIGINAL) - .build() - }, - contentScale = if (isGridLayout) ContentScale.FillWidth - else ContentScale.FillHeight, - contentDescription = channel.title, - loading = { - Column( - verticalArrangement = Arrangement.SpaceAround, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .padding(spacing.medium) - ) { - TvText( - text = channel.title, - maxLines = 1 - ) - CircularProgressIndicator() - } - }, - error = { - Column( - verticalArrangement = Arrangement.SpaceAround, - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .padding(spacing.medium) - ) { - TvText( - text = channel.title, - maxLines = 1 - ) - Icon( - imageVector = Icons.Rounded.BrokenImage, - contentDescription = null - ) - } - }, - modifier = Modifier.then( - if (isGridLayout) Modifier.fillMaxWidth() - else Modifier.fillMaxHeight() - ) - ) - } - } - } -} diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt deleted file mode 100644 index f08eeeee..00000000 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt +++ /dev/null @@ -1,318 +0,0 @@ -@file:Suppress("UsingMaterialAndMaterial3Libraries") - -package com.m3u.feature.playlist.internal - -import android.content.res.Configuration.ORIENTATION_LANDSCAPE -import android.content.res.Configuration.ORIENTATION_PORTRAIT -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState -import androidx.compose.material.BackdropScaffold -import androidx.compose.material.BackdropValue -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Sort -import androidx.compose.material.icons.rounded.Refresh -import androidx.compose.material.rememberBackdropScaffoldState -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.input.nestedscroll.NestedScrollConnection -import androidx.compose.ui.input.nestedscroll.NestedScrollSource -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.LifecycleResumeEffect -import com.m3u.core.wrapper.Event -import com.m3u.data.database.model.Programme -import com.m3u.data.database.model.Channel -import com.m3u.feature.playlist.PlaylistViewModel -import com.m3u.feature.playlist.components.PlaylistTabRow -import com.m3u.feature.playlist.components.SmartphoneChannelGallery -import com.m3u.i18n.R.string -import com.m3u.material.components.TextField -import com.m3u.material.ktx.isAtTop -import com.m3u.material.ktx.only -import com.m3u.material.ktx.split -import com.m3u.material.model.LocalHazeState -import com.m3u.material.model.LocalSpacing -import com.m3u.ui.EventHandler -import com.m3u.ui.MediaSheet -import com.m3u.ui.MediaSheetValue -import com.m3u.ui.Sort -import com.m3u.ui.SortBottomSheet -import com.m3u.ui.helper.Action -import com.m3u.ui.helper.Metadata -import dev.chrisbanes.haze.HazeDefaults -import dev.chrisbanes.haze.haze -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch - -@Composable -@InternalComposeApi -internal fun SmartphonePlaylistScreenImpl( - categoryWithChannels: List, - pinnedCategories: List, - onPinOrUnpinCategory: (String) -> Unit, - onHideCategory: (String) -> Unit, - zapping: Channel?, - query: String, - onQuery: (String) -> Unit, - rowCount: Int, - scrollUp: Event, - sorts: List, - sort: Sort, - onSort: (Sort) -> Unit, - onPlayChannel: (Channel) -> Unit, - refreshing: Boolean, - onRefresh: () -> Unit, - favourite: (channelId: Int) -> Unit, - onHide: (channelId: Int) -> Unit, - onSaveCover: (channelId: Int) -> Unit, - onCreateShortcut: (channelId: Int) -> Unit, - isAtTopState: MutableState, - isVodOrSeriesPlaylist: Boolean, - getProgrammeCurrently: suspend (channelId: Int) -> Programme?, - modifier: Modifier = Modifier, - contentPadding: PaddingValues = PaddingValues() -) { - val spacing = LocalSpacing.current - val configuration = LocalConfiguration.current - val focusManager = LocalFocusManager.current - - val scaffoldState = rememberBackdropScaffoldState( - initialValue = BackdropValue.Concealed - ) - val connection = remember { - object : NestedScrollConnection { - override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { - return if (scaffoldState.isRevealed) available - else Offset.Zero - } - } - } - val currentColor = MaterialTheme.colorScheme.background - val currentContentColor = MaterialTheme.colorScheme.onBackground - - val sheetState = rememberModalBottomSheetState() - - var mediaSheetValue: MediaSheetValue.PlaylistScreen by remember { mutableStateOf(MediaSheetValue.PlaylistScreen()) } - var isSortSheetVisible by rememberSaveable { mutableStateOf(false) } - - LifecycleResumeEffect(refreshing) { - Metadata.actions = buildList { - Action( - icon = Icons.AutoMirrored.Rounded.Sort, - contentDescription = "sort", - onClick = { isSortSheetVisible = true } - ).also { add(it) } - Action( - icon = Icons.Rounded.Refresh, - enabled = !refreshing, - contentDescription = "refresh", - onClick = onRefresh - ).also { add(it) } - } - onPauseOrDispose { - Metadata.actions = emptyList() - } - } - - val categories = remember(categoryWithChannels) { categoryWithChannels.map { it.category } } - var category by remember(categories) { mutableStateOf(categories.firstOrNull().orEmpty()) } - - val (inner, outer) = contentPadding split WindowInsetsSides.Bottom - - BackdropScaffold( - scaffoldState = scaffoldState, - gesturesEnabled = isAtTopState.value, - appBar = {}, - frontLayerShape = RectangleShape, - peekHeight = 0.dp, - backLayerContent = { - val coroutineScope = rememberCoroutineScope() - val focusRequester = remember { FocusRequester() } - LaunchedEffect(scaffoldState.currentValue) { - if (scaffoldState.isConcealed) { - focusManager.clearFocus() - } else { - focusRequester.requestFocus() - } - } - BackHandler(scaffoldState.isRevealed || query.isNotEmpty()) { - if (scaffoldState.isRevealed) { - coroutineScope.launch { - scaffoldState.conceal() - } - } - if (query.isNotEmpty()) { - onQuery("") - } - } - Box( - modifier = Modifier - .padding(spacing.medium) - .fillMaxWidth() - ) { - TextField( - text = query, - onValueChange = onQuery, - fontWeight = FontWeight.Bold, - placeholder = stringResource(string.feat_playlist_query_placeholder).uppercase(), - modifier = Modifier - .focusRequester(focusRequester) - .heightIn(max = 48.dp) - ) - } - }, - frontLayerContent = { - val state = rememberLazyStaggeredGridState() - LaunchedEffect(Unit) { - snapshotFlow { state.isAtTop } - .onEach { isAtTopState.value = it } - .launchIn(this) - } - EventHandler(scrollUp) { - state.scrollToItem(0) - } - val orientation = configuration.orientation - val actualRowCount = remember(orientation, rowCount) { - when (orientation) { - ORIENTATION_LANDSCAPE -> rowCount + 2 - ORIENTATION_PORTRAIT -> rowCount - else -> rowCount - } - } - var isExpanded by remember(sort == Sort.MIXED) { - mutableStateOf(false) - } - BackHandler(isExpanded) { isExpanded = false } - - val tabs = @Composable { - PlaylistTabRow( - selectedCategory = category, - categories = categories, - isExpanded = isExpanded, - bottomContentPadding = contentPadding only WindowInsetsSides.Bottom, - onExpanded = { isExpanded = !isExpanded }, - onCategoryChanged = { category = it }, - pinnedCategories = pinnedCategories, - onPinOrUnpinCategory = onPinOrUnpinCategory, - onHideCategory = onHideCategory - ) - } - - val gallery = @Composable { - val channel = remember(categoryWithChannels, category) { - categoryWithChannels.find { it.category == category } - } - SmartphoneChannelGallery( - state = state, - rowCount = actualRowCount, - categoryWithChannels = channel, - zapping = zapping, - recently = sort == Sort.RECENTLY, - isVodOrSeriesPlaylist = isVodOrSeriesPlaylist, - onClick = onPlayChannel, - contentPadding = inner, - onLongClick = { - mediaSheetValue = MediaSheetValue.PlaylistScreen(it) - }, - getProgrammeCurrently = getProgrammeCurrently, - modifier = Modifier.haze( - LocalHazeState.current, - HazeDefaults.style(MaterialTheme.colorScheme.surface) - ) - ) - } - Column( - Modifier.background(MaterialTheme.colorScheme.surfaceContainerHighest) - ) { - if (!isExpanded) { - AnimatedVisibility( - visible = categories.size > 1, - enter = fadeIn(animationSpec = tween(400)) - ) { - tabs() - } - gallery() - } else { - AnimatedVisibility( - visible = categories.size > 1, - enter = fadeIn(animationSpec = tween(400)) - ) { - tabs() - } - } - } - }, - backLayerBackgroundColor = Color.Transparent, - backLayerContentColor = currentContentColor, - frontLayerScrimColor = currentColor.copy(alpha = 0.45f), - frontLayerBackgroundColor = Color.Transparent, - modifier = modifier - .padding(outer) - .nestedScroll( - connection = connection - ) - ) - - SortBottomSheet( - visible = isSortSheetVisible, - sort = sort, - sorts = sorts, - sheetState = sheetState, - onChanged = onSort, - onDismissRequest = { isSortSheetVisible = false } - ) - - MediaSheet( - value = mediaSheetValue, - onFavouriteChannel = { channel -> - favourite(channel.id) - mediaSheetValue = MediaSheetValue.PlaylistScreen() - }, - onHideChannel = { channel -> - onHide(channel.id) - mediaSheetValue = MediaSheetValue.PlaylistScreen() - }, - onSaveChannelCover = { channel -> - onSaveCover(channel.id) - mediaSheetValue = MediaSheetValue.PlaylistScreen() - }, - onCreateShortcut = { channel -> - onCreateShortcut(channel.id) - mediaSheetValue = MediaSheetValue.PlaylistScreen() - }, - onDismissRequest = { mediaSheetValue = MediaSheetValue.PlaylistScreen() } - ) -} diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt deleted file mode 100644 index 06fbb25c..00000000 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt +++ /dev/null @@ -1,307 +0,0 @@ -package com.m3u.feature.playlist.internal - -import androidx.activity.compose.BackHandler -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Shortcut -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Favorite -import androidx.compose.material.icons.rounded.Image -import androidx.compose.runtime.Composable -import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.tv.material3.DenseListItem -import com.m3u.core.architecture.preferences.hiltPreferences -import com.m3u.data.database.model.Programme -import com.m3u.data.database.model.Channel -import com.m3u.feature.playlist.PlaylistViewModel -import com.m3u.feature.playlist.components.ImmersiveBackground -import com.m3u.feature.playlist.components.TvChannelGallery -import com.m3u.i18n.R -import com.m3u.material.components.Background -import com.m3u.material.components.Icon -import com.m3u.material.components.tv.dialogFocusable -import com.m3u.material.ktx.Edge -import com.m3u.material.ktx.blurEdge -import com.m3u.material.model.LocalHazeState -import com.m3u.ui.Sort -import com.m3u.ui.TvSortFullScreenDialog -import dev.chrisbanes.haze.HazeDefaults -import dev.chrisbanes.haze.HazeStyle -import dev.chrisbanes.haze.haze -import dev.chrisbanes.haze.hazeChild -import androidx.tv.material3.ListItemDefaults as TvListItemDefaults -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText - -@Composable -@InternalComposeApi -internal fun TvPlaylistScreenImpl( - title: String, - categoryWithChannels: List, - query: String, - onQuery: (String) -> Unit, - sorts: List, - sort: Sort, - onSort: (Sort) -> Unit, - favorite: (channelId: Int) -> Unit, - hide: (channelId: Int) -> Unit, - savePicture: (channelId: Int) -> Unit, - createTvRecommend: (channelId: Int) -> Unit, - onPlayChannel: (Channel) -> Unit, - onRefresh: () -> Unit, - getProgrammeCurrently: suspend (channelId: Int) -> Programme?, - isVodOrSeriesPlaylist: Boolean, - modifier: Modifier = Modifier -) { - val preferences = hiltPreferences() - val multiCategories = categoryWithChannels.size > 1 - val noPictureMode = preferences.noPictureMode - val useGridLayout = sort != Sort.UNSPECIFIED - - val maxBrowserHeight by animateDpAsState( - targetValue = when { - useGridLayout || isVodOrSeriesPlaylist -> 360.dp - noPictureMode -> 320.dp - multiCategories -> 256.dp - else -> 180.dp - }, - label = "max-browser-height" - ) - - var isSortSheetVisible by rememberSaveable { mutableStateOf(false) } - - var press: Channel? by remember { mutableStateOf(null) } - var focus: Channel? by remember { mutableStateOf(null) } - - val content = @Composable { - Box( - modifier = modifier.fillMaxWidth() - ) { - ImmersiveBackground( - title = title, - channel = focus, - maxBrowserHeight = maxBrowserHeight, - onRefresh = onRefresh, - openSearchDrawer = {}, - openSortDrawer = { isSortSheetVisible = true }, - getProgrammeCurrently = getProgrammeCurrently, - modifier = Modifier.haze( - LocalHazeState.current, - HazeDefaults.style(TvMaterialTheme.colorScheme.background) - ) - ) - TvChannelGallery( - categoryWithChannels = categoryWithChannels, - maxBrowserHeight = maxBrowserHeight, - isSpecifiedSort = useGridLayout, - isVodOrSeriesPlaylist = isVodOrSeriesPlaylist, - onClick = onPlayChannel, - onLongClick = { channel -> press = channel }, - onFocus = { channel -> focus = channel }, - modifier = Modifier - .hazeChild( - LocalHazeState.current, - style = HazeStyle(blurRadius = 4.dp) - ) - .blurEdge( - color = TvMaterialTheme.colorScheme.background, - edge = Edge.Top - ) - .align(Alignment.BottomCenter) - ) - } - } - - Background { - content() - MenuFullScreenDialog( - channel = press, - favorite = favorite, - hide = hide, - savePicture = savePicture, - createShortcutOrTvRecommend = createTvRecommend, - onDismissRequest = { press = null } - ) - TvSortFullScreenDialog( - visible = isSortSheetVisible, - sort = sort, - sorts = sorts, - onChanged = { onSort(it) }, - onDismissRequest = { isSortSheetVisible = false } - ) - } -} - -@Composable -private fun MenuFullScreenDialog( - channel: Channel?, - favorite: (channelId: Int) -> Unit, - hide: (channelId: Int) -> Unit, - savePicture: (channelId: Int) -> Unit, - createShortcutOrTvRecommend: (channelId: Int) -> Unit, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier -) { - val favouriteTitle = stringResource( - if (channel?.favourite == true) R.string.feat_playlist_dialog_favourite_cancel_title - else R.string.feat_playlist_dialog_favourite_title - ).uppercase() - val hideTitle = stringResource(R.string.feat_playlist_dialog_hide_title).uppercase() - val createShortcutTitle = - stringResource(R.string.feat_playlist_dialog_create_shortcut_title).uppercase() - val savePictureTitle = - stringResource(R.string.feat_playlist_dialog_save_picture_title).uppercase() - Box( - Modifier - .fillMaxSize() - .then(modifier) - ) { - AnimatedVisibility( - visible = channel != null, - modifier = Modifier - .fillMaxHeight() - .fillMaxWidth(0.4f) - .align(Alignment.CenterEnd) - ) { - LazyColumn( - Modifier - .fillMaxHeight() - .background(TvMaterialTheme.colorScheme.surfaceVariant) - .padding(12.dp) - .selectableGroup() - .dialogFocusable(), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(10.dp) - ) { - item { - DenseListItem( - selected = false, - headlineContent = { - TvText( - text = channel?.title.orEmpty(), - maxLines = 1, - style = TvMaterialTheme.typography.titleLarge - ) - }, - onClick = {} - ) - } - item { - DenseListItem( - selected = false, - headlineContent = { - TvText( - text = favouriteTitle - ) - }, - onClick = { - channel?.let { channel -> - favorite(channel.id) - onDismissRequest() - } - }, - leadingContent = { - Icon( - imageVector = Icons.Rounded.Favorite, - contentDescription = null - ) - }, - scale = TvListItemDefaults.scale(0.95f, 1f) - ) - } - item { - DenseListItem( - selected = false, - onClick = { - channel?.let { - hide(it.id) - onDismissRequest() - } - }, - headlineContent = { - TvText( - text = hideTitle - ) - }, - leadingContent = { - Icon( - imageVector = Icons.Rounded.Delete, - contentDescription = null - ) - }, - scale = TvListItemDefaults.scale(0.95f, 1f) - ) - } - item { - DenseListItem( - selected = false, - onClick = { - channel?.let { - createShortcutOrTvRecommend(it.id) - onDismissRequest() - } - }, - headlineContent = { - TvText( - text = createShortcutTitle - ) - }, - leadingContent = { - Icon( - imageVector = Icons.AutoMirrored.Rounded.Shortcut, - contentDescription = null - ) - }, - scale = TvListItemDefaults.scale(0.95f, 1f) - ) - } - item { - DenseListItem( - selected = false, - onClick = { - channel?.let { - savePicture(it.id) - onDismissRequest() - } - }, - headlineContent = { - TvText( - text = savePictureTitle - ) - }, - leadingContent = { - Icon( - imageVector = Icons.Rounded.Image, - contentDescription = null - ) - }, - scale = TvListItemDefaults.scale(0.95f, 1f) - ) - } - } - BackHandler { - onDismissRequest() - } - } - } -} - diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/navigation/PlaylistNavigation.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/navigation/PlaylistNavigation.kt index 3a0a95f0..6bde70c9 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/navigation/PlaylistNavigation.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/navigation/PlaylistNavigation.kt @@ -10,14 +10,11 @@ import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.NavOptions import androidx.navigation.NavType -import androidx.navigation.activity import androidx.navigation.compose.composable import androidx.navigation.navArgument import com.m3u.feature.playlist.PlaylistRoute -import com.m3u.feature.playlist.TvPlaylistActivity private const val PLAYLIST_ROUTE_PATH = "playlist_route" -private const val PLAYLIST_TV_ROUTE_PATH = "playlist_tv_route" object PlaylistNavigation { internal const val TYPE_URL = "url" @@ -28,23 +25,14 @@ object PlaylistNavigation { internal fun createPlaylistRoute(url: String): String { return "$PLAYLIST_ROUTE_PATH?$TYPE_URL=$url" } - - internal const val PLAYLIST_TV_ROUTE = - "$PLAYLIST_TV_ROUTE_PATH?$TYPE_URL={$TYPE_URL}" - - internal fun createPlaylistTvRoute(url: String): String { - return "$PLAYLIST_TV_ROUTE_PATH?${TYPE_URL}=$url" - } } fun NavController.navigateToPlaylist( playlistUrl: String, - tv: Boolean = false, navOptions: NavOptions? = null, ) { val encodedUrl = Uri.encode(playlistUrl) - val route = if (tv) PlaylistNavigation.createPlaylistTvRoute(encodedUrl) - else PlaylistNavigation.createPlaylistRoute(encodedUrl) + val route = PlaylistNavigation.createPlaylistRoute(encodedUrl) this.navigate(route, navOptions) } @@ -70,12 +58,3 @@ fun NavGraphBuilder.playlistScreen( ) } } - -fun NavGraphBuilder.playlistTvScreen() { - activity(PlaylistNavigation.PLAYLIST_TV_ROUTE) { - activityClass = TvPlaylistActivity::class - argument(PlaylistNavigation.TYPE_URL) { - type = NavType.StringType - } - } -} diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/SettingScreen.kt b/feature/setting/src/main/java/com/m3u/feature/setting/SettingScreen.kt index a72b10de..902518c9 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/SettingScreen.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/SettingScreen.kt @@ -41,8 +41,6 @@ import com.m3u.feature.setting.fragments.OptionalFragment import com.m3u.feature.setting.fragments.SubscriptionsFragment import com.m3u.feature.setting.fragments.preferences.PreferencesFragment import com.m3u.i18n.R.string -import com.m3u.material.ktx.includeChildGlowPadding -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalHazeState import com.m3u.ui.Destination import com.m3u.ui.EventHandler @@ -59,7 +57,6 @@ fun SettingRoute( modifier: Modifier = Modifier, viewModel: SettingViewModel = hiltViewModel() ) { - val tv = tv() val controller = LocalSoftwareKeyboardController.current val colorSchemes by viewModel.colorSchemes.collectAsStateWithLifecycle() @@ -129,18 +126,16 @@ fun SettingRoute( modifier = modifier.fillMaxSize(), contentPadding = contentPadding ) - if (!tv) { - CanvasBottomSheet( - sheetState = sheetState, - colorScheme = colorScheme, - onApplyColor = { argb, isDark -> - viewModel.applyColor(colorScheme, argb, isDark) - }, - onDismissRequest = { - colorScheme = null - } - ) - } + CanvasBottomSheet( + sheetState = sheetState, + colorScheme = colorScheme, + onApplyColor = { argb, isDark -> + viewModel.applyColor(colorScheme, argb, isDark) + }, + onDismissRequest = { + colorScheme = null + } + ) } @Composable @@ -249,7 +244,6 @@ private fun SettingScreen( onClearCache = onClearCache, modifier = Modifier .fillMaxSize() - .includeChildGlowPadding() ) }, detailPane = { diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/components/CanvasBottomSheet.kt b/feature/setting/src/main/java/com/m3u/feature/setting/components/CanvasBottomSheet.kt index 1a919219..a7c91b6f 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/components/CanvasBottomSheet.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/components/CanvasBottomSheet.kt @@ -38,7 +38,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.m3u.data.database.model.ColorScheme import com.m3u.i18n.R.string -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.ktx.createScheme import com.m3u.material.model.LocalSpacing import com.m3u.material.model.SugarColors diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/components/DataSourceSelection.kt b/feature/setting/src/main/java/com/m3u/feature/setting/components/DataSourceSelection.kt index 4e685ccd..c183225f 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/components/DataSourceSelection.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/components/DataSourceSelection.kt @@ -25,7 +25,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.m3u.data.database.model.DataSource import com.m3u.material.components.ClickableSelection -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.components.SelectionsDefaults @Composable diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/components/EpgPlaylistItem.kt b/feature/setting/src/main/java/com/m3u/feature/setting/components/EpgPlaylistItem.kt index 04d1a009..d29328ed 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/components/EpgPlaylistItem.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/components/EpgPlaylistItem.kt @@ -2,6 +2,7 @@ package com.m3u.feature.setting.components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -9,7 +10,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import com.m3u.data.database.model.Playlist -import com.m3u.material.components.IconButton +import androidx.compose.material3.IconButton @Composable internal fun EpgPlaylistItem( @@ -34,10 +35,13 @@ internal fun EpgPlaylistItem( }, trailingContent = { IconButton( - icon = Icons.Rounded.Delete, - onClick = onDeleteEpgPlaylist, - contentDescription = "delete epg" - ) + onClick = onDeleteEpgPlaylist + ) { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = "delete epg" + ) + } }, modifier = modifier ) diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/components/LocalStorageButton.kt b/feature/setting/src/main/java/com/m3u/feature/setting/components/LocalStorageButton.kt index 000d0833..bdecbf9f 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/components/LocalStorageButton.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/components/LocalStorageButton.kt @@ -16,7 +16,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import com.m3u.core.util.readFileName import com.m3u.i18n.R.string -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.components.ToggleableSelection @Composable diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/fragments/AppearanceFragment.kt b/feature/setting/src/main/java/com/m3u/feature/setting/fragments/AppearanceFragment.kt index 3b769fd6..34df65e0 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/fragments/AppearanceFragment.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/fragments/AppearanceFragment.kt @@ -39,7 +39,6 @@ import com.m3u.material.components.Preference import com.m3u.material.components.TextPreference import com.m3u.material.components.ThemeAddSelection import com.m3u.material.components.ThemeSelection -import com.m3u.material.ktx.tv import com.m3u.material.ktx.minus import com.m3u.material.ktx.only import com.m3u.material.ktx.plus @@ -60,7 +59,6 @@ internal fun AppearanceFragment( val isDarkMode = preferences.darkMode val useDynamicColors = preferences.useDynamicColors - val tv = tv() Column( modifier = modifier @@ -181,17 +179,15 @@ internal fun AppearanceFragment( enabled = useDynamicColorsAvailable ) } - if (!tv) { - item { - SwitchSharedPreference( - title = string.feat_setting_colorful_background, - icon = Icons.Rounded.Stars, - checked = preferences.colorfulBackground, - onChanged = { - preferences.colorfulBackground = !preferences.colorfulBackground - } - ) - } + item { + SwitchSharedPreference( + title = string.feat_setting_colorful_background, + icon = Icons.Rounded.Stars, + checked = preferences.colorfulBackground, + onChanged = { + preferences.colorfulBackground = !preferences.colorfulBackground + } + ) } item { Preference( @@ -201,15 +197,13 @@ internal fun AppearanceFragment( ) } item { - if (!tv) { - SwitchSharedPreference( - title = string.feat_setting_god_mode, - content = string.feat_setting_god_mode_description, - icon = Icons.Rounded.DeviceHub, - checked = preferences.godMode, - onChanged = { preferences.godMode = !preferences.godMode } - ) - } + SwitchSharedPreference( + title = string.feat_setting_god_mode, + content = string.feat_setting_god_mode_description, + icon = Icons.Rounded.DeviceHub, + checked = preferences.godMode, + onChanged = { preferences.godMode = !preferences.godMode } + ) } } } diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/fragments/OptionalFragment.kt b/feature/setting/src/main/java/com/m3u/feature/setting/fragments/OptionalFragment.kt index 671ce999..7ece9433 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/fragments/OptionalFragment.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/fragments/OptionalFragment.kt @@ -37,8 +37,6 @@ import com.m3u.core.util.basic.title import com.m3u.feature.setting.components.SwitchSharedPreference import com.m3u.i18n.R.string import com.m3u.material.components.TextPreference -import com.m3u.material.ktx.includeChildGlowPadding -import com.m3u.material.ktx.tv import com.m3u.material.ktx.plus import com.m3u.material.model.LocalSpacing import kotlin.time.DurationUnit @@ -51,13 +49,11 @@ internal fun OptionalFragment( ) { val spacing = LocalSpacing.current val preferences = hiltPreferences() - val tv = tv() LazyColumn( verticalArrangement = Arrangement.spacedBy(spacing.small), contentPadding = contentPadding + PaddingValues(horizontal = spacing.medium), modifier = modifier .fillMaxSize() - .includeChildGlowPadding() ) { item { SwitchSharedPreference( @@ -86,32 +82,30 @@ internal fun OptionalFragment( ) } - if (!tv) { - item { - SwitchSharedPreference( - title = string.feat_setting_zapping_mode, - content = string.feat_setting_zapping_mode_description, - icon = Icons.Rounded.PictureInPicture, - checked = preferences.zappingMode, - onChanged = { preferences.zappingMode = !preferences.zappingMode } - ) - } - item { - SwitchSharedPreference( - title = string.feat_setting_gesture_brightness, - icon = Icons.Rounded.BrightnessMedium, - checked = preferences.brightnessGesture, - onChanged = { preferences.brightnessGesture = !preferences.brightnessGesture } - ) - } - item { - SwitchSharedPreference( - title = string.feat_setting_gesture_volume, - icon = Icons.AutoMirrored.Rounded.VolumeUp, - checked = preferences.volumeGesture, - onChanged = { preferences.volumeGesture = !preferences.volumeGesture } - ) - } + item { + SwitchSharedPreference( + title = string.feat_setting_zapping_mode, + content = string.feat_setting_zapping_mode_description, + icon = Icons.Rounded.PictureInPicture, + checked = preferences.zappingMode, + onChanged = { preferences.zappingMode = !preferences.zappingMode } + ) + } + item { + SwitchSharedPreference( + title = string.feat_setting_gesture_brightness, + icon = Icons.Rounded.BrightnessMedium, + checked = preferences.brightnessGesture, + onChanged = { preferences.brightnessGesture = !preferences.brightnessGesture } + ) + } + item { + SwitchSharedPreference( + title = string.feat_setting_gesture_volume, + icon = Icons.AutoMirrored.Rounded.VolumeUp, + checked = preferences.volumeGesture, + onChanged = { preferences.volumeGesture = !preferences.volumeGesture } + ) } item { SwitchSharedPreference( @@ -140,24 +134,22 @@ internal fun OptionalFragment( onChanged = { preferences.cache = !preferences.cache } ) } - if (!tv) { - item { - SwitchSharedPreference( - title = string.feat_setting_screen_rotating, - content = string.feat_setting_screen_rotating_description, - icon = Icons.Rounded.ScreenRotation, - checked = preferences.screenRotating, - onChanged = { preferences.screenRotating = !preferences.screenRotating } - ) - } - item { - SwitchSharedPreference( - title = string.feat_setting_screencast, - icon = Icons.Rounded.Cast, - checked = preferences.screencast, - onChanged = { preferences.screencast = !preferences.screencast } - ) - } + item { + SwitchSharedPreference( + title = string.feat_setting_screen_rotating, + content = string.feat_setting_screen_rotating_description, + icon = Icons.Rounded.ScreenRotation, + checked = preferences.screenRotating, + onChanged = { preferences.screenRotating = !preferences.screenRotating } + ) + } + item { + SwitchSharedPreference( + title = string.feat_setting_screencast, + icon = Icons.Rounded.Cast, + checked = preferences.screencast, + onChanged = { preferences.screencast = !preferences.screencast } + ) } item { TextPreference( @@ -261,10 +253,8 @@ internal fun OptionalFragment( } item { SwitchSharedPreference( - title = if (!tv) string.feat_setting_remote_control - else string.feat_setting_remote_control_tv_side, - content = if (!tv) string.feat_setting_remote_control_description - else string.feat_setting_remote_control_tv_side_description, + title = string.feat_setting_remote_control, + content = string.feat_setting_remote_control_description, icon = Icons.Rounded.SettingsRemote, checked = preferences.remoteControl, onChanged = { preferences.remoteControl = !preferences.remoteControl } diff --git a/feature/setting/src/main/java/com/m3u/feature/setting/fragments/SubscriptionsFragment.kt b/feature/setting/src/main/java/com/m3u/feature/setting/fragments/SubscriptionsFragment.kt index 4ee31863..20df6cd0 100644 --- a/feature/setting/src/main/java/com/m3u/feature/setting/fragments/SubscriptionsFragment.kt +++ b/feature/setting/src/main/java/com/m3u/feature/setting/fragments/SubscriptionsFragment.kt @@ -20,6 +20,8 @@ import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Warning +import androidx.compose.material3.Button +import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -44,13 +46,10 @@ import com.m3u.feature.setting.components.LocalStorageButton import com.m3u.feature.setting.components.LocalStorageSwitch import com.m3u.feature.setting.components.RemoteControlSubscribeSwitch import com.m3u.i18n.R.string -import com.m3u.material.components.Button import com.m3u.material.components.HorizontalPagerIndicator -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import com.m3u.material.components.PlaceholderField -import com.m3u.material.components.TonalButton import com.m3u.material.ktx.checkPermissionOrRationale -import com.m3u.material.ktx.tv import com.m3u.material.ktx.textHorizontalLabel import com.m3u.material.model.LocalSpacing import com.m3u.ui.helper.LocalHelper @@ -172,7 +171,6 @@ private fun MainContentImpl( val clipboardManager = LocalClipboardManager.current val helper = LocalHelper.current - val tv = tv() val remoteControl = preferences.remoteControl LazyColumn( @@ -233,7 +231,7 @@ private fun MainContentImpl( enabled = !forTvState.value ) } - if (!tv && remoteControl) { + if (remoteControl) { RemoteControlSubscribeSwitch( checked = forTvState.value, onChanged = { forTvState.value = !forTvState.value }, @@ -247,7 +245,6 @@ private fun MainContentImpl( Manifest.permission.POST_NOTIFICATIONS ) Button( - text = stringResource(string.feat_setting_label_subscribe), onClick = { postNotificationPermission.checkPermissionOrRationale( showRationale = { @@ -266,17 +263,20 @@ private fun MainContentImpl( ) }, modifier = Modifier.fillMaxWidth() - ) + ) { + Text(stringResource(string.feat_setting_label_subscribe)) + } when (selectedState.value) { DataSource.M3U, DataSource.Xtream -> { - TonalButton( - text = stringResource(string.feat_setting_label_parse_from_clipboard), + FilledTonalButton( enabled = !localStorageState.value, onClick = { onClipboard(clipboardManager.getText()?.text.orEmpty()) }, modifier = Modifier.fillMaxWidth() - ) + ) { + Text(stringResource(string.feat_setting_label_parse_from_clipboard)) + } } else -> {} @@ -284,18 +284,22 @@ private fun MainContentImpl( } item { - TonalButton( - text = stringResource(string.feat_setting_label_backup), + FilledTonalButton( enabled = !forTvState.value && backingUpOrRestoring == BackingUpAndRestoringState.NONE, onClick = backup, modifier = Modifier.fillMaxWidth() - ) - TonalButton( - text = stringResource(string.feat_setting_label_restore), + ) { + Text( + text = stringResource(string.feat_setting_label_backup) + ) + } + FilledTonalButton( enabled = !forTvState.value && backingUpOrRestoring == BackingUpAndRestoringState.NONE, onClick = restore, modifier = Modifier.fillMaxWidth() - ) + ) { + Text(text = stringResource(string.feat_setting_label_restore)) + } } item { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 65a9210f..26e9a30d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -55,6 +55,7 @@ ktor-server = "3.0.0-beta-1" mm2d-mmupnp = "3.1.6" symbolProcessingApi = "2.0.0-1.0.22" profileinstaller = "1.4.1" +tvFoundation = "1.0.0-alpha12" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core" } @@ -168,6 +169,11 @@ net-mm2d-mmupnp-mmupnp = { group = "net.mm2d.mmupnp", name = "mmupnp", version.r androidx-graphics-shapes-android = { group = "androidx.graphics", name = "graphics-shapes-android", version.ref = "androidx-graphics-shapes" } symbol-processing-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "symbolProcessingApi" } androidx-profileinstaller = { group = "androidx.profileinstaller", name = "profileinstaller", version.ref = "profileinstaller" } +androidx-ui = { group = "androidx.compose.ui", name = "ui" } +androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } +androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } +androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } +androidx-tv-foundation = { group = "androidx.tv", name = "tv-foundation", version.ref = "tvFoundation" } [plugins] com-android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" } diff --git a/i18n/src/main/res/values/feat_foryou.xml b/i18n/src/main/res/values/feat_foryou.xml index 0e4aa533..9cf3deb1 100644 --- a/i18n/src/main/res/values/feat_foryou.xml +++ b/i18n/src/main/res/values/feat_foryou.xml @@ -11,6 +11,6 @@ %d days %d hours new release - enter code from tv + enter code from TV Make sure to connect to the same Wi-Fi \ No newline at end of file diff --git a/material/build.gradle.kts b/material/build.gradle.kts index 01dc392a..934790f0 100644 --- a/material/build.gradle.kts +++ b/material/build.gradle.kts @@ -42,8 +42,6 @@ dependencies { implementation(libs.airbnb.lottie.compose) - api(libs.androidx.tv.material) - api(libs.androidx.graphics.shapes.android) api(libs.google.material) api(libs.haze) diff --git a/material/src/main/java/com/m3u/material/components/Backgrounds.kt b/material/src/main/java/com/m3u/material/components/Backgrounds.kt index 2a2399d1..7b6ea262 100644 --- a/material/src/main/java/com/m3u/material/components/Backgrounds.kt +++ b/material/src/main/java/com/m3u/material/components/Backgrounds.kt @@ -14,9 +14,6 @@ import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.unit.dp -import com.m3u.material.ktx.tv -import androidx.tv.material3.LocalContentColor as TvLocalContentColor -import androidx.tv.material3.MaterialTheme as TvMtaterialTheme @Composable inline fun Background( @@ -26,14 +23,8 @@ inline fun Background( shape: Shape = RectangleShape, crossinline content: @Composable () -> Unit ) { - val actualColor = color.takeOrElse { - if (!tv()) MaterialTheme.colorScheme.background - else TvMtaterialTheme.colorScheme.background - } - val actualContentColor = contentColor.takeOrElse { - if (!tv()) MaterialTheme.colorScheme.onBackground - else TvMtaterialTheme.colorScheme.onBackground - } + val actualColor = color.takeOrElse { MaterialTheme.colorScheme.background } + val actualContentColor = contentColor.takeOrElse { MaterialTheme.colorScheme.onBackground } Box( modifier = Modifier .clip(shape) @@ -44,8 +35,7 @@ inline fun Background( ) { CompositionLocalProvider( LocalAbsoluteTonalElevation provides 0.dp, - LocalContentColor provides actualContentColor, - TvLocalContentColor provides actualContentColor + LocalContentColor provides actualContentColor ) { content() } diff --git a/material/src/main/java/com/m3u/material/components/Buttons.kt b/material/src/main/java/com/m3u/material/components/Buttons.kt deleted file mode 100644 index f5d9dd32..00000000 --- a/material/src/main/java/com/m3u/material/components/Buttons.kt +++ /dev/null @@ -1,263 +0,0 @@ -@file:Suppress("unused") - -package com.m3u.material.components - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.LocalContentColor -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProvideTextStyle -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.isUnspecified -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.unit.dp -import com.m3u.material.ktx.tv -import com.m3u.material.model.LocalSpacing -import androidx.tv.material3.Button as TvButton -import androidx.tv.material3.OutlinedButton as TvOutlinedButton -import androidx.tv.material3.Text as TvText - -@Composable -fun Button( - text: String, - modifier: Modifier = Modifier, - enabled: Boolean = true, - containerColor: Color = MaterialTheme.colorScheme.primary, - contentColor: Color = MaterialTheme.colorScheme.onPrimary, - disabledContainerColor: Color = containerColor.copy(alpha = 0.12f), - disabledContentColor: Color = containerColor.copy(alpha = 0.38f), - onClick: () -> Unit -) { - val spacing = LocalSpacing.current - val tv = tv() - if (!tv) { - Button( - shape = RoundedCornerShape(8.dp), - onClick = onClick, - enabled = enabled, - modifier = modifier, - colors = ButtonDefaults.buttonColors( - containerColor = containerColor, - contentColor = contentColor, - disabledContainerColor = disabledContainerColor, - disabledContentColor = disabledContentColor - ) - ) { - Text( - text = text.uppercase() - ) - } - } else { - TvButton( - onClick = onClick, - enabled = enabled, - modifier = Modifier - .padding(spacing.extraSmall) - .then(modifier), - colors = androidx.tv.material3.ButtonDefaults.colors( - containerColor = containerColor, - contentColor = contentColor, - disabledContainerColor = disabledContainerColor, - disabledContentColor = disabledContentColor - ) - ) { - TvText( - text = text.uppercase() - ) - } - } - -} - -@Composable -fun TextButton( - text: String, - modifier: Modifier = Modifier, - enabled: Boolean = true, - containerColor: Color = MaterialTheme.colorScheme.surface, - contentColor: Color = MaterialTheme.colorScheme.primary, - disabledContentColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), - onClick: () -> Unit -) { - val spacing = LocalSpacing.current - - val tv = tv() - if (!tv) { - TextButton( - shape = RoundedCornerShape(8.dp), - onClick = onClick, - enabled = enabled, - modifier = modifier, - colors = ButtonDefaults.buttonColors( - containerColor = containerColor, - contentColor = contentColor, - disabledContentColor = disabledContentColor - ) - ) { - Text( - text = text.uppercase() - ) - } - } else { - TvOutlinedButton( - onClick = onClick, - enabled = enabled, - modifier = Modifier - .padding(spacing.extraSmall) - .then(modifier), - colors = androidx.tv.material3.ButtonDefaults.colors( - containerColor = containerColor, - contentColor = contentColor, - disabledContentColor = disabledContentColor - ) - ) { - TvText( - text = text.uppercase() - ) - } - } -} - -@Composable -fun TonalButton( - text: String, - modifier: Modifier = Modifier, - enabled: Boolean = true, - onClick: () -> Unit -) { - val spacing = LocalSpacing.current - - val tv = tv() - if (!tv) { - FilledTonalButton( - shape = RoundedCornerShape(8.dp), - onClick = onClick, - enabled = enabled, - modifier = modifier, - elevation = ButtonDefaults.filledTonalButtonElevation(spacing.none) - ) { - Text( - text = text.uppercase() - ) - } - } else { - TvOutlinedButton( - onClick = onClick, - enabled = enabled, - modifier = Modifier - .padding(spacing.extraSmall) - .then(modifier), - ) { - TvText( - text = text.uppercase() - ) - } - } -} - -@Composable -fun IconButton( - icon: ImageVector, - contentDescription: String?, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - tint: Color = Color.Unspecified -) { - val tv = tv() - if (!tv) { - IconButton( - onClick = onClick, - enabled = enabled, - modifier = modifier, - colors = if (tint.isUnspecified) IconButtonDefaults.iconButtonColors() - else IconButtonDefaults.iconButtonColors( - contentColor = tint - ) - ) { - Icon( - imageVector = icon, - contentDescription = contentDescription, - ) - } - } else { - androidx.tv.material3.IconButton( - onClick = onClick, - enabled = enabled, - modifier = modifier, - colors = if (tint.isUnspecified) androidx.tv.material3.IconButtonDefaults.colors() - else androidx.tv.material3.IconButtonDefaults.colors( - contentColor = tint, - focusedContainerColor = tint, - pressedContainerColor = tint - ) - ) { - Icon( - imageVector = icon, - contentDescription = contentDescription - ) - } - } -} - -@Composable -fun BrushButton( - text: String, - brush: Brush, - modifier: Modifier = Modifier, - enabled: Boolean = true, - onClick: () -> Unit -) { - Box( - contentAlignment = Alignment.Center, - modifier = modifier - .clip(RoundedCornerShape(8.dp)) - .background(brush) - .clickable( - enabled = enabled, - role = Role.Button, - onClick = onClick - ), - ) { - CompositionLocalProvider(LocalContentColor provides Color.White) { - ProvideTextStyle(value = MaterialTheme.typography.titleSmall) { - Row( - Modifier - .defaultMinSize( - minWidth = ButtonDefaults.MinWidth, - minHeight = ButtonDefaults.MinHeight - ) - .padding(ButtonDefaults.ContentPadding), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - content = { - Text( - text = text, - maxLines = 1 - ) - } - ) - } - } - } -} diff --git a/material/src/main/java/com/m3u/material/components/Icons.kt b/material/src/main/java/com/m3u/material/components/Icons.kt deleted file mode 100644 index 02bed021..00000000 --- a/material/src/main/java/com/m3u/material/components/Icons.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.m3u.material.components - -import androidx.compose.material3.LocalContentColor -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.takeOrElse -import androidx.compose.ui.graphics.vector.ImageVector -import com.m3u.material.ktx.tv - -@Composable -fun Icon( - imageVector: ImageVector, - contentDescription: String?, - modifier: Modifier = Modifier, - tint: Color = Color.Unspecified -) { - val tv = tv() - if (!tv) { - androidx.compose.material3.Icon( - imageVector = imageVector, - contentDescription = contentDescription, - modifier = modifier, - tint = tint.takeOrElse { LocalContentColor.current } - ) - } else { - androidx.tv.material3.Icon( - imageVector = imageVector, - contentDescription = contentDescription, - modifier = modifier, - tint = tint.takeOrElse { androidx.tv.material3.LocalContentColor.current } - ) - } -} \ No newline at end of file diff --git a/material/src/main/java/com/m3u/material/components/Preferences.kt b/material/src/main/java/com/m3u/material/components/Preferences.kt index 4e88c28a..02f39360 100644 --- a/material/src/main/java/com/m3u/material/components/Preferences.kt +++ b/material/src/main/java/com/m3u/material/components/Preferences.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.CardDefaults import androidx.compose.material3.Checkbox +import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalAbsoluteTonalElevation @@ -33,16 +34,8 @@ import androidx.compose.ui.text.intl.Locale import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalSpacing import com.m3u.material.shape.AbsoluteSmoothCornerShape -import androidx.tv.material3.Checkbox as TvCheckbox -import androidx.tv.material3.Icon as TvIcon -import androidx.tv.material3.ListItem as TvListItem -import androidx.tv.material3.ListItemDefaults as TvListItemDefaults -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Switch as TvSwitch -import androidx.tv.material3.Text as TvText @Composable fun Preference( @@ -73,92 +66,53 @@ fun Preference( } ) { val alpha = if (enabled) 1f else 0.38f - if (!tv()) { - OutlinedCard( - colors = CardDefaults.outlinedCardColors(Color.Transparent), - shape = AbsoluteSmoothCornerShape(spacing.medium, 65) - ) { - ListItem( - headlineContent = { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - }, - supportingContent = { - if (content != null) { - Text( - text = content.capitalize(Locale.current), - style = MaterialTheme.typography.bodyMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier then if (focus) Modifier.basicMarquee() - else Modifier - ) - } - }, - trailingContent = trailing, - leadingContent = icon?.let { - @Composable { - Icon(imageVector = it, contentDescription = null) - } - }, - tonalElevation = LocalAbsoluteTonalElevation.current, - shadowElevation = elevation, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent, - overlineColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha), - supportingColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha), - headlineColor = MaterialTheme.colorScheme.onSurface.copy(alpha) - ), - modifier = modifier - .semantics(mergeDescendants = true) {} - .clickable( - enabled = enabled, - onClick = onClick, - interactionSource = interactionSource, - indication = ripple() - ) - .fillMaxWidth() - ) - } - } else { - TvListItem( - selected = focus, - interactionSource = interactionSource, + OutlinedCard( + colors = CardDefaults.outlinedCardColors(Color.Transparent), + shape = AbsoluteSmoothCornerShape(spacing.medium, 65) + ) { + ListItem( headlineContent = { - TvText( + Text( text = title, style = MaterialTheme.typography.titleMedium, maxLines = 1, + overflow = TextOverflow.Ellipsis, ) }, supportingContent = { if (content != null) { - TvText( + Text( text = content.capitalize(Locale.current), style = MaterialTheme.typography.bodyMedium, maxLines = 1, - overflow = TextOverflow.Ellipsis + overflow = TextOverflow.Ellipsis, + modifier = Modifier then if (focus) Modifier.basicMarquee() + else Modifier ) } }, trailingContent = trailing, leadingContent = icon?.let { @Composable { - TvIcon(imageVector = it, contentDescription = null) + Icon(imageVector = it, contentDescription = null) } }, - scale = TvListItemDefaults.scale( - scale = 0.95f, - focusedScale = 1f + tonalElevation = LocalAbsoluteTonalElevation.current, + shadowElevation = elevation, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent, + overlineColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha), + supportingColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha), + headlineColor = MaterialTheme.colorScheme.onSurface.copy(alpha) ), - onClick = onClick, - enabled = enabled, modifier = modifier .semantics(mergeDescendants = true) {} + .clickable( + enabled = enabled, + onClick = onClick, + interactionSource = interactionSource, + indication = ripple() + ) .fillMaxWidth() ) } @@ -189,19 +143,11 @@ fun CheckBoxPreference( }, modifier = modifier, trailing = { - if (!tv()) { - Checkbox( - enabled = enabled, - checked = checked, - onCheckedChange = null - ) - } else { - TvCheckbox( - enabled = enabled, - checked = checked, - onCheckedChange = null - ) - } + Checkbox( + enabled = enabled, + checked = checked, + onCheckedChange = null + ) }, icon = icon ) @@ -230,19 +176,11 @@ fun SwitchPreference( }, modifier = modifier, trailing = { - if (!tv()) { - Switch( - enabled = enabled, - checked = checked, - onCheckedChange = null - ) - } else { - TvSwitch( - enabled = enabled, - checked = checked, - onCheckedChange = null - ) - } + Switch( + enabled = enabled, + checked = checked, + onCheckedChange = null + ) }, icon = icon ) @@ -267,18 +205,11 @@ fun TrailingIconPreference( elevation = elevation, modifier = modifier, trailing = { - if (!tv()) { - Icon( - imageVector = trailingIcon, - contentDescription = null, - tint = LocalContentColor.current.copy(alpha = 0.65f) - ) - } else { - TvIcon( - imageVector = trailingIcon, - contentDescription = null, - ) - } + Icon( + imageVector = trailingIcon, + contentDescription = null, + tint = LocalContentColor.current.copy(alpha = 0.65f) + ) }, icon = icon ) @@ -305,24 +236,14 @@ fun TextPreference( }, modifier = modifier, trailing = { - if (!tv()) { - Text( - text = trailing.uppercase(), - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } else { - TvText( - text = trailing.uppercase(), - style = TvMaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Bold, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } + Text( + text = trailing.uppercase(), + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) }, icon = icon ) diff --git a/material/src/main/java/com/m3u/material/components/TextFields.kt b/material/src/main/java/com/m3u/material/components/TextFields.kt index 92dab926..84d7eab5 100644 --- a/material/src/main/java/com/m3u/material/components/TextFields.kt +++ b/material/src/main/java/com/m3u/material/components/TextFields.kt @@ -2,12 +2,9 @@ package com.m3u.material.components -import android.view.KeyEvent import androidx.activity.compose.BackHandler -import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState @@ -24,6 +21,7 @@ import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.selection.LocalTextSelectionColors import androidx.compose.foundation.text.selection.TextSelectionColors +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -31,20 +29,14 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.input.key.onKeyEvent import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight @@ -55,15 +47,8 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.tv.material3.Border -import androidx.tv.material3.ClickableSurfaceDefaults -import androidx.tv.material3.LocalContentColor -import androidx.tv.material3.Surface import com.m3u.material.ktx.InteractionType import com.m3u.material.ktx.interactionBorder -import com.m3u.material.ktx.tv -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.Text as TvText @Composable fun TextField( @@ -185,151 +170,15 @@ fun PlaceholderField( icon: ImageVector? = null, onValueChange: (String) -> Unit = {}, ) { - if (!tv()) { - val focusManager = LocalFocusManager.current - val interactionSource = remember { MutableInteractionSource() } - val focus by interactionSource.collectIsFocusedAsState() - - BackHandler(focus) { - focusManager.clearFocus() - } - - val fontSize = TextFieldDefaults.MinimizeLabelFontSize - - val theme = MaterialTheme.colorScheme - CompositionLocalProvider( - LocalTextSelectionColors provides TextSelectionColors( - handleColor = theme.primary, - backgroundColor = theme.primary.copy(alpha = 0.45f) - ) - ) { - BasicTextField( - value = text, - singleLine = singleLine, - enabled = enabled, - textStyle = TextStyle( - fontFamily = MaterialTheme.typography.bodyMedium.fontFamily, - fontSize = fontSize, - color = contentColor, - fontWeight = fontWeight - ), - onValueChange = { - onValueChange(it) - }, - keyboardActions = keyboardActions ?: KeyboardActions( - onDone = { focusManager.clearFocus() }, - onNext = { focusManager.moveFocus(FocusDirection.Down) }, - onSearch = { focusManager.clearFocus() } - ), - keyboardOptions = KeyboardOptions( - keyboardType = keyboardType, - autoCorrectEnabled = false, - imeAction = imeAction - ), - interactionSource = interactionSource, - modifier = modifier.fillMaxWidth(), - readOnly = readOnly, - cursorBrush = SolidColor(contentColor.copy(.35f)), - decorationBox = { innerTextField -> - Row( - modifier = Modifier - .clip(shape) - .background(backgroundColor) - .interactionBorder( - type = InteractionType.PRESS, - source = interactionSource, - shape = shape - ), - verticalAlignment = Alignment.CenterVertically - ) { - icon?.let { icon -> - Icon( - modifier = Modifier - .size(56.dp) - .padding(15.dp), - imageVector = icon, - contentDescription = null, - tint = contentColor - ) - } - - Box( - Modifier - .interactionBorder( - type = InteractionType.PRESS, - source = interactionSource, - shape = shape - ) - .fillMaxWidth() - .defaultMinSize(minHeight = 56.dp) - .padding( - start = if (icon == null) 15.dp else 0.dp, - end = 15.dp - ), - contentAlignment = Alignment.CenterStart - ) { - val hasText = text.isNotEmpty() - - val animPlaceholder: Dp by animateDpAsState( - if (focus || hasText) (-10).dp else 0.dp, - label = "placeholder-translation-y" - ) - val animPlaceHolderFontSize: Float by animateFloatAsState( - targetValue = if (focus || hasText) 12f else 14f, - label = "placeholder-font-size" - ) - - Text( - modifier = Modifier - .graphicsLayer { - translationY = animPlaceholder.toPx() - }, - text = placeholder, - color = contentColor.copy(alpha = .35f), - fontSize = animPlaceHolderFontSize.sp, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontWeight = FontWeight.SemiBold - ) - - Box( - Modifier - .padding(top = -animPlaceholder) - .fillMaxWidth() - .heightIn(18.dp), - ) { - innerTextField() - } - } - } - } - ) - } - } else { - TvTextFieldImpl( - value = text, - onValueChange = onValueChange, - placeholder = placeholder, - shape = shape, - modifier = modifier - ) - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -private fun TvTextFieldImpl( - value: String, - onValueChange: (String) -> Unit, - placeholder: String, - modifier: Modifier = Modifier, - shape: Shape = RectangleShape, - keyboardActions: KeyboardActions = KeyboardActions.Default, -) { - val focusRequester = remember { FocusRequester() } val focusManager = LocalFocusManager.current val interactionSource = remember { MutableInteractionSource() } - val isFocus by interactionSource.collectIsFocusedAsState() + val focus by interactionSource.collectIsFocusedAsState() + + BackHandler(focus) { + focusManager.clearFocus() + } + + val fontSize = TextFieldDefaults.MinimizeLabelFontSize val theme = MaterialTheme.colorScheme CompositionLocalProvider( @@ -338,92 +187,107 @@ private fun TvTextFieldImpl( backgroundColor = theme.primary.copy(alpha = 0.45f) ) ) { - Surface( - shape = ClickableSurfaceDefaults.shape(shape), - scale = ClickableSurfaceDefaults.scale(focusedScale = 1f), - colors = ClickableSurfaceDefaults.colors( - containerColor = TvMaterialTheme.colorScheme.inverseOnSurface, - focusedContainerColor = TvMaterialTheme.colorScheme.inverseOnSurface, - pressedContainerColor = TvMaterialTheme.colorScheme.inverseOnSurface, - focusedContentColor = TvMaterialTheme.colorScheme.onSurface, - pressedContentColor = TvMaterialTheme.colorScheme.onSurface + BasicTextField( + value = text, + singleLine = singleLine, + enabled = enabled, + textStyle = TextStyle( + fontFamily = MaterialTheme.typography.bodyMedium.fontFamily, + fontSize = fontSize, + color = contentColor, + fontWeight = fontWeight ), - border = ClickableSurfaceDefaults.border( - focusedBorder = Border( - border = BorderStroke( - width = if (isFocus) 2.dp else 1.dp, - color = animateColorAsState( - targetValue = if (isFocus) TvMaterialTheme.colorScheme.primary - else TvMaterialTheme.colorScheme.border, label = "" - ).value - ), - shape = shape - ) + onValueChange = { + onValueChange(it) + }, + keyboardActions = keyboardActions ?: KeyboardActions( + onDone = { focusManager.clearFocus() }, + onNext = { focusManager.moveFocus(FocusDirection.Down) }, + onSearch = { focusManager.clearFocus() } ), - tonalElevation = 2.dp, - modifier = modifier, - onClick = { focusRequester.requestFocus() } - ) { - BasicTextField( - value = value, - onValueChange = onValueChange, - decorationBox = { innerTextField -> + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, + autoCorrectEnabled = false, + imeAction = imeAction + ), + interactionSource = interactionSource, + modifier = modifier.fillMaxWidth(), + readOnly = readOnly, + cursorBrush = SolidColor(contentColor.copy(.35f)), + decorationBox = { innerTextField -> + Row( + modifier = Modifier + .clip(shape) + .background(backgroundColor) + .interactionBorder( + type = InteractionType.PRESS, + source = interactionSource, + shape = shape + ), + verticalAlignment = Alignment.CenterVertically + ) { + icon?.let { icon -> + Icon( + modifier = Modifier + .size(56.dp) + .padding(15.dp), + imageVector = icon, + contentDescription = null, + tint = contentColor + ) + } + Box( - contentAlignment = Alignment.CenterStart, - modifier = Modifier + Modifier + .interactionBorder( + type = InteractionType.PRESS, + source = interactionSource, + shape = shape + ) .fillMaxWidth() .defaultMinSize(minHeight = 56.dp) - .padding(horizontal = 16.dp), + .padding( + start = if (icon == null) 15.dp else 0.dp, + end = 15.dp + ), + contentAlignment = Alignment.CenterStart ) { - innerTextField() - if (value.isEmpty()) { - TvText( - modifier = Modifier.graphicsLayer { alpha = 0.6f }, - text = placeholder, - style = TvMaterialTheme.typography.titleSmall - ) + val hasText = text.isNotEmpty() + + val animPlaceholder: Dp by animateDpAsState( + if (focus || hasText) (-10).dp else 0.dp, + label = "placeholder-translation-y" + ) + val animPlaceHolderFontSize: Float by animateFloatAsState( + targetValue = if (focus || hasText) 12f else 14f, + label = "placeholder-font-size" + ) + + Text( + modifier = Modifier + .graphicsLayer { + translationY = animPlaceholder.toPx() + }, + text = placeholder, + color = contentColor.copy(alpha = .35f), + fontSize = animPlaceHolderFontSize.sp, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontWeight = FontWeight.SemiBold + ) + + Box( + Modifier + .padding(top = -animPlaceholder) + .fillMaxWidth() + .heightIn(18.dp), + ) { + innerTextField() } } - }, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .onKeyEvent { - if (it.nativeKeyEvent.action == KeyEvent.ACTION_UP) { - when (it.nativeKeyEvent.keyCode) { - KeyEvent.KEYCODE_DPAD_DOWN -> { - focusManager.moveFocus(FocusDirection.Down) - } - - KeyEvent.KEYCODE_DPAD_UP -> { - focusManager.moveFocus(FocusDirection.Up) - } - - KeyEvent.KEYCODE_BACK -> { - focusManager.moveFocus(FocusDirection.Exit) - } - } - } - true - }, - cursorBrush = Brush.verticalGradient( - colors = listOf( - LocalContentColor.current, - LocalContentColor.current, - ) - ), - keyboardOptions = KeyboardOptions( - autoCorrectEnabled = false, - imeAction = ImeAction.Search - ), - keyboardActions = keyboardActions, - maxLines = 1, - interactionSource = interactionSource, - textStyle = TvMaterialTheme.typography.titleSmall.copy( - color = TvMaterialTheme.colorScheme.onSurface - ) - ) - } + } + } + ) } } diff --git a/material/src/main/java/com/m3u/material/components/ThemeSelection.kt b/material/src/main/java/com/m3u/material/components/ThemeSelection.kt index 34be26e8..2c773674 100644 --- a/material/src/main/java/com/m3u/material/components/ThemeSelection.kt +++ b/material/src/main/java/com/m3u/material/components/ThemeSelection.kt @@ -25,6 +25,7 @@ import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.LightMode import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text @@ -43,17 +44,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.tv.material3.Border import com.m3u.material.LocalM3UHapticFeedback import com.m3u.material.ktx.InteractionType import com.m3u.material.ktx.createScheme import com.m3u.material.ktx.interactionBorder -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalSpacing import com.m3u.material.model.SugarColors -import androidx.tv.material3.Card as TvCard -import androidx.tv.material3.CardDefaults as TvCardDefaults -import androidx.tv.material3.Icon as TvIcon @Composable fun ThemeSelection( @@ -68,7 +64,6 @@ fun ThemeSelection( modifier: Modifier = Modifier, ) { val spacing = LocalSpacing.current - val tv = tv() val colorScheme = remember(argb, isDark) { createScheme(argb, isDark) @@ -144,113 +139,67 @@ fun ThemeSelection( Box( contentAlignment = Alignment.Center ) { - if (!tv) { - val zoom by animateFloatAsState( - targetValue = if (selected) 0.95f else 0.85f, - label = "zoom" - ) - val corner by animateDpAsState( - targetValue = if (!selected) spacing.extraLarge else spacing.medium, - label = "corner" - ) - val shape = RoundedCornerShape(corner) + val zoom by animateFloatAsState( + targetValue = if (selected) 0.95f else 0.85f, + label = "zoom" + ) + val corner by animateDpAsState( + targetValue = if (!selected) spacing.extraLarge else spacing.medium, + label = "corner" + ) + val shape = RoundedCornerShape(corner) - OutlinedCard( - shape = shape, - colors = CardDefaults.outlinedCardColors( - containerColor = colorScheme.background, - contentColor = colorScheme.onBackground - ), - modifier = modifier - .graphicsLayer { - scaleX = zoom - scaleY = zoom + OutlinedCard( + shape = shape, + colors = CardDefaults.outlinedCardColors( + containerColor = colorScheme.background, + contentColor = colorScheme.onBackground + ), + modifier = modifier + .graphicsLayer { + scaleX = zoom + scaleY = zoom + } + .size(96.dp) + .interactionBorder( + type = InteractionType.PRESS, + source = interactionSource, + shape = shape, + color = colorScheme.primary + ) + ) { + Box( + modifier = Modifier.combinedClickable( + interactionSource = interactionSource, + indication = ripple(), + onClick = { + if (selected) return@combinedClickable + feedback.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) + onClick() + }, + onLongClick = { + feedback.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) + onLongClick() + } + ), + content = { content() } + ) + } + + Crossfade(selected, label = "icon") { selected -> + if (!selected) { + Icon( + imageVector = when (isDark) { + true -> Icons.Rounded.DarkMode + false -> Icons.Rounded.LightMode + }, + contentDescription = "", + tint = when (isDark) { + true -> SugarColors.Tee + false -> SugarColors.Yellow } - .size(96.dp) - .interactionBorder( - type = InteractionType.PRESS, - source = interactionSource, - shape = shape, - color = colorScheme.primary - ) - ) { - Box( - modifier = Modifier.combinedClickable( - interactionSource = interactionSource, - indication = ripple(), - onClick = { - if (selected) return@combinedClickable - feedback.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) - onClick() - }, - onLongClick = { - feedback.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) - onLongClick() - } - ), - content = { content() } ) } - - Crossfade(selected, label = "icon") { selected -> - if (!selected) { - Icon( - imageVector = when (isDark) { - true -> Icons.Rounded.DarkMode - false -> Icons.Rounded.LightMode - }, - contentDescription = "", - tint = when (isDark) { - true -> SugarColors.Tee - false -> SugarColors.Yellow - } - ) - } - } - } else { - TvCard( - colors = TvCardDefaults.colors( - containerColor = colorScheme.background, - contentColor = colorScheme.onBackground - ), - shape = TvCardDefaults.shape( - RoundedCornerShape(spacing.large) - ), - border = TvCardDefaults.border(focusedBorder = Border.None), - scale = TvCardDefaults.scale( - scale = 0.8f, - focusedScale = 0.95f, - pressedScale = 0.85f - ), - onClick = { - if (selected) return@TvCard - onClick() - }, - onLongClick = { - onLongClick() - }, - modifier = modifier.size(96.dp), - content = { - Box(contentAlignment = Alignment.Center) { - content() - Crossfade(selected, label = "icon") { selected -> - if (!selected) { - TvIcon( - imageVector = when (isDark) { - true -> Icons.Rounded.DarkMode - false -> Icons.Rounded.LightMode - }, - contentDescription = "", - tint = when (isDark) { - true -> SugarColors.Tee - false -> SugarColors.Yellow - } - ) - } - } - } - } - ) } } } diff --git a/material/src/main/java/com/m3u/material/components/mask/MaskButton.kt b/material/src/main/java/com/m3u/material/components/mask/MaskButton.kt index 564d4eff..a550d856 100644 --- a/material/src/main/java/com/m3u/material/components/mask/MaskButton.kt +++ b/material/src/main/java/com/m3u/material/components/mask/MaskButton.kt @@ -1,5 +1,6 @@ package com.m3u.material.components.mask +import androidx.compose.material3.Icon import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Text import androidx.compose.material3.TooltipBox @@ -8,12 +9,9 @@ import androidx.compose.material3.TooltipState import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector -import com.m3u.material.components.IconButton -import com.m3u.material.ktx.tv -import com.m3u.material.ktx.thenIf +import androidx.compose.material3.IconButton @Composable fun MaskButton( @@ -26,8 +24,6 @@ fun MaskButton( tint: Color = Color.Unspecified, enabled: Boolean = true ) { - val tv = tv() - TooltipBox( state = tooltipState, positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(), @@ -38,21 +34,18 @@ fun MaskButton( } ) { IconButton( - icon = icon, - enabled = enabled, - contentDescription = contentDescription, onClick = { state.wake() onClick() }, - modifier = modifier.thenIf(tv) { - Modifier.onFocusEvent { - if (it.isFocused) { - state.wake() - } - } - }, - tint = tint - ) + enabled = enabled, + modifier = modifier + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = tint + ) + } } } diff --git a/material/src/main/java/com/m3u/material/components/mask/MaskCircleButton.kt b/material/src/main/java/com/m3u/material/components/mask/MaskCircleButton.kt index 5b283bad..fc6a75f7 100644 --- a/material/src/main/java/com/m3u/material/components/mask/MaskCircleButton.kt +++ b/material/src/main/java/com/m3u/material/components/mask/MaskCircleButton.kt @@ -3,6 +3,7 @@ package com.m3u.material.components.mask import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.material3.Surface import androidx.compose.runtime.Composable @@ -11,12 +12,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.takeOrElse import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp -import androidx.tv.material3.ClickableSurfaceDefaults -import com.m3u.material.components.Icon -import com.m3u.material.ktx.tv -import androidx.tv.material3.Icon as TvIcon -import androidx.tv.material3.LocalContentColor as TvLocalContentColor -import androidx.tv.material3.Surface as TvSurface @Composable fun MaskCircleButton( @@ -29,47 +24,23 @@ fun MaskCircleButton( isSmallDimension: Boolean = false, interactionSource: MutableInteractionSource? = null ) { - val tv = tv() val dimension = if (isSmallDimension) 48.dp else 64.dp - if (!tv) { - Surface( - shape = CircleShape, - enabled = enabled, - onClick = { - state.wake() - onClick() - }, - interactionSource = interactionSource, - modifier = modifier, - color = Color.Unspecified, - contentColor = tint.takeOrElse { LocalContentColor.current } - ) { - Icon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(dimension) - ) - } - } else { - TvSurface( - shape = ClickableSurfaceDefaults.shape(CircleShape), - enabled = enabled, - onClick = { - state.wake() - onClick() - }, - interactionSource = interactionSource, - modifier = modifier, - colors = ClickableSurfaceDefaults.colors( - containerColor = Color.Unspecified, - contentColor = tint.takeOrElse { TvLocalContentColor.current } - ) - ) { - TvIcon( - imageVector = icon, - contentDescription = null, - modifier = Modifier.size(dimension) - ) - } + Surface( + shape = CircleShape, + enabled = enabled, + onClick = { + state.wake() + onClick() + }, + interactionSource = interactionSource, + modifier = modifier, + color = Color.Unspecified, + contentColor = tint.takeOrElse { LocalContentColor.current } + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(dimension) + ) } } \ No newline at end of file diff --git a/material/src/main/java/com/m3u/material/components/tv/Dialog.kt b/material/src/main/java/com/m3u/material/components/tv/Dialog.kt deleted file mode 100644 index a0266b2a..00000000 --- a/material/src/main/java/com/m3u/material/components/tv/Dialog.kt +++ /dev/null @@ -1,685 +0,0 @@ -package com.m3u.material.components.tv - -import androidx.compose.animation.core.CubicBezierEasing -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.Transition -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.foundation.focusGroup -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.ReadOnlyComposable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.Modifier -import androidx.compose.ui.composed -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.focus.FocusDirection -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusProperties -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.layout.Layout -import androidx.compose.ui.layout.Placeable -import androidx.compose.ui.platform.LocalFocusManager -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.semantics.dismiss -import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import androidx.compose.ui.window.DialogProperties -import androidx.tv.material3.ExperimentalTvMaterial3Api -import androidx.tv.material3.surfaceColorAtElevation -import kotlin.math.max -import androidx.tv.material3.ColorScheme as TvColorScheme -import androidx.tv.material3.LocalContentColor as TvLocalContentColor -import androidx.tv.material3.MaterialTheme as TvMaterialTheme -import androidx.tv.material3.ProvideTextStyle as TvProvideTextStyle - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun StandardDialog( - showDialog: Boolean, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - dismissButton: @Composable (() -> Unit)? = null, - icon: @Composable (() -> Unit)? = null, - title: @Composable (() -> Unit)? = null, - text: @Composable (() -> Unit)? = null, - shape: Shape = StandardDialogDefaults.shape, - containerColor: Color = StandardDialogDefaults.containerColor, - iconContentColor: Color = StandardDialogDefaults.iconContentColor, - titleContentColor: Color = StandardDialogDefaults.titleContentColor, - textContentColor: Color = StandardDialogDefaults.textContentColor, - tonalElevation: Dp = StandardDialogDefaults.TonalElevation, - properties: DialogProperties = DialogProperties(), - confirmButton: @Composable () -> Unit -) { - val elevatedContainerColor = TvMaterialTheme.colorScheme.applyTonalElevation( - backgroundColor = containerColor, - elevation = tonalElevation - ) - - Dialog( - showDialog = showDialog, - onDismissRequest = onDismissRequest, - properties = properties - ) { - Column( - modifier = Modifier - .widthIn( - min = StandardDialogDefaults.DialogMinWidth, - max = StandardDialogDefaults.DialogMaxWidth - ) - .dialogFocusable() - .then(modifier) - .graphicsLayer { - this.clip = true - this.shape = shape - } - .drawBehind { drawRect(color = elevatedContainerColor) } - .padding(StandardDialogDefaults.DialogPadding) - ) { - icon?.let { nnIcon -> - CompositionLocalProvider( - TvLocalContentColor provides iconContentColor, - content = { - nnIcon() - Spacer( - modifier = Modifier.padding(StandardDialogDefaults.IconBottomSpacing) - ) - } - ) - } - title?.let { nnTitle -> - CompositionLocalProvider(TvLocalContentColor provides titleContentColor) { - TvProvideTextStyle( - value = StandardDialogDefaults.titleTextStyle, - content = { - Box( - modifier = Modifier.heightIn( - max = StandardDialogDefaults.TitleMaxHeight - ) - ) { nnTitle() } - } - ) - } - } - text?.let { nnText -> - CompositionLocalProvider(TvLocalContentColor provides textContentColor) { - TvProvideTextStyle( - value = StandardDialogDefaults.textStyle, - content = { - Spacer(modifier = Modifier.padding(StandardDialogDefaults.TextPadding)) - Box(modifier = Modifier.weight(weight = 1f, fill = false)) { - nnText() - } - } - ) - } - } - Spacer(modifier = Modifier.padding(StandardDialogDefaults.ButtonsFlowRowPadding)) - TvProvideTextStyle( - value = StandardDialogDefaults.buttonsTextStyle, - content = { - DialogFlowRow( - mainAxisSpacing = StandardDialogDefaults.ButtonsMainAxisSpacing, - crossAxisSpacing = StandardDialogDefaults.ButtonsCrossAxisSpacing - ) { - confirmButton() - dismissButton?.invoke() - } - } - ) - } - } -} - -@OptIn(ExperimentalComposeUiApi::class) -@Composable -fun FullScreenDialog( - showDialog: Boolean, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - dismissButton: @Composable (() -> Unit)? = null, - icon: @Composable (() -> Unit)? = null, - title: @Composable (() -> Unit)? = null, - text: @Composable (() -> Unit)? = null, - backgroundColor: Color = FullScreenDialogDefaults.backgroundColor, - iconContentColor: Color = FullScreenDialogDefaults.iconContentColor, - titleContentColor: Color = FullScreenDialogDefaults.titleContentColor, - textContentColor: Color = FullScreenDialogDefaults.descriptionContentColor, - properties: DialogProperties = DialogProperties(), - confirmButton: @Composable () -> Unit -) { - Dialog( - showDialog = showDialog, - onDismissRequest = onDismissRequest, - properties = properties - ) { - Box( - modifier = Modifier - .fillMaxSize() - .drawBehind { drawRect(color = backgroundColor) } - .dialogFocusable(), - contentAlignment = Alignment.Center - ) { - Column( - modifier = Modifier - .fillMaxWidth(FullScreenDialogDefaults.DialogMaxWidth) - .then(modifier), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - CompositionLocalProvider( - TvLocalContentColor provides iconContentColor, - content = { - icon?.let { nnIcon -> - nnIcon() - Spacer( - modifier = Modifier.padding(FullScreenDialogDefaults.IconPadding) - ) - } - } - ) - - CompositionLocalProvider( - TvLocalContentColor provides titleContentColor, - content = { - title?.let { nnTitle -> - TvProvideTextStyle( - value = FullScreenDialogDefaults.titleTextStyle - ) { - nnTitle() - Spacer( - modifier = Modifier.padding( - FullScreenDialogDefaults.TitlePadding - ) - ) - } - } - } - ) - - CompositionLocalProvider( - TvLocalContentColor provides textContentColor, - content = { - text?.let { nnText -> - TvProvideTextStyle(FullScreenDialogDefaults.descriptionTextStyle) { - Box( - modifier = Modifier.weight(weight = 1f, fill = false) - ) { nnText() } - Spacer( - modifier = Modifier.padding( - FullScreenDialogDefaults.DescriptionPadding - ) - ) - } - } - } - ) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy( - space = FullScreenDialogDefaults.ButtonSpacing, - alignment = Alignment.CenterHorizontally - ) - ) { - TvProvideTextStyle(value = FullScreenDialogDefaults.buttonsTextStyle) { - confirmButton() - dismissButton?.invoke() - } - } - } - } - } -} - -/** - * A state object that can be hoisted to control and observe a [Dialog]'s animation progress. - */ -@ExperimentalTvMaterial3Api -class DialogState { - /** - * Current animation progress of the [Dialog]. This value will range between 0f and 1f. This - * progress is generally linked with the dialog's alpha progress as it is the first element to - * be displayed on the view and the last element to the removed from the view. - */ - var animationProgress by mutableStateOf(0f) - private set - - internal fun updateProgress(currentProgress: Float) { - animationProgress = currentProgress - } -} - -/** - * [Dialog] displays a full-screen dialog, layered over any other content. It takes a single - * composable slot, which is expected to be an opinionated TV dialog content, such as - * [StandardDialog], [FullScreenDialog], etc. - - * @param showDialog Controls whether to display the [Dialog]. Set to true initially to trigger - * an 'intro' animation and display the [Dialog]. Subsequently, setting to false triggers - * an 'outro' animation, then [Dialog] calls [onDismissRequest] and hides itself. - * @param onDismissRequest Executes when the user dismisses the dialog. - * Must remove the dialog from the composition. - * @param modifier Modifier to be applied to the dialog. - * @param properties Typically platform specific properties to further configure the dialog. - * @param content Slot for dialog content such as [StandardDialog], [FullScreenDialog], etc. - */ -@ExperimentalComposeUiApi -@ExperimentalTvMaterial3Api -@Composable -fun Dialog( - showDialog: Boolean, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - properties: DialogProperties = DialogProperties(), - state: DialogState = remember { DialogState() }, - content: @Composable BoxScope.() -> Unit -) { - // Transitions for background and 'dialog content' alpha. - var alphaTransitionState by remember { - mutableStateOf(MutableTransitionState(AnimationStage.Intro)) - } - val alphaTransition = updateTransition(alphaTransitionState, label = "alphaTransition") - - // Transitions for dialog content scaling. - var scaleTransitionState by remember { - mutableStateOf(MutableTransitionState(AnimationStage.Intro)) - } - val scaleTransition = updateTransition(scaleTransitionState, label = "scaleTransition") - - if (showDialog || alphaTransitionState.targetState != AnimationStage.Intro || - scaleTransitionState.targetState != AnimationStage.Intro - ) { - Dialog( - onDismissRequest = onDismissRequest, - properties = properties, - ) { - val alpha by animateDialogAlpha(alphaTransition, alphaTransitionState) - val scale by animateDialogScale(scaleTransition, scaleTransitionState) - - Box( - modifier = Modifier - .fillMaxSize() - .graphicsLayer { - this.scaleX = scale - this.scaleY = scale - this.alpha = alpha - } - .semantics { - dismiss { - onDismissRequest() - true - } - } - .then(modifier), - contentAlignment = Alignment.Center, - content = content - ) - - LaunchedEffect(alpha) { - state.updateProgress(currentProgress = alpha) - } - - // Trigger Outro animation when the caller updates showDialog to false. - LaunchedEffect(showDialog) { - if (!showDialog) { - // a) Fade out dialog contents b) Scale down dialog contents. - alphaTransitionState.targetState = AnimationStage.Outro - scaleTransitionState.targetState = AnimationStage.Outro - } - } - - LaunchedEffect(alphaTransitionState.currentState) { - when (alphaTransitionState.currentState) { - AnimationStage.Intro -> { - // a) Fade in dialog background b) Scale up dialog contents. - alphaTransitionState.targetState = AnimationStage.Display - scaleTransitionState.targetState = AnimationStage.Display - } - - AnimationStage.Outro -> { - // After the outro animation, leave the dialog & reset alpha/scale - // transitions. - onDismissRequest() - alphaTransitionState = MutableTransitionState(AnimationStage.Intro) - scaleTransitionState = MutableTransitionState(AnimationStage.Intro) - } - - else -> Unit - } - } - } - } -} - -/** - * Simple clone of FlowRow that arranges its children in a horizontal flow with limited - * customization. - */ -@Composable -internal fun DialogFlowRow( - mainAxisSpacing: Dp, - crossAxisSpacing: Dp, - content: @Composable () -> Unit -) { - val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl - Layout(content) { measurables, constraints -> - val sequences = mutableListOf>() - val crossAxisSizes = mutableListOf() - val crossAxisPositions = mutableListOf() - - var mainAxisSpace = 0 - var crossAxisSpace = 0 - - val currentSequence = mutableListOf() - var currentMainAxisSize = 0 - var currentCrossAxisSize = 0 - - // Return whether the placeable can be added to the current sequence. - fun canAddToCurrentSequence(placeable: Placeable) = - currentSequence.isEmpty() || currentMainAxisSize + mainAxisSpacing.roundToPx() + - placeable.width <= constraints.maxWidth - - // Store current sequence information and start a new sequence. - fun startNewSequence() { - if (sequences.isNotEmpty()) { - crossAxisSpace += crossAxisSpacing.roundToPx() - } - sequences += currentSequence.toList() - crossAxisSizes += currentCrossAxisSize - crossAxisPositions += crossAxisSpace - - crossAxisSpace += currentCrossAxisSize - mainAxisSpace = max(mainAxisSpace, currentMainAxisSize) - - currentSequence.clear() - currentMainAxisSize = 0 - currentCrossAxisSize = 0 - } - - val measurablesList = if (isRtl) measurables.reversed() else measurables - - for (measurable in measurablesList) { - // Ask the child for its preferred size. - val placeable = measurable.measure(constraints) - - // Start a new sequence if there is not enough space. - if (!canAddToCurrentSequence(placeable)) startNewSequence() - - // Add the child to the current sequence. - if (currentSequence.isNotEmpty()) { - currentMainAxisSize += mainAxisSpacing.roundToPx() - } - currentSequence.add(placeable) - currentMainAxisSize += placeable.width - currentCrossAxisSize = max(currentCrossAxisSize, placeable.height) - } - - if (currentSequence.isNotEmpty()) startNewSequence() - - val mainAxisLayoutSize = max(mainAxisSpace, constraints.minWidth) - - val crossAxisLayoutSize = max(crossAxisSpace, constraints.minHeight) - - layout(mainAxisLayoutSize, crossAxisLayoutSize) { - sequences.forEachIndexed { i, placeables -> - val childrenMainAxisSizes = IntArray(placeables.size) { j -> - placeables[j].width + - if (j < placeables.lastIndex) mainAxisSpacing.roundToPx() else 0 - } - val arrangement = Arrangement.Bottom - // Handle vertical direction - val mainAxisPositions = IntArray(childrenMainAxisSizes.size) { 0 } - with(arrangement) { - arrange(mainAxisLayoutSize, childrenMainAxisSizes, mainAxisPositions) - } - placeables.forEachIndexed { j, placeable -> - placeable.place( - x = mainAxisPositions[j], - y = crossAxisPositions[i] - ) - } - } - } - } -} - -/** - * Makes the current dialog a focus group with a [FocusRequester] and restricts the focus from - * exiting its bounds while it's visible. - */ -@OptIn(ExperimentalComposeUiApi::class) -fun Modifier.dialogFocusable() = composed { - val focusRequester = remember { FocusRequester() } - val focusManager = LocalFocusManager.current - - LaunchedEffect(Unit) { - focusRequester.requestFocus() - focusManager.moveFocus(FocusDirection.Enter) - } - this.then( - Modifier - .focusRequester(focusRequester) - .focusProperties { exit = { FocusRequester.Cancel } } - .focusGroup() - ) -} - -object StandardDialogDefaults { - internal val DialogMinWidth = 280.dp - internal val DialogMaxWidth = 560.dp - - internal val TitleMaxHeight = 56.dp - internal val ButtonsMainAxisSpacing = 16.dp - internal val ButtonsCrossAxisSpacing = 16.dp - - internal val DialogPadding = PaddingValues(all = 24.dp) - internal val IconBottomSpacing = PaddingValues(top = 32.dp) - internal val TextPadding = PaddingValues(top = 20.dp) - internal val ButtonsFlowRowPadding = PaddingValues(top = 24.dp) - private const val TextColorOpacity = 0.8f - - /** The default shape for StandardDialogs */ - val shape: Shape = RoundedCornerShape(28.0.dp) - - /** The default container color for StandardDialogs */ - val containerColor: Color - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.colorScheme.inverseOnSurface - - /** The default icon color for StandardDialogs */ - val iconContentColor: Color - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.colorScheme.secondary - - /** The default title color for StandardDialogs */ - val titleContentColor: Color - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.colorScheme.onSurface - - /** The default [TextStyle] for StandardDialogs' title */ - val titleTextStyle: TextStyle - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.typography.headlineMedium - - /** The default [TextStyle] for StandardDialogs' buttons */ - val buttonsTextStyle - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.typography.labelLarge - - /** The default text color for StandardDialogs */ - val textContentColor: Color - @ReadOnlyComposable - - @Composable get() = TvMaterialTheme.colorScheme.onSurfaceVariant - - /** The default text style for StandardDialogs */ - val textStyle - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.typography.bodyLarge - .copy(color = TvLocalContentColor.current.copy(alpha = TextColorOpacity)) - - /** The default tonal elevation for StandardDialogs */ - val TonalElevation: Dp = 6.dp -} - -@ExperimentalTvMaterial3Api -object FullScreenDialogDefaults { - internal val ButtonSpacing = 16.dp - internal val DescriptionPadding = PaddingValues(top = 48.dp) - internal val TitlePadding = PaddingValues(top = 20.dp) - internal val IconPadding = PaddingValues(top = 32.dp) - internal const val DialogMaxWidth = .5f - private const val DescriptionColorOpacity = 0.8f - - /** The default background color for FullScreenDialogs */ - val backgroundColor: Color - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.colorScheme.background - - /** The default icon color for FullScreenDialogs */ - val iconContentColor: Color - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.colorScheme.onSurface - - /** The default title color for FullScreenDialogs */ - val titleContentColor: Color - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.colorScheme.onSurface - - /** The default title text style for FullScreenDialogs */ - val titleTextStyle: TextStyle - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.typography.headlineMedium - .copy(textAlign = TextAlign.Center) - - /** The default buttons text style for FullScreenDialogs */ - val buttonsTextStyle: TextStyle - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.typography.labelLarge - - /** The default description text color for FullScreenDialogs */ - val descriptionContentColor: Color - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.colorScheme.onSurfaceVariant - - /** The default description text style for FullScreenDialogs */ - val descriptionTextStyle: TextStyle - @ReadOnlyComposable - @Composable get() = TvMaterialTheme.typography.bodyLarge.copy( - textAlign = TextAlign.Center, - color = TvLocalContentColor.current.copy(alpha = DescriptionColorOpacity) - ) -} - -@Composable -private fun animateDialogAlpha( - alphaTransition: Transition, - alphaTransitionState: MutableTransitionState -) = alphaTransition.animateFloat( - transitionSpec = { - if (alphaTransitionState.currentState == AnimationStage.Intro) - tween( - durationMillis = ENTER_DURATION, - easing = MotionTokens.EnterEasing, - delayMillis = ENTER_DELAY - ) - else if (alphaTransitionState.targetState == AnimationStage.Outro) - tween( - durationMillis = EXIT_DURATION, - easing = MotionTokens.ExitEasing, - delayMillis = EXIT_DELAY - ) - else - tween(durationMillis = 0) - }, - label = "alpha" -) { stage -> - when (stage) { - AnimationStage.Intro -> 0.0f - AnimationStage.Display -> 1.0f - AnimationStage.Outro -> 0.0f - } -} - -@Composable -private fun animateDialogScale( - scaleTransition: Transition, - scaleTransitionState: MutableTransitionState -) = scaleTransition.animateFloat( - transitionSpec = { - if (scaleTransitionState.currentState == AnimationStage.Intro) - tween( - durationMillis = ENTER_DURATION, - easing = MotionTokens.EnterEasing, - delayMillis = ENTER_DELAY - ) - else - tween( - durationMillis = EXIT_DURATION, - easing = MotionTokens.ExitEasing, - delayMillis = EXIT_DELAY - ) - }, - label = "scale" -) { stage -> - when (stage) { - AnimationStage.Intro -> 0.97f - AnimationStage.Display -> 1.0f - AnimationStage.Outro -> 0.97f - } -} - -// Transition stages - scaling and alpha is applied as single Intro/Outro animations. -private enum class AnimationStage { - Intro, Display, Outro; -} - -private const val ENTER_DURATION = 500 -private const val EXIT_DURATION = 250 - -private const val ENTER_DELAY = 250 -private const val EXIT_DELAY = 150 - -object MotionTokens { - val EnterEasing = CubicBezierEasing(0.12f, 1f, 0.4f, 1f) - val ExitEasing = CubicBezierEasing(0.4f, 1f, 0.12f, 1f) -} - -@OptIn(ExperimentalTvMaterial3Api::class) -private fun TvColorScheme.applyTonalElevation(backgroundColor: Color, elevation: Dp): Color { - return if (backgroundColor == surface) { - surfaceColorAtElevation(elevation) - } else { - backgroundColor - } -} diff --git a/material/src/main/java/com/m3u/material/ktx/Tv.kt b/material/src/main/java/com/m3u/material/ktx/Tv.kt deleted file mode 100644 index 35cd4a73..00000000 --- a/material/src/main/java/com/m3u/material/ktx/Tv.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.m3u.material.ktx - -import android.content.res.Configuration -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Typography -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration -import com.m3u.material.model.LocalSpacing -import com.m3u.material.model.asTvScheme -import com.m3u.material.model.asTvTypography -import androidx.tv.material3.MaterialTheme as TvMaterialTheme - -@Composable -fun tv(): Boolean = LocalConfiguration.current.run { - val type = uiMode and Configuration.UI_MODE_TYPE_MASK - type == Configuration.UI_MODE_TYPE_TELEVISION -} - -/** - * Check current Platform and apply new colorScheme. - * @param fallback apply std material3 MaterialTheme as well. - */ -@Composable -internal fun PlatformTheme( - colorScheme: ColorScheme = MaterialTheme.colorScheme, - typography: Typography = MaterialTheme.typography, - fallback: Boolean = true, - block: @Composable () -> Unit -) { - val tv = tv() - val car = false - val content = @Composable { - when { - tv -> { - TvMaterialTheme( - colorScheme = remember(colorScheme) { colorScheme.asTvScheme() }, - typography = remember(typography) { typography.asTvTypography() } - ) { - block() - } - } - - car -> throw UnsupportedOperationException() - else -> block() - } - } - val commonPlatform = !tv && !car - if (commonPlatform || fallback) { - MaterialTheme( - colorScheme = colorScheme, - typography = typography - ) { - content() - } - } else { - content() - } -} - -@Composable -fun Modifier.includeChildGlowPadding(): Modifier = thenIf(tv()) { - Modifier.padding(LocalSpacing.current.medium) -} diff --git a/material/src/main/java/com/m3u/material/model/Theme.kt b/material/src/main/java/com/m3u/material/model/Theme.kt index 48d1b027..37e19502 100644 --- a/material/src/main/java/com/m3u/material/model/Theme.kt +++ b/material/src/main/java/com/m3u/material/model/Theme.kt @@ -3,17 +3,14 @@ package com.m3u.material.model import android.annotation.SuppressLint import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Typography import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext -import com.m3u.material.ktx.PlatformTheme import com.m3u.material.ktx.createScheme -import androidx.tv.material3.ColorScheme as TvColorScheme -import androidx.tv.material3.Typography as TvTypography @Composable @SuppressLint("RestrictedApi") @@ -34,61 +31,10 @@ fun Theme( } } - PlatformTheme( + MaterialTheme( colorScheme = colorScheme, typography = typography ) { content() } } - -fun ColorScheme.asTvScheme(): TvColorScheme { - return TvColorScheme( - primary = primary, - onPrimary = onPrimary, - primaryContainer = primaryContainer, - onPrimaryContainer = onPrimaryContainer, - inversePrimary = inversePrimary, - secondary = secondary, - onSecondary = onSecondary, - secondaryContainer = secondaryContainer, - onSecondaryContainer = onSecondaryContainer, - tertiary = tertiary, - onTertiary = onTertiary, - tertiaryContainer = tertiaryContainer, - onTertiaryContainer = onTertiaryContainer, - background = background, - onBackground = onBackground, - surface = surface, - onSurface = onSurface, - surfaceVariant = surfaceVariant, - onSurfaceVariant = onSurfaceVariant, - surfaceTint = surfaceVariant, // todo - inverseSurface = inverseSurface, - inverseOnSurface = inverseOnSurface, - error = error, - onError = onError, - errorContainer = errorContainer, - onErrorContainer = onErrorContainer, - scrim = scrim, - border = outline, - borderVariant = outlineVariant - ) -} - -fun Typography.asTvTypography(): TvTypography { - return TvTypography( - displayLarge = displayLarge, - displayMedium = displayMedium, - displaySmall = displaySmall, - headlineLarge = headlineLarge, - headlineMedium = headlineMedium, - headlineSmall = headlineSmall, - titleLarge = titleLarge, - titleMedium = titleMedium, - titleSmall = titleSmall, - bodyLarge = bodyLarge, - bodyMedium = bodyMedium, - bodySmall = bodySmall - ) -} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b2c34b29..ccb6d0e6 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -34,3 +34,4 @@ include(":i18n") include(":codec:lite", ":codec:rich") include(":annotation") include(":processor") +include(":tv") diff --git a/smartphone/src/main/java/com/m3u/smartphone/ui/App.kt b/smartphone/src/main/java/com/m3u/smartphone/ui/App.kt index 0f413ab7..251e3859 100644 --- a/smartphone/src/main/java/com/m3u/smartphone/ui/App.kt +++ b/smartphone/src/main/java/com/m3u/smartphone/ui/App.kt @@ -34,8 +34,7 @@ import com.m3u.smartphone.ui.sheet.RemoteControlSheet import com.m3u.smartphone.ui.sheet.RemoteControlSheetValue import com.m3u.core.architecture.preferences.hiltPreferences import com.m3u.data.tv.model.RemoteDirection -import com.m3u.material.components.Icon -import com.m3u.material.ktx.tv +import androidx.compose.material3.Icon import com.m3u.material.model.LocalSpacing import com.m3u.ui.Destination import com.m3u.ui.FontFamilies @@ -108,8 +107,6 @@ private fun AppImpl( val spacing = LocalSpacing.current val preferences = hiltPreferences() - val tv = tv() - val entry by navController.currentBackStackEntryAsState() val rootDestination by remember { @@ -147,7 +144,7 @@ private fun AppImpl( ) { SnackHost(Modifier.weight(1f)) AnimatedVisibility( - visible = !tv && preferences.remoteControl, + visible = preferences.remoteControl, enter = scaleIn(initialScale = 0.65f) + fadeIn(), exit = scaleOut(targetScale = 0.65f) + fadeOut(), ) { diff --git a/smartphone/src/main/java/com/m3u/smartphone/ui/AppNavHost.kt b/smartphone/src/main/java/com/m3u/smartphone/ui/AppNavHost.kt index d715a58c..e397b5f4 100644 --- a/smartphone/src/main/java/com/m3u/smartphone/ui/AppNavHost.kt +++ b/smartphone/src/main/java/com/m3u/smartphone/ui/AppNavHost.kt @@ -18,9 +18,7 @@ import com.m3u.feature.playlist.configuration.navigateToPlaylistConfiguration import com.m3u.feature.playlist.configuration.playlistConfigurationScreen import com.m3u.feature.playlist.navigation.navigateToPlaylist import com.m3u.feature.playlist.navigation.playlistScreen -import com.m3u.feature.playlist.navigation.playlistTvScreen import com.m3u.feature.channel.PlayerActivity -import com.m3u.material.ktx.tv import com.m3u.ui.Destination import com.m3u.ui.Events import com.m3u.ui.SettingDestination @@ -36,8 +34,6 @@ fun AppNavHost( val context = LocalContext.current val preferences = hiltPreferences() - val tv = tv() - NavHost( navController = navController, startDestination = startDestination, @@ -48,7 +44,7 @@ fun AppNavHost( rootGraph( contentPadding = contentPadding, navigateToPlaylist = { playlist -> - navController.navigateToPlaylist(playlist.url, tv) + navController.navigateToPlaylist(playlist.url) }, navigateToChannel = { if (preferences.zappingMode && PlayerActivity.isInPipMode) return@rootGraph @@ -87,7 +83,6 @@ fun AppNavHost( }, contentPadding = contentPadding ) - playlistTvScreen() playlistConfigurationScreen(contentPadding) } } diff --git a/smartphone/src/main/java/com/m3u/smartphone/ui/Scaffold.kt b/smartphone/src/main/java/com/m3u/smartphone/ui/Scaffold.kt index 9a32d00f..6eefcdad 100644 --- a/smartphone/src/main/java/com/m3u/smartphone/ui/Scaffold.kt +++ b/smartphone/src/main/java/com/m3u/smartphone/ui/Scaffold.kt @@ -17,6 +17,8 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -40,16 +42,12 @@ import androidx.compose.ui.unit.offset import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastMap import androidx.compose.ui.util.fastMaxOfOrNull -import com.m3u.smartphone.ui.internal.SmartphoneScaffoldImpl -import com.m3u.smartphone.ui.internal.TabletScaffoldImpl -import com.m3u.smartphone.ui.internal.TvScaffoldImpl import com.m3u.material.components.Background -import com.m3u.material.components.Icon -import com.m3u.material.components.IconButton import com.m3u.material.effects.currentBackStackEntry -import com.m3u.material.ktx.tv import com.m3u.material.model.LocalHazeState import com.m3u.material.model.LocalSpacing +import com.m3u.smartphone.ui.internal.SmartphoneScaffoldImpl +import com.m3u.smartphone.ui.internal.TabletScaffoldImpl import com.m3u.ui.Destination import com.m3u.ui.FontFamilies import com.m3u.ui.helper.Fob @@ -59,8 +57,6 @@ import com.m3u.ui.helper.useRailNav import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.hazeChild -import androidx.tv.material3.Icon as TvIcon -import androidx.tv.material3.Text as TvText @Composable @OptIn(InternalComposeApi::class) @@ -73,23 +69,12 @@ internal fun Scaffold( content: @Composable BoxScope.(PaddingValues) -> Unit ) { val useRailNav = LocalHelper.current.useRailNav - val tv = tv() val hazeState = remember { HazeState() } CompositionLocalProvider(LocalHazeState provides hazeState) { Background { when { - tv -> { - TvScaffoldImpl( - rootDestination = rootDestination, - navigateToRoot = navigateToRootDestination, - onBackPressed = onBackPressed, - content = content, - modifier = modifier - ) - } - !useRailNav -> { SmartphoneScaffoldImpl( rootDestination = rootDestination, @@ -132,7 +117,6 @@ internal fun MainContent( onBackPressed: (() -> Unit)?, content: @Composable BoxScope.(PaddingValues) -> Unit ) { - val tv = tv() val spacing = LocalSpacing.current val hazeState = LocalHazeState.current @@ -144,72 +128,76 @@ internal fun MainContent( Scaffold( topBar = { - if (!tv) { - TopAppBar( - colors = TopAppBarDefaults.topAppBarColors(Color.Transparent), - windowInsets = windowInsets, - title = { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.defaultMinSize(minHeight = 56.dp) + TopAppBar( + colors = TopAppBarDefaults.topAppBarColors(Color.Transparent), + windowInsets = windowInsets, + title = { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.defaultMinSize(minHeight = 56.dp) + ) { + Column( + modifier = Modifier + .padding(horizontal = spacing.medium) + .weight(1f) ) { - Column( - modifier = Modifier - .padding(horizontal = spacing.medium) - .weight(1f) - ) { + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + fontFamily = FontFamilies.LexendExa + ) + AnimatedVisibility(subtitle.text.isNotEmpty()) { Text( - text = title, + text = subtitle, + style = MaterialTheme.typography.titleMedium, maxLines = 1, - overflow = TextOverflow.Ellipsis, - fontFamily = FontFamilies.LexendExa + overflow = TextOverflow.Ellipsis ) - AnimatedVisibility(subtitle.text.isNotEmpty()) { - Text( - text = subtitle, - style = MaterialTheme.typography.titleMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } } - - Row { - actions.forEach { action -> - IconButton( - icon = action.icon, - contentDescription = action.contentDescription, - onClick = action.onClick, - enabled = action.enabled - ) - } - } - - Spacer(modifier = Modifier.width(spacing.medium)) } - }, - navigationIcon = { - AnimatedContent( - targetState = onBackPressed, - label = "app-scaffold-icon" - ) { onBackPressed -> - if (onBackPressed != null) { + + Row { + actions.forEach { action -> IconButton( - icon = backStackEntry?.navigationIcon + onClick = action.onClick, + enabled = action.enabled + ) { + Icon( + imageVector = action.icon, + contentDescription = action.contentDescription, + ) + } + } + } + + Spacer(modifier = Modifier.width(spacing.medium)) + } + }, + navigationIcon = { + AnimatedContent( + targetState = onBackPressed, + label = "app-scaffold-icon" + ) { onBackPressed -> + if (onBackPressed != null) { + IconButton( + onClick = onBackPressed, + modifier = Modifier.wrapContentSize() + ) { + Icon( + imageVector = backStackEntry?.navigationIcon ?: Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = null, - onClick = onBackPressed, - modifier = Modifier.wrapContentSize() ) } } - }, - modifier = Modifier - .hazeChild(hazeState, style = HazeStyle(blurRadius = 6.dp)) - .fillMaxWidth() - ) - } + } + }, + modifier = Modifier + .hazeChild(hazeState, style = HazeStyle(blurRadius = 6.dp)) + .fillMaxWidth() + ) }, contentWindowInsets = windowInsets, containerColor = Color.Transparent @@ -238,47 +226,26 @@ internal fun NavigationItemLayout( ) { val hapticFeedback = LocalHapticFeedback.current - val tv = tv() val usefob = fob?.rootDestination == currentRootDestination val selected = usefob || currentRootDestination == rootDestination val icon = @Composable { - if (!tv) { - Icon( - imageVector = when { - fob != null && usefob -> fob.icon - selected -> currentRootDestination.selectedIcon - else -> currentRootDestination.unselectedIcon - }, - contentDescription = null - ) - } else { - TvIcon( - imageVector = when { - fob != null && usefob -> fob.icon - selected -> currentRootDestination.selectedIcon - else -> currentRootDestination.unselectedIcon - }, - contentDescription = null - ) - } + Icon( + imageVector = when { + fob != null && usefob -> fob.icon + selected -> currentRootDestination.selectedIcon + else -> currentRootDestination.unselectedIcon + }, + contentDescription = null + ) } val label: @Composable () -> Unit = remember(usefob, fob) { @Composable { - if (!tv) { - Text( - text = stringResource( - if (usefob) fob.iconTextId - else currentRootDestination.iconTextId - ).uppercase() - ) - } else { - TvText( - text = stringResource( - if (usefob) fob.iconTextId - else currentRootDestination.iconTextId - ).uppercase() - ) - } + Text( + text = stringResource( + if (usefob) fob.iconTextId + else currentRootDestination.iconTextId + ).uppercase() + ) } } val actualOnClick: () -> Unit = if (usefob) { diff --git a/smartphone/src/main/java/com/m3u/smartphone/ui/StarBackground.kt b/smartphone/src/main/java/com/m3u/smartphone/ui/StarBackground.kt index c5093a94..4b19dc7c 100644 --- a/smartphone/src/main/java/com/m3u/smartphone/ui/StarBackground.kt +++ b/smartphone/src/main/java/com/m3u/smartphone/ui/StarBackground.kt @@ -32,7 +32,6 @@ import androidx.graphics.shapes.RoundedPolygon import androidx.graphics.shapes.star import androidx.graphics.shapes.toPath import com.m3u.core.architecture.preferences.hiltPreferences -import com.m3u.material.ktx.tv data class StarSpec( val numVertices: Int, @@ -88,11 +87,10 @@ fun StarBackground( modifier: Modifier = Modifier, colors: StarColors = StarColors.defaults(), ) { - val tv = tv() val preferences = hiltPreferences() val specs = remember(colors) { createStarSpecs(colors) } AnimatedVisibility( - visible = !tv && preferences.colorfulBackground, + visible = preferences.colorfulBackground, enter = fadeIn() + scaleIn(initialScale = 2.3f), exit = fadeOut() + scaleOut(targetScale = 2.3f), modifier = modifier diff --git a/smartphone/src/main/java/com/m3u/smartphone/ui/internal/TelevisionScaffoldImpl.kt b/smartphone/src/main/java/com/m3u/smartphone/ui/internal/TelevisionScaffoldImpl.kt deleted file mode 100644 index 6c579702..00000000 --- a/smartphone/src/main/java/com/m3u/smartphone/ui/internal/TelevisionScaffoldImpl.kt +++ /dev/null @@ -1,139 +0,0 @@ -package com.m3u.smartphone.ui.internal - -import androidx.compose.animation.animateColorAsState -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.runtime.InternalComposeApi -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import com.m3u.smartphone.ui.Items -import com.m3u.smartphone.ui.MainContent -import com.m3u.smartphone.ui.NavigationItemLayout -import com.m3u.smartphone.ui.ScaffoldLayout -import com.m3u.smartphone.ui.ScaffoldRole -import com.m3u.material.components.Background -import com.m3u.material.ktx.plus -import com.m3u.material.model.LocalSpacing -import com.m3u.ui.Destination -import com.m3u.ui.helper.Metadata -import androidx.tv.material3.Border as TvBorder -import androidx.tv.material3.Card as TvCard -import androidx.tv.material3.CardDefaults as TvCardDefaults -import androidx.tv.material3.MaterialTheme as TvMaterialTheme - -@Composable -@InternalComposeApi -fun TvScaffoldImpl( - rootDestination: Destination.Root?, - navigateToRoot: (Destination.Root) -> Unit, - onBackPressed: (() -> Unit)?, - content: @Composable BoxScope.(PaddingValues) -> Unit, - modifier: Modifier = Modifier -) { - val spacing = LocalSpacing.current - val fob = Metadata.fob - - val navigation = @Composable { - TvNavigation { - Items { currentRootDestination -> - NavigationItemLayout( - rootDestination = rootDestination, - fob = fob, - currentRootDestination = currentRootDestination, - navigateToRoot = navigateToRoot - ) { selected: Boolean, - onClick: () -> Unit, - icon: @Composable () -> Unit, - _: @Composable () -> Unit -> - val source = remember { MutableInteractionSource() } - val focused by source.collectIsFocusedAsState() - val currentContainerColor by with(TvMaterialTheme.colorScheme) { - animateColorAsState( - targetValue = when { - selected -> inverseSurface - focused -> primaryContainer.copy(0.67f) - else -> background - }, - label = "scaffold-navigation-container" - ) - } - val currentContentColor by with(TvMaterialTheme.colorScheme) { - animateColorAsState( - targetValue = when { - selected -> inverseOnSurface - focused -> onPrimaryContainer - else -> onBackground - }, - label = "scaffold-navigation-content" - ) - } - TvCard( - onClick = onClick, - colors = TvCardDefaults.colors( - containerColor = currentContainerColor, - contentColor = currentContentColor - ), - interactionSource = source, - shape = TvCardDefaults.shape(CircleShape), - border = TvCardDefaults.border(focusedBorder = TvBorder.None), - scale = TvCardDefaults.scale( - scale = if (selected) 1.1f else 1f, - focusedScale = if (selected) 1.2f else 1.1f - ), - content = { - Box(modifier = Modifier.padding(spacing.medium)) { icon() } - } - ) - } - } - } - } - val mainContent = @Composable { contentPadding: PaddingValues -> - MainContent( - windowInsets = WindowInsets.systemBars, - onBackPressed = onBackPressed, - content = { content(it + contentPadding) } - ) - } - - Background(modifier) { - ScaffoldLayout( - role = ScaffoldRole.Tv, - navigation = navigation, - mainContent = mainContent - ) - } -} - -@Composable -private fun TvNavigation( - modifier: Modifier = Modifier, - content: @Composable ColumnScope.() -> Unit -) { - val spacing = LocalSpacing.current - Column( - verticalArrangement = Arrangement.spacedBy( - spacing.medium, - Alignment.CenterVertically - ), - modifier = modifier - .fillMaxHeight() - .padding(spacing.medium) - ) { - content() - } -} \ No newline at end of file diff --git a/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/RemoteDirectionController.kt b/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/RemoteDirectionController.kt index 2c56e4d2..2abce590 100644 --- a/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/RemoteDirectionController.kt +++ b/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/RemoteDirectionController.kt @@ -23,7 +23,7 @@ import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowLeft import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight import androidx.compose.material.icons.rounded.KeyboardArrowDown import androidx.compose.material.icons.rounded.KeyboardArrowUp -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/VirtualNumberKeyboard.kt b/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/VirtualNumberKeyboard.kt index 06636d14..71c464c6 100644 --- a/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/VirtualNumberKeyboard.kt +++ b/smartphone/src/main/java/com/m3u/smartphone/ui/sheet/VirtualNumberKeyboard.kt @@ -15,7 +15,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBackIosNew import androidx.compose.material.icons.rounded.Delete import androidx.compose.material3.ripple -import com.m3u.material.components.Icon +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton diff --git a/tv/.gitignore b/tv/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/tv/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/tv/build.gradle.kts b/tv/build.gradle.kts new file mode 100644 index 00000000..3364b0a9 --- /dev/null +++ b/tv/build.gradle.kts @@ -0,0 +1,104 @@ +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter + +plugins { + alias(libs.plugins.com.android.application) + alias(libs.plugins.org.jetbrains.kotlin.android) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.com.google.dagger.hilt.android) + alias(libs.plugins.com.google.devtools.ksp) + alias(libs.plugins.androidx.baselineprofile) + id("kotlin-parcelize") +} +android { + namespace = "com.m3u.tv" + compileSdk = 35 + defaultConfig { + applicationId = "com.m3u.tv" + minSdk = 26 + targetSdk = 33 + versionCode = 1 + versionName = "1.0.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + isMinifyEnabled = true + isShrinkResources = true + signingConfig = signingConfigs.getByName("debug") + } + all { + isCrunchPngs = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + aaptOptions.cruncherEnabled = false + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + kotlinOptions { + jvmTarget = "17" + } + buildFeatures { + compose = true + buildConfig = true + } + packaging { + resources.excludes += "META-INF/**" + } +} + +hilt { + enableAggregatingTask = true +} + +baselineProfile { + dexLayoutOptimization = true + saveInSrc = true +} + +dependencies { + implementation(project(":core")) + implementation(project(":ui")) + implementation(project(":feature:foryou")) + implementation(project(":feature:favorite")) + implementation(project(":feature:setting")) + implementation(project(":feature:playlist")) + implementation(project(":feature:channel")) + implementation(project(":feature:playlist-configuration")) + implementation(project(":feature:crash")) +// implementation(libs.androidx.profileinstaller) +// "baselineProfile"(project(":baselineprofile")) + + // tv + api(libs.androidx.tv.material) + + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.startup.runtime) + + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.runtime.compose) + implementation(libs.androidx.lifecycle.process) + + implementation(libs.androidx.core.splashscreen) + + implementation(libs.google.dagger.hilt) + ksp(libs.google.dagger.hilt.compiler) + implementation(libs.androidx.hilt.navigation.compose) + + implementation(libs.androidx.work.runtime.ktx) + ksp(libs.androidx.hilt.compiler) + implementation(libs.androidx.hilt.work) + + implementation(libs.androidx.glance.appwidget) + implementation(libs.androidx.glance.material3) + + debugImplementation(libs.squareup.leakcanary) +} diff --git a/tv/proguard-rules.pro b/tv/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/tv/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/tv/src/main/AndroidManifest.xml b/tv/src/main/AndroidManifest.xml new file mode 100644 index 00000000..4dcd5e90 --- /dev/null +++ b/tv/src/main/AndroidManifest.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tv/src/main/java/com/m3u/tv/MainActivity.kt b/tv/src/main/java/com/m3u/tv/MainActivity.kt new file mode 100644 index 00000000..e13ad525 --- /dev/null +++ b/tv/src/main/java/com/m3u/tv/MainActivity.kt @@ -0,0 +1,45 @@ +package com.m3u.tv + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.tv.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.tooling.preview.Preview +import androidx.tv.material3.Surface +import com.m3u.tv.ui.theme.M3UTheme + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + M3UTheme { + Surface( + modifier = Modifier.fillMaxSize(), + shape = RectangleShape + ) { + Greeting("Android") + } + } + } + } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Text( + text = "Hello $name!", + modifier = modifier + ) +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + M3UTheme { + Greeting("Android") + } +} \ No newline at end of file diff --git a/tv/src/main/java/com/m3u/tv/ui/theme/Color.kt b/tv/src/main/java/com/m3u/tv/ui/theme/Color.kt new file mode 100644 index 00000000..1d69f267 --- /dev/null +++ b/tv/src/main/java/com/m3u/tv/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.m3u.tv.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/tv/src/main/java/com/m3u/tv/ui/theme/Theme.kt b/tv/src/main/java/com/m3u/tv/ui/theme/Theme.kt new file mode 100644 index 00000000..871f9dc2 --- /dev/null +++ b/tv/src/main/java/com/m3u/tv/ui/theme/Theme.kt @@ -0,0 +1,34 @@ +package com.m3u.tv.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.runtime.Composable +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.MaterialTheme +import androidx.tv.material3.darkColorScheme +import androidx.tv.material3.lightColorScheme + +@OptIn(ExperimentalTvMaterial3Api::class) +@Composable +fun M3UTheme( + isInDarkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { + val colorScheme = if (isInDarkTheme) { + darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 + ) + } else { + lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + ) + } + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/tv/src/main/java/com/m3u/tv/ui/theme/Type.kt b/tv/src/main/java/com/m3u/tv/ui/theme/Type.kt new file mode 100644 index 00000000..487419a0 --- /dev/null +++ b/tv/src/main/java/com/m3u/tv/ui/theme/Type.kt @@ -0,0 +1,36 @@ +package com.m3u.tv.ui.theme + +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp +import androidx.tv.material3.ExperimentalTvMaterial3Api +import androidx.tv.material3.Typography + +// Set of Material typography styles to start with +@OptIn(ExperimentalTvMaterial3Api::class) +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/tv/src/main/res/mipmap-hdpi/ic_launcher.webp b/tv/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..c209e78ecd372343283f4157dcfd918ec5165bb3 GIT binary patch literal 1404 zcmV-?1%vuhNk&F=1pok7MM6+kP&il$0000G0000-002h-06|PpNX!5L00Dqw+t%{r zzW2vH!KF=w&cMnnN@{whkTw+#mAh0SV?YL=)3MimFYCWp#fpdtz~8$hD5VPuQgtcN zXl<@<#Cme5f5yr2h%@8TWh?)bSK`O z^Z@d={gn7J{iyxL_y_%J|L>ep{dUxUP8a{byupH&!UNR*OutO~0{*T4q5R6@ApLF! z5{w?Z150gC7#>(VHFJZ-^6O@PYp{t!jH(_Z*nzTK4 zkc{fLE4Q3|mA2`CWQ3{8;gxGizgM!zccbdQoOLZc8hThi-IhN90RFT|zlxh3Ty&VG z?Fe{#9RrRnxzsu|Lg2ddugg7k%>0JeD+{XZ7>Z~{=|M+sh1MF7~ zz>To~`~LVQe1nNoR-gEzkpe{Ak^7{{ZBk2i_<+`Bq<^GB!RYG+z)h;Y3+<{zlMUYd zrd*W4w&jZ0%kBuDZ1EW&KLpyR7r2=}fF2%0VwHM4pUs}ZI2egi#DRMYZPek*^H9YK zay4Iy3WXFG(F14xYsoDA|KXgGc5%2DhmQ1gFCkrgHBm!lXG8I5h*uf{rn48Z!_@ z4Bk6TJAB2CKYqPjiX&mWoW>OPFGd$wqroa($ne7EUK;#3VYkXaew%Kh^3OrMhtjYN?XEoY`tRPQsAkH-DSL^QqyN0>^ zmC>{#F14jz4GeW{pJoRpLFa_*GI{?T93^rX7SPQgT@LbLqpNA}<@2wH;q493)G=1Y z#-sCiRNX~qf3KgiFzB3I>4Z%AfS(3$`-aMIBU+6?gbgDb!)L~A)je+;fR0jWLL-Fu z4)P{c7{B4Hp91&%??2$v9iRSFnuckHUm}or9seH6 z>%NbT+5*@L5(I9j@06@(!{ZI?U0=pKn8uwIg&L{JV14+8s2hnvbRrU|hZCd}IJu7*;;ECgO%8_*W Kmw_-CKmY()leWbG literal 0 HcmV?d00001 diff --git a/tv/src/main/res/mipmap-mdpi/ic_launcher.webp b/tv/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..4f0f1d64e58ba64d180ce43ee13bf9a17835fbca GIT binary patch literal 982 zcmV;{11bDcNk&G_0{{S5MM6+kP&il$0000G0000l001ul06|PpNU8t;00Dqo+t#w^ z^1csucXz7-Qrhzl9HuHB%l>&>1tG2^vb*E&k^T3$FG1eQZ51g$uv4V+kI`0<^1Z@N zk?Jjh$olyC%l>)Xq;7!>{iBj&BjJ`P&$fsCfpve_epJOBkTF?nu-B7D!hO=2ZR}

C%4 zc_9eOXvPbC4kzU8YowIA8cW~Uv|eB&yYwAObSwL2vY~UYI7NXPvf3b+c^?wcs~_t{ ze_m66-0)^{JdOMKPwjpQ@Sna!*?$wTZ~su*tNv7o!gXT!GRgivP}ec?5>l1!7<(rT zds|8x(qGc673zrvYIz;J23FG{9nHMnAuP}NpAED^laz3mAN1sy+NXK)!6v1FxQ;lh zOBLA>$~P3r4b*NcqR;y6pwyhZ3_PiDb|%n1gGjl3ZU}ujInlP{eks-#oA6>rh&g+!f`hv#_%JrgYPu z(U^&XLW^QX7F9Z*SRPpQl{B%x)_AMp^}_v~?j7 zapvHMKxSf*Mtyx8I}-<*UGn3)oHd(nn=)BZ`d$lDBwq_GL($_TPaS{UeevT(AJ`p0 z9%+hQb6z)U9qjbuXjg|dExCLjpS8$VKQ55VsIC%@{N5t{NsW)=hNGI`J=x97_kbz@ E0Of=7!To6o6Gy zRq6Ap5(_{XLdXcL-MzlN`ugSdZY_`jXhcENAu)N_0?GhF))9R;E`!bo9p?g?SRgw_ zEXHhFG$0{qYOqhdX<(wE4N@es3VIo$%il%6xP9gjiBri+2pI6aY4 zJbgh-Ud|V%3O!IcHKQx1FQH(_*TK;1>FQWbt^$K1zNn^cczkBs=QHCYZ8b&l!UV{K z{L0$KCf_&KR^}&2Fe|L&?1I7~pBENnCtCuH3sjcx6$c zwqkNkru);ie``q+_QI;IYLD9OV0ZxkuyBz|5<$1BH|vtey$> z5oto4=l-R-Aaq`Dk0}o9N0VrkqW_#;!u{!bJLDq%0092{Ghe=F;(kn} z+sQ@1=UlX30+2nWjkL$B^b!H2^QYO@iFc0{(-~yXj2TWz?VG{v`Jg zg}WyYnwGgn>{HFaG7E~pt=)sOO}*yd(UU-D(E&x{xKEl6OcU?pl)K%#U$dn1mDF19 zSw@l8G!GNFB3c3VVK0?uyqN&utT-D5%NM4g-3@Sii9tSXKtwce~uF zS&Jn746EW^wV~8zdQ1XC28~kXu8+Yo9p!<8h&(Q({J*4DBglPdpe4M_mD8AguZFn~ ztiuO~{6Bx?SfO~_ZV(GIboeR9~hAym{{fV|VM=77MxDrbW6`ujX z<3HF(>Zr;#*uCvC*bpoSr~C$h?_%nXps@A)=l_;({Fo#6Y1+Zv`!T5HB+)#^-Ud_; zBwftPN=d8Vx)*O1Mj+0oO=mZ+NVH*ptNDC-&zZ7Hwho6UQ#l-yNvc0Cm+2$$6YUk2D2t#vdZX-u3>-Be1u9gtTBiMB^xwWQ_rgvGpZ6(C@e23c!^K=>ai-Rqu zhqT`ZQof;9Bu!AD(i^PCbYV%yha9zuoKMp`U^z;3!+&d@Hud&_iy!O-$b9ZLcSRh? z)R|826w}TU!J#X6P%@Zh=La$I6zXa#h!B;{qfug}O%z@K{EZECu6zl)7CiNi%xti0 zB{OKfAj83~iJvmpTU|&q1^?^cIMn2RQ?jeSB95l}{DrEPTW{_gmU_pqTc)h@4T>~& zluq3)GM=xa(#^VU5}@FNqpc$?#SbVsX!~RH*5p0p@w z;~v{QMX0^bFT1!cXGM8K9FP+=9~-d~#TK#ZE{4umGT=;dfvWi?rYj;^l_Zxywze`W z^Cr{55U@*BalS}K%Czii_80e0#0#Zkhlij4-~I@}`-JFJ7$5{>LnoJSs??J8kWVl6|8A}RCGAu9^rAsfCE=2}tHwl93t0C?#+jMpvr7O3`2=tr{Hg$=HlnjVG^ewm|Js0J*kfPa6*GhtB>`fN!m#9J(sU!?(OSfzY*zS(FJ<-Vb zfAIg+`U)YaXv#sY(c--|X zEB+TVyZ%Ie4L$gi#Fc++`h6%vzsS$pjz9aLt+ZL(g;n$Dzy5=m=_TV(3H8^C{r0xd zp#a%}ht55dOq?yhwYPrtp-m1xXp;4X;)NhxxUpgP%XTLmO zcjaFva^}dP3$&sfFTIR_jC=2pHh9kpI@2(6V*GQo7Ws)`j)hd+tr@P~gR*2gO@+1? zG<`_tB+LJuF|SZ9tIec;h%}}6WClT`L>HSW?E{Hp1h^+mlbf_$9zA>!ug>NALJsO{ mU%z=YwVD?}XMya)Bp;vlyE5&E_6!fzx9pwrdz474!~g(M6R?N? literal 0 HcmV?d00001 diff --git a/tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/tv/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000000000000000000000000000000000000..28d4b77f9f036a47549d47db79c16788749dca10 GIT binary patch literal 2884 zcmV-K3%m4ENk&FI3jhFDMM6+kP&il$0000G0001w0055w06|PpNY()W00EFA*|uso z=UmW3;Ri7@GcyiBW{ey$jes55b5S`|ZVZ{(x$xch{z?D+^{yErVgleVwa9qvGt40r z42;MG=7<0QySlzE=Ig6%01!FBK^$Fsxe@Hfe6aCy?Wh2r0~}@_lQAF90oTUi0FhEr z#(*;kTC(r!tQk6;gxj4h%FdHAt(^M3YvYj(!tOeN)+Hvj6+< zzyJRG?^lZfWuR#t!tUKP&(?%3v&Zd$R2YN>lB(Lq`OInY48%4%yTv2 zYe1{G`3)(PDEio5Y@-I5tUf`c%%OCJMtSW56g3iEg%3`$7XSJJHyA z<|7&N)5Xrlgv~%BO24eFd;Hd;uiK%D`EdK|quUeRZDqbh9l)%j%J#0lfrZumvA<_w zu&=AVvdChf6}eqh(bUz`(`Ue*p01{fBAcTgKyDYLs_I+YyJEk+rM@avU~>fB$n)HS zM7pfJydu`i%gfS<{PF94kZDv$t>06sAkheDzu40NJ$5CMW%n^Lls?8^p^QGWURbKu3ZduZQZ((s2? zzE`}<{;Zt7<$C|9R8A~DJ~@%x>TfP zF>TX8)@v|t)q4GjRt<}5s6hLHwRel7>V@&r-O|Av(yh;Q1A{E>Ir>p+%dHD|=l+lT zpr(Dg&>#Nu=!)6bCLr-ZS%|;h)Ij$+e@r8_{qO19QvDe=&1tmpY*0lcA^Cc-#{9fQ z<~$*<&P$Q<_jy#<$40PMofM7aQ}C=jphI`4kLg}Z7CIN#26D{-4v-_CA-LiE@(%{y!BzsU%gG`Q?sjLUf%qFSl0y)2#ae*+EI>s|i`d^V$Dn)qmzqRq6VJRY|{4ujsIU%#bnqU6MR&-1I_43=|5(6Jr;Jvert) zE?S|Tmn}Tv<-??sxV5@9t}3D=>YZ0JrQe$CO~|EY=Lj9RM&4svQHPQL6%pV5fPFiH zfXDx;l@~et{*{U*#c#Dvzu)|znDO7$#CRx)Z&yp-}SrD{&|(MQtfUz~n35@RLfUy=aqrhCX0M}J_r5QsK~NmRCR|Nm&L z41UdsLjWxSUlL41r^0K&nCCK>fdR-!MYjFg(z9_mF^C|#ZQw?`)f6uVzF^`bRnVY& zo}@M06J&_+>w9@jpaO4snmU;0t-(zYW1qVBHtuD!d?%?AtN7Plp><-1Y8Rqb20ZaP zTCgn*-Sri4Q8Xn>=gNaWQ57%!D35UkA@ksOlPB*Dvw}t02ENAqw|kFhn%ZyyW%+t{ zNdM!uqEM^;2}f+tECHbwLmH*!nZVrb$-az%t50Y2pg(HqhvY-^-lb}>^6l{$jOI6} zo_kBzj%8aX|6H5M0Y<)7pzz_wLkIpRm!;PzY)9+24wk2&TT{w--phDGDCOz{cN_ca zpnm7`$oDy=HX%0i-`769*0M6(e5j-?(?24%)<)&46y0e&6@HCDZAm9W6Ib#Y#BF6- z=30crHGg+RRTe%VBC>T00OV6F+gQDAK38Ne3N9bm|62tPccBJi)5{B z4zc^Db72XiBd}v$CF|yU{Z=M|DZ%-(XarYNclODlb1Kz1_EKLy(NSLCN`eUl(rBCL zT*jx@wNvze0|TSqgE(QArOZU)_?qH(sj#TwzElLs9q)(0u!_P|R%Cy_0JFQxgGV>1 zz4?_uq<8_gM0`c*Hh|;UMz~vrg1gQXp{ufg`hM_qU;U>+zmvc5blCLSq@PrEBSGR# z&8=2Z4uXN`F3p73ueD1l{s{k$WipAvSh5W7ABe?4)t;r@V?y`bNB5FvBuE|0VRTb< zM1Hn^?DSsJY+sX@T5xW=#>T9VEV|?<(=6|ge$X6Sb05!LFdjDcoq*gM(Zq=t;_)Le&jyt(&9jzR73noru`a# zN*<`KwGa^gZU3-)MSLF0aFag#f0<>E(bYTeHmtdbns#|I)-$)mJ`q9ctQ8g0=ET?| zdO}eZ*b_p>ygRTtR^5Ggdam=Zb5wmd{}np+Jn1d_=M`~P=M67jj})fH4ztb5yQqQW z^C|C&^LHAK-u+ooIK)yM)QM?t;|<{P;;{`p=BclzAN#JzL4jCwXkQB1Dy{=^KR`=~ zTrr)y7eiYBzSNs_DvO=4A6#EgGS-zY%Vi)N*Yb`U;6o}KR}dq{r9pT5wqZ@3NOE8- z9-(}D|Nc5732CSYQbL)!gPQ#RbD8BhK3dl{sUuPvei0tkvnJBxDEAYTesU8H$)g(Plra{VH(v3u^CO1~(+ zU0O7#)jaS4{NcwA+LuSm&VBcX2#Im3xg)W}ySNw%->orn1taZ&+d)}8gJTqA!u|5P z{yv?zol_3|(1(%M(EVU=cp?L`{Pi|ixk{U)*guFML3P!OSlz;zGA#T+E@8@cgQ_mv1o7RSU=Zo_82F?&&2r;WE z@wk}JHYEZ9nYUc(Vv~iTCa3u8e4q(yq<29VoNbKk|`mq%I6u)My=gPIDuUb&lzf4`MEA9^g8u z)vp8|$$HE9m_BTV?lOosIGa4jud=jIbw)O2eCMfyw2*S8?hjWw^nqws$O*M$3I1)x zR0PWFb3$ySOcGTe1dz%N0l;RPc`x%05FtT^f^j{Yo!9>IaV6aUZ*?W>} zs4%E?srLW`CJh0GCIK@hTkrW7A15Iu%N&?Q^$0+!{Tv&|t^Y@u%!L zglTg&?Q5q#ijZ;&HBQ?FNPp;k3J5!&{^+SGq?AX~SiOM9jJMRpyP?RCr@z38AQyy&WRMaC;n4una$~nJKSp?q|s8F00c9?Q! zY_ovvjTFm+DeQM^LXJ#v0}6HRt3R1%5PT*}W!k8BEM;Jrj8dIceFo2fhzTqaB3KKk zGlCLI)gU25(#u6ch6GeB1k@eHq7l{EHXv0n6xE#ws#ri}08kkCf8hUt{|Ejb`2YW* zvg}0nSSX1m=76s?sZhRY$K=3dpJ+y*eDULGnL2}4>4nvW^7_<~wIM_5fjvwt4h1|g z)g0Z6ZFq9j<~9~b8((~TN{Z?ZQfw|is&Xp~AC61sj;xItKyCHdI|tCMC_LbXF>~vR z=w6V3^H=W4CbAgR4#xw}ETTwu2guW~=Crl@SMXv85jQ=%y!s^?m4PI0My7MWICO;- z175jm%&PcPWh8QdOU(#8bp4!N7ET-+)N}N2zk2)8ch|4Q&lPFNQgT-thu053`r*h3 z_8dI@G;`zn;lH$zX3RzIk`E8~`J=BBdR}qD%n@vVG1834)!pS1Y?zVkJGtsa(sB~y zNfMYKsOJb%5J(0ivK8d+l2D2y&5X!cg3BG!AJ}910|_${nF}sC1QF^nLIhzXk-Y#x z0)&1iK!O;Og0Ky!;`b~v%b$`S4E&fB)1NB4v@8wr( z&+NX4e^&o)ecb=)dd~C!{(1e6t?&9j{l8%U*k4)?`(L3;Qjw z#w7FS+U(94MaJKS!J9O8^$)36_J8;thW#2$y9i{bB{?M{QS_inZIJ!jwqAbfXYVd$ zQ5fC$6Nc9hFi8m^;oI-%C#BS|c8vy+@{jx6hFcf^_;2VRgkoN(0h!_VSGmgNPRsxI z8$rTo0LaYq-H5i&gtj81=&xU?H-Y2==G@uQV7E`@+2E9XQW@{&j`?EOktk|Ho{HU>ZqDzvgjwBmdex z&uZNd2C1h{{}2k6Ys9$*nFP3;K%u!MhW`uZy7Sn`1M1zs@Es&;z*Z>Gsh@-3Fe6pE zQD2@cqF((NrRevgvLsvM_8;;iNyJ5nyPyy?e!kvKjGj`6diRFBEe49Oa7wwkJFV7Z z$YT&DWloYu-H?3<0BKn9L&JYDT-SK~*6c5pi18P26$JESKRYj{T7Zk6KiRJcbvOO*{P56Q6s8msbeI3>|j>K9}Q9UBeq*inXKemCm`-<5|-$ZyN4u$(3 z&HcvqehFD%5Yrmykg-^d`=BSa8(i=>ZoC77^mWY{evp(km@aHqhUECBz76YiR+VYK zY_avFC~V3$=`6C4JhfHAQ@DZtUOwH`L;oYX6zK0-uI^?hS$ALfq}A7evR;ohJHij} zHSZdW?EKv9U1s4oD*<(0oQ*;MaQ6@cvGL zuHCPgm_NhVsgp^sfr*ia^Db}swo1?O(_Q2)y+S$CBm+g=9wCOUPbz(x)_GbaKa@A7 zuI&!ynLiZRT#V%_y_-D`0Z5lT*auoe{(U5NylTzFSJW()W-#F6*&A`LNO1bV#Y;QJ zSbLBnp|B^dtK|KIWC|No>JjWBWE@n7O)x{&^E(WMeMvp57#qA8m* zeTow*U@_86B#Fm*rxyYu5PRWaWHx8y> z*qmHEp(AMDl0v)ij(AY8fnH=~ZwwjVAbu*m5;xPfidh@ov6d8g zfJsi&!QyK53Es%sC39ts;54V68koALD4b|%tNHW0bIkZAJKa=W&FomJSEDT>W1xIX z1x%Z>AvNIsSPLcn3RTcHXb@KB?cuM)=x6fcIx>&(GxqZ8w3p#jJ(GVgc*`c0HG}dv zIop&Qim!K1NFwic%07KcjWgHBPUkq7f~lj;TPqVGTiT#cUeim>;nY`>h@a*S{qQex zQ`z62WK|Mj)Y{tfF{;T4P;c8$Q|KU?Joh zIkA^z%X7z|r>4aTh@|StTi!-r1D!g=zb#3d#{{&K3CqE$Iz-UH<%37c zRfkO`&uM%#AD3PHv`g5t0e^O%nVL0d{Xlx^EjEC3#skF@`zl-7PF^0oxW)1!C!JxR zWvuAHH?)61FKA1QeT*_sY7;_Id#!GmV4n`MO{~sv}VLSK` zXRw=Y=Clz*00B(5y^K;gCZMAzjT5+c3IC=)l(9VIDdatpxj3y89WwI|bH&$!ZEvp` zPR!T@#!(|KfI-w?!&+7$N3F6>tD{YO4Qg$d_`nNEdfVCha9vaPn0jI0`)`@*72hq! zpU5ND^P*RoEkbD5o#az(-g=Y)L>HH>Oc%}$ zT3Rs_ih0;4+Lv4Y;@Iv(;fUbQ=i-G(#>vghec~*j(I#r|5mqFiJBpzi&hzEcD{u$< zRsm0BVYn=pT;0>R(itW|*D&;O%bOc7et9ACaH#J>z3A1A~6fdP>pmbM%xzm4>|;c_?B+%sl;Qs2{t!60$^u zH1t@9^6>;?!FuusnISi$f5CL&;z?EqJN$FBuWDA#D5`cy_UvCFIVvf{c?4N0teh;d zET$7aVbj08KTQS!x?Nd1Is8q8qFzs}a=!@nJ;7FSfCY^T@D-gpw`w<6e#X3+;O}1h z$%I!M)0bg|EKUA04Qjn@+x{Rj8vt6Wn!R|3A92z}^$KfF5(#CWr4y#~re1CN4i4w0 z#GsypBR{xA3Er7sgAi(|}1-W?s~n$7?K|9WL8kpVfw-;#b9 z+mn;=ep!162U5R>_t}fOt~tE?s#m( zO-S$7>Ay6*hHdZ)7_oU915WYYCIX;hFI-U2EWYX!pllONr@Q--2o~`!isi6vTPLJ4@(|o=%NHYjo0_S&q*UQIROw@*N-By@PaQ&;YxFZ0aR zX&}LeOEz);#m~Hwm^VAY8DK}b$F4bo{jMN?d!lxKPhNklzr^Cd`0f4oJr^z=I|l`* zm8AHm*fPV`0=lF3Pnnp}&J0N1X@}-D94YvmUabFrLGSnTz7Mu^21F#O5tN#CuY9Vh zUZBH=ez%h*wkf0hBtXJh1SN3d+IF{gzT7lp)j}n?03lt;XSQRAh7qd&v;RwTYDuQ# zbI2*r<>?x-G0@hM{;%{VBD7nLKt~D`T~-HAt5;h%i0_=Ifs=yHma5dhJ+QMG?Ux(a z|E?1CMy1!~oA`FP!k~iG=t&5#>bVdz=peT8HMB6Y)#7PpETtNryT^+Rv3vpJaF^zP z{H}0-LyV9Fu21ID%wO9f1IKlFr1p4c{o-?03vyB-tr5duk^&L$;m_|f$vs`^Sl{j2 z95}oY{LlY+=ZS%J+tZoXCd0*sSU7w^gjovXn+g7uyra5{cU49@yHf#Z^Jl-$9cIfo z+AJuxH$VLb=#+uBbVmUjnx zxb1pZ@-O9=AIk4@S)m6fJ2?{HrNYwwnL3a45muuNjr;6$O`bGEM0T4A2_S$t=86*- zcO+0mywg*j + tv + \ No newline at end of file diff --git a/tv/src/main/res/values/themes.xml b/tv/src/main/res/values/themes.xml new file mode 100644 index 00000000..298dd025 --- /dev/null +++ b/tv/src/main/res/values/themes.xml @@ -0,0 +1,4 @@ + + +