From 6f74e04c1dafa5363996e6d5e0e10dfb6b05d0f2 Mon Sep 17 00:00:00 2001 From: oxy Date: Sun, 26 May 2024 21:18:04 +0800 Subject: [PATCH] build: compose 1.7 and kotlin 2.0 --- .gitignore | 1 + androidApp/build.gradle.kts | 2 - .../androidApp/ui/sheet/RemoteControlSheet.kt | 2 +- .../ui/sheet/VirtualNumberKeyboard.kt | 6 +- features/playlist/build.gradle.kts | 2 +- .../features/playlist/PlaylistViewModel.kt | 5 +- .../playlist/components/PlaylistTabRow.kt | 4 +- features/setting/build.gradle.kts | 2 + .../com/m3u/features/setting/SettingScreen.kt | 242 ++++++------------ .../setting/components/CanvasBottomSheet.kt | 2 +- .../com/m3u/features/stream/StreamMask.kt | 8 +- .../com/m3u/features/stream/StreamScreen.kt | 18 +- .../m3u/features/stream/StreamViewModel.kt | 28 +- .../components/DlnaDevicesBottomSheet.kt | 2 +- .../stream/components/FormatsBottomSheet.kt | 2 +- gradle/libs.versions.toml | 18 +- .../com/m3u/material/RecomposeHighlighter.kt | 139 ++++++++++ .../m3u/material/components/BottomSheet.kt | 5 +- .../m3u/material/components/Preferences.kt | 4 +- .../m3u/material/components/ThemeSelection.kt | 4 +- ui/src/main/java/com/m3u/ui/Destination.kt | 8 +- ui/src/main/java/com/m3u/ui/Pager.kt | 29 ++- 22 files changed, 297 insertions(+), 236 deletions(-) create mode 100644 material/src/main/java/com/m3u/material/RecomposeHighlighter.kt diff --git a/.gitignore b/.gitignore index edb4d7dc..1475c0ab 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ local.properties jks.txt *.jks /sample/ +/.kotlin diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index 20f7b253..63327d69 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -154,6 +154,4 @@ dependencies { implementation(libs.androidx.hilt.work) debugImplementation(libs.squareup.leakcanary) - - implementation(libs.androidx.compose.material3.adaptive) } diff --git a/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/RemoteControlSheet.kt b/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/RemoteControlSheet.kt index 705caaac..a0bb1cd0 100644 --- a/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/RemoteControlSheet.kt +++ b/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/RemoteControlSheet.kt @@ -63,7 +63,7 @@ internal fun RemoteControlSheet( if (visible) { ModalBottomSheet( sheetState = sheetState, - windowInsets = WindowInsets(0), +// windowInsets = WindowInsets(0), onDismissRequest = { if (!searchingOrConnecting) onDismissRequest() }, diff --git a/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/VirtualNumberKeyboard.kt b/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/VirtualNumberKeyboard.kt index 00500a86..b84969fd 100644 --- a/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/VirtualNumberKeyboard.kt +++ b/androidApp/src/main/java/com/m3u/androidApp/ui/sheet/VirtualNumberKeyboard.kt @@ -14,7 +14,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowBackIosNew import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import com.m3u.material.components.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -111,7 +111,7 @@ internal fun VirtualNumberKeyboard( ) } }, - indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + indication = ripple(color = MaterialTheme.colorScheme.primary), interactionSource = remember { MutableInteractionSource() } ), horizontalAlignment = Alignment.CenterHorizontally, @@ -139,7 +139,7 @@ internal fun VirtualNumberKeyboard( onCode("") } }, - indication = rememberRipple(color = MaterialTheme.colorScheme.primary), + indication = ripple(color = MaterialTheme.colorScheme.primary), interactionSource = remember { MutableInteractionSource() } ), horizontalAlignment = Alignment.CenterHorizontally, diff --git a/features/playlist/build.gradle.kts b/features/playlist/build.gradle.kts index 89a5a7da..05813f2c 100644 --- a/features/playlist/build.gradle.kts +++ b/features/playlist/build.gradle.kts @@ -51,7 +51,7 @@ dependencies { implementation(libs.androidx.core.ktx) // for m2 BackdropScaffold only - implementation("androidx.compose.material:material:${libs.versions.androidx.compose}") + implementation("androidx.compose.material:material") implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.runtime.compose) diff --git a/features/playlist/src/main/java/com/m3u/features/playlist/PlaylistViewModel.kt b/features/playlist/src/main/java/com/m3u/features/playlist/PlaylistViewModel.kt index 91b45d72..cbfc0910 100644 --- a/features/playlist/src/main/java/com/m3u/features/playlist/PlaylistViewModel.kt +++ b/features/playlist/src/main/java/com/m3u/features/playlist/PlaylistViewModel.kt @@ -347,7 +347,7 @@ class PlaylistViewModel @Inject constructor( ) @OptIn(FlowPreview::class) - private val categories = flatmapCombined(playlistUrl, query) { playlistUrl, query -> + private val categories: Flow> = flatmapCombined(playlistUrl, query) { playlistUrl, query -> playlistRepository.observeCategoriesByPlaylistUrlIgnoreHidden(playlistUrl, query) } .let { flow -> @@ -360,8 +360,7 @@ class PlaylistViewModel @Inject constructor( internal val channels: StateFlow> = combine( playlistUrl, categories, - query, - sort + query, sort ) { playlistUrl, categories, query, sort -> ChannelParameters( playlistUrl = playlistUrl, diff --git a/features/playlist/src/main/java/com/m3u/features/playlist/components/PlaylistTabRow.kt b/features/playlist/src/main/java/com/m3u/features/playlist/components/PlaylistTabRow.kt index d3c91c4c..e8ca2cb8 100644 --- a/features/playlist/src/main/java/com/m3u/features/playlist/components/PlaylistTabRow.kt +++ b/features/playlist/src/main/java/com/m3u/features/playlist/components/PlaylistTabRow.kt @@ -25,13 +25,13 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Menu import androidx.compose.material.icons.rounded.PushPin import androidx.compose.material.icons.rounded.VisibilityOff -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect @@ -213,7 +213,7 @@ private fun PlaylistTabRowItem( } ) ) { - val indication = if (hasOtherFocused) null else rememberRipple() + val indication = if (hasOtherFocused) null else ripple() val shape = if (isExpanded) RectangleShape else RoundedCornerShape(topStart = 12.dp, topEnd = 12.dp) Card( diff --git a/features/setting/build.gradle.kts b/features/setting/build.gradle.kts index ceec2db5..6c1a0328 100644 --- a/features/setting/build.gradle.kts +++ b/features/setting/build.gradle.kts @@ -60,4 +60,6 @@ dependencies { implementation(libs.androidx.hilt.work) implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material3.adaptive.navigation) + implementation(libs.androidx.compose.material3.adaptive.layout) } diff --git a/features/setting/src/main/java/com/m3u/features/setting/SettingScreen.kt b/features/setting/src/main/java/com/m3u/features/setting/SettingScreen.kt index eba54a42..fc87a9a9 100644 --- a/features/setting/src/main/java/com/m3u/features/setting/SettingScreen.kt +++ b/features/setting/src/main/java/com/m3u/features/setting/SettingScreen.kt @@ -4,42 +4,28 @@ import android.net.Uri import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.Crossfade import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ChangeCircle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.adaptive.AnimatedPane -import androidx.compose.material3.adaptive.HingePolicy -import androidx.compose.material3.adaptive.ListDetailPaneScaffold -import androidx.compose.material3.adaptive.ListDetailPaneScaffoldRole -import androidx.compose.material3.adaptive.PaneScaffoldDirective -import androidx.compose.material3.adaptive.Posture -import androidx.compose.material3.adaptive.ThreePaneScaffoldDestinationItem -import androidx.compose.material3.adaptive.WindowAdaptiveInfo -import androidx.compose.material3.adaptive.allVerticalHingeBounds -import androidx.compose.material3.adaptive.calculateListDetailPaneScaffoldState -import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo -import androidx.compose.material3.adaptive.occludingVerticalHingeBounds -import androidx.compose.material3.adaptive.separatingVerticalHingeBounds +import androidx.compose.material3.adaptive.layout.AnimatedPane +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold +import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole +import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Rect import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -201,34 +187,35 @@ private fun SettingScreen( val colorArgb = preferences.argb - var fragment: SettingDestination by remember { mutableStateOf(SettingDestination.Default) } - - EventHandler(Events.settingDestination) { - fragment = it - } - val visiblePageInfos = LocalVisiblePageInfos.current val pageIndex = remember { Destination.Root.entries.indexOf(Destination.Root.Setting) } val isPageInfoVisible = remember(pageIndex, visiblePageInfos) { visiblePageInfos.find { it.index == pageIndex } != null } + val scaffoldNavigator = rememberListDetailPaneScaffoldNavigator() + val destination = scaffoldNavigator.currentDestination?.content ?: SettingDestination.Default + + EventHandler(Events.settingDestination) { + scaffoldNavigator.navigateTo(ListDetailPaneScaffoldRole.Detail, it) + } + if (isPageInfoVisible) { - LifecycleResumeEffect(fragment, defaultTitle, playlistTitle, appearanceTitle, fragment) { - Metadata.title = when (fragment) { + LifecycleResumeEffect(destination, defaultTitle, playlistTitle, appearanceTitle) { + Metadata.title = when (destination) { SettingDestination.Default -> defaultTitle SettingDestination.Playlists -> playlistTitle SettingDestination.Appearance -> appearanceTitle }.title() Metadata.color = Color.Unspecified Metadata.contentColor = Color.Unspecified - if (fragment != SettingDestination.Default) { + if (destination != SettingDestination.Default) { Metadata.fob = Fob( rootDestination = Destination.Root.Setting, icon = Icons.Rounded.ChangeCircle, iconTextId = string.feat_setting_back_home ) { - fragment = SettingDestination.Default + scaffoldNavigator.navigateBack() } } Metadata.actions = emptyList() @@ -238,84 +225,78 @@ private fun SettingScreen( } } - val currentPaneScaffoldRole by remember { - derivedStateOf { - when (fragment) { - SettingDestination.Default -> ListDetailPaneScaffoldRole.List - else -> ListDetailPaneScaffoldRole.Detail - } - } - } - val scaffoldState = calculateListDetailPaneScaffoldState( - currentDestination = ThreePaneScaffoldDestinationItem(currentPaneScaffoldRole, null), - scaffoldDirective = calculateStandardPaneScaffoldDirective(currentWindowAdaptiveInfo()) - ) - ListDetailPaneScaffold( - scaffoldState = scaffoldState, - // we handle the window insets in app scaffold - windowInsets = WindowInsets(0), + directive = scaffoldNavigator.scaffoldDirective, + value = scaffoldNavigator.scaffoldValue, listPane = { - PreferencesFragment( - fragment = fragment, - contentPadding = contentPadding, - versionName = versionName, - versionCode = versionCode, - navigateToPlaylistManagement = { - fragment = SettingDestination.Playlists - }, - navigateToThemeSelector = { - fragment = SettingDestination.Appearance - }, - cacheSpace = cacheSpace, - onClearCache = onClearCache, - modifier = Modifier.fillMaxSize() - ) + AnimatedPane { + Crossfade(destination) { destination -> + PreferencesFragment( + fragment = destination, + contentPadding = contentPadding, + versionName = versionName, + versionCode = versionCode, + navigateToPlaylistManagement = { + scaffoldNavigator.navigateTo( + pane = ListDetailPaneScaffoldRole.Detail, + content = SettingDestination.Playlists + ) + }, + navigateToThemeSelector = { + scaffoldNavigator.navigateTo( + pane = ListDetailPaneScaffoldRole.Detail, + content = SettingDestination.Appearance + ) + }, + cacheSpace = cacheSpace, + onClearCache = onClearCache, + modifier = Modifier.fillMaxSize() + ) + } + } }, detailPane = { - if (fragment != SettingDestination.Default) { - AnimatedPane(Modifier) { - when (fragment) { - SettingDestination.Playlists -> { - SubscriptionsFragment( - titleState = titleState, - urlState = urlState, - uriState = uriState, - selectedState = selectedState, - basicUrlState = basicUrlState, - usernameState = usernameState, - passwordState = passwordState, - epgState = epgState, - localStorageState = localStorageState, - forTvState = forTvState, - backingUpOrRestoring = backingUpOrRestoring, - hiddenStreams = hiddenStreams, - hiddenCategoriesWithPlaylists = hiddenCategoriesWithPlaylists, - onUnhideStream = onUnhideStream, - onUnhidePlaylistCategory = onUnhidePlaylistCategory, - onClipboard = onClipboard, - onSubscribe = onSubscribe, - backup = backup, - restore = restore, - epgs = epgs, - onDeleteEpgPlaylist = onDeleteEpgPlaylist, - contentPadding = contentPadding, - modifier = Modifier.fillMaxSize() - ) - } - - SettingDestination.Appearance -> { - AppearanceFragment( - colorSchemes = colorSchemes, - colorArgb = colorArgb, - openColorCanvas = openColorCanvas, - restoreSchemes = restoreSchemes, - contentPadding = contentPadding - ) - } - - else -> {} + AnimatedPane(Modifier.fillMaxSize()) { + when (destination) { + SettingDestination.Playlists -> { + SubscriptionsFragment( + titleState = titleState, + urlState = urlState, + uriState = uriState, + selectedState = selectedState, + basicUrlState = basicUrlState, + usernameState = usernameState, + passwordState = passwordState, + epgState = epgState, + localStorageState = localStorageState, + forTvState = forTvState, + backingUpOrRestoring = backingUpOrRestoring, + hiddenStreams = hiddenStreams, + hiddenCategoriesWithPlaylists = hiddenCategoriesWithPlaylists, + onUnhideStream = onUnhideStream, + onUnhidePlaylistCategory = onUnhidePlaylistCategory, + onClipboard = onClipboard, + onSubscribe = onSubscribe, + backup = backup, + restore = restore, + epgs = epgs, + onDeleteEpgPlaylist = onDeleteEpgPlaylist, + contentPadding = contentPadding, + modifier = Modifier.fillMaxSize() + ) } + + SettingDestination.Appearance -> { + AppearanceFragment( + colorSchemes = colorSchemes, + colorArgb = colorArgb, + openColorCanvas = openColorCanvas, + restoreSchemes = restoreSchemes, + contentPadding = contentPadding + ) + } + + else -> {} } } }, @@ -327,60 +308,7 @@ private fun SettingScreen( ) .testTag("feature:setting") ) - BackHandler(fragment != SettingDestination.Default) { - fragment = SettingDestination.Default + BackHandler(scaffoldNavigator.canNavigateBack()) { + scaffoldNavigator.navigateBack() } } - -private fun calculateStandardPaneScaffoldDirective( - windowAdaptiveInfo: WindowAdaptiveInfo, - verticalHingePolicy: HingePolicy = HingePolicy.AvoidSeparating -): PaneScaffoldDirective { - val maxHorizontalPartitions: Int - val verticalSpacerSize: Dp - when (windowAdaptiveInfo.windowSizeClass.widthSizeClass) { - WindowWidthSizeClass.Compact -> { - maxHorizontalPartitions = 1 - verticalSpacerSize = 0.dp - } - - WindowWidthSizeClass.Medium -> { - maxHorizontalPartitions = 1 - verticalSpacerSize = 0.dp - } - - else -> { - maxHorizontalPartitions = 2 - verticalSpacerSize = 24.dp - } - } - val maxVerticalPartitions: Int - val horizontalSpacerSize: Dp - - if (windowAdaptiveInfo.windowPosture.isTabletop) { - maxVerticalPartitions = 2 - horizontalSpacerSize = 24.dp - } else { - maxVerticalPartitions = 1 - horizontalSpacerSize = 0.dp - } - - return PaneScaffoldDirective( - // keep no paddings - PaddingValues(), - maxHorizontalPartitions, - verticalSpacerSize, - maxVerticalPartitions, - horizontalSpacerSize, - getExcludedVerticalBounds(windowAdaptiveInfo.windowPosture, verticalHingePolicy) - ) -} - -private fun getExcludedVerticalBounds(posture: Posture, hingePolicy: HingePolicy): List { - return when (hingePolicy) { - HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds - HingePolicy.AvoidOccluding -> posture.occludingVerticalHingeBounds - HingePolicy.AlwaysAvoid -> posture.allVerticalHingeBounds - else -> emptyList() - } -} \ No newline at end of file diff --git a/features/setting/src/main/java/com/m3u/features/setting/components/CanvasBottomSheet.kt b/features/setting/src/main/java/com/m3u/features/setting/components/CanvasBottomSheet.kt index 7ed1e718..e348b24f 100644 --- a/features/setting/src/main/java/com/m3u/features/setting/components/CanvasBottomSheet.kt +++ b/features/setting/src/main/java/com/m3u/features/setting/components/CanvasBottomSheet.kt @@ -71,7 +71,7 @@ internal fun CanvasBottomSheet( ) { ModalBottomSheet( sheetState = sheetState, - windowInsets = WindowInsets(0), +// windowInsets = WindowInsets(0), onDismissRequest = onDismissRequest, ) { Column( diff --git a/features/stream/src/main/java/com/m3u/features/stream/StreamMask.kt b/features/stream/src/main/java/com/m3u/features/stream/StreamMask.kt index fd7e009d..b52452f5 100644 --- a/features/stream/src/main/java/com/m3u/features/stream/StreamMask.kt +++ b/features/stream/src/main/java/com/m3u/features/stream/StreamMask.kt @@ -78,6 +78,7 @@ import com.m3u.material.components.mask.MaskState import com.m3u.material.ktx.isTelevision import com.m3u.material.ktx.thenIf import com.m3u.material.model.LocalSpacing +import com.m3u.material.recomposeHighlighter import com.m3u.ui.FontFamilies import com.m3u.ui.Image import com.m3u.ui.helper.LocalHelper @@ -103,7 +104,7 @@ internal fun StreamMask( favourite: Boolean, isSeriesPlaylist: Boolean, isPanelExpanded: Boolean, - formatsIsNotEmpty: Boolean, + hasTrack: Boolean, onFavourite: () -> Unit, onBackPressed: () -> Unit, openDlnaDevices: () -> Unit, @@ -258,7 +259,8 @@ internal fun StreamMask( } else volumeBeforeMuted ) }, - contentDescription = defaultBrightnessOrVolumeContentDescription + contentDescription = defaultBrightnessOrVolumeContentDescription, + modifier = Modifier.recomposeHighlighter() ) if (!isSeriesPlaylist) { MaskButton( @@ -271,7 +273,7 @@ internal fun StreamMask( ) } - if (formatsIsNotEmpty) { + if (hasTrack) { MaskButton( state = maskState, icon = Icons.Rounded.HighQuality, diff --git a/features/stream/src/main/java/com/m3u/features/stream/StreamScreen.kt b/features/stream/src/main/java/com/m3u/features/stream/StreamScreen.kt index f34dbecc..da2d40b9 100644 --- a/features/stream/src/main/java/com/m3u/features/stream/StreamScreen.kt +++ b/features/stream/src/main/java/com/m3u/features/stream/StreamScreen.kt @@ -84,16 +84,16 @@ fun StreamRoute( val isDevicesVisible by viewModel.isDevicesVisible.collectAsStateWithLifecycle() val searching by viewModel.searching.collectAsStateWithLifecycle() - val formats by viewModel.tracks.collectAsStateWithLifecycle() - val selectedFormats by viewModel.currentTracks.collectAsStateWithLifecycle() + val tracks by viewModel.tracks.collectAsStateWithLifecycle(emptyMap()) + val selectedFormats by viewModel.currentTracks.collectAsStateWithLifecycle(emptyMap()) val volume by viewModel.volume.collectAsStateWithLifecycle() - val isSeriesPlaylist by viewModel.isSeriesPlaylist.collectAsStateWithLifecycle() + val isSeriesPlaylist by viewModel.isSeriesPlaylist.collectAsStateWithLifecycle(false) val isProgrammeSupported by viewModel.isProgrammeSupported.collectAsStateWithLifecycle( initialValue = false ) - val neighboring = viewModel.neighboring.collectAsLazyPagingItems() + val channels = viewModel.channels.collectAsLazyPagingItems() val programmes = viewModel.programmes.collectAsLazyPagingItems() val programmeRange by viewModel.programmeRange.collectAsStateWithLifecycle() @@ -190,7 +190,7 @@ fun StreamRoute( isPanelExpanded = isPanelExpanded, isChannelsSupported = !isSeriesPlaylist, isProgrammeSupported = isProgrammeSupported, - channels = neighboring, + channels = channels, programmes = programmes, programmeRange = programmeRange ) @@ -219,7 +219,7 @@ fun StreamRoute( playerState = playerState, playlist = playlist, stream = stream, - formatsIsNotEmpty = formats.isNotEmpty(), + hasTrack = tracks.isNotEmpty(), isPanelExpanded = isPanelExpanded, volume = volume, onVolume = viewModel::onVolume, @@ -256,7 +256,7 @@ fun StreamRoute( FormatsBottomSheet( visible = choosing, - formats = formats, + formats = tracks, selectedFormats = selectedFormats, maskState = maskState, onDismiss = { choosing = false }, @@ -276,7 +276,7 @@ private fun StreamPlayer( playlist: Playlist?, stream: Stream?, isSeriesPlaylist: Boolean, - formatsIsNotEmpty: Boolean, + hasTrack: Boolean, isPanelExpanded: Boolean, volume: Float, brightness: Float, @@ -339,7 +339,7 @@ private fun StreamPlayer( maskState = maskState, favourite = favourite, isSeriesPlaylist = isSeriesPlaylist, - formatsIsNotEmpty = formatsIsNotEmpty, + hasTrack = hasTrack, isPanelExpanded = isPanelExpanded, onFavourite = onFavourite, onBackPressed = onBackPressed, diff --git a/features/stream/src/main/java/com/m3u/features/stream/StreamViewModel.kt b/features/stream/src/main/java/com/m3u/features/stream/StreamViewModel.kt index de4977f6..37bc3099 100644 --- a/features/stream/src/main/java/com/m3u/features/stream/StreamViewModel.kt +++ b/features/stream/src/main/java/com/m3u/features/stream/StreamViewModel.kt @@ -80,13 +80,7 @@ class StreamViewModel @Inject constructor( internal val stream: StateFlow = playerManager.stream internal val playlist: StateFlow = playerManager.playlist - internal val isSeriesPlaylist: StateFlow = playlist - .map { it?.isSeries ?: false } - .stateIn( - scope = viewModelScope, - initialValue = false, - started = SharingStarted.WhileSubscribed(5_000L) - ) + internal val isSeriesPlaylist: Flow = playlist.map { it?.isSeries ?: false } internal val isProgrammeSupported: Flow = playlist.map { it ?: return@map false @@ -98,26 +92,14 @@ class StreamViewModel @Inject constructor( } } - internal val tracks: StateFlow>> = playerManager - .tracks + internal val tracks: Flow>> = playerManager.tracks .map { all -> all .mapValues { (_, formats) -> formats } .toMap() } - .stateIn( - scope = viewModelScope, - initialValue = emptyMap(), - started = SharingStarted.WhileSubscribed(5_000L) - ) - internal val currentTracks: StateFlow> = playerManager - .currentTracks - .stateIn( - scope = viewModelScope, - initialValue = emptyMap(), - started = SharingStarted.WhileSubscribed(5_000L) - ) + internal val currentTracks: Flow> = playerManager.currentTracks internal fun chooseTrack(type: @C.TrackType Int, format: Format) { val groups = playerManager.tracksGroups.value @@ -256,7 +238,9 @@ class StreamViewModel @Inject constructor( playerManager.pauseOrContinue(isContinued) } - internal val neighboring: Flow> = playlist.flatMapLatest { playlist -> + // the channels which is in the same category with the current channel + // or the episodes which is in the same series. + internal val channels: Flow> = playlist.flatMapLatest { playlist -> playlist ?: return@flatMapLatest flowOf(PagingData.empty()) Pager(PagingConfig(10)) { streamRepository.pagingAllByPlaylistUrl( diff --git a/features/stream/src/main/java/com/m3u/features/stream/components/DlnaDevicesBottomSheet.kt b/features/stream/src/main/java/com/m3u/features/stream/components/DlnaDevicesBottomSheet.kt index 5854833c..378beb56 100644 --- a/features/stream/src/main/java/com/m3u/features/stream/components/DlnaDevicesBottomSheet.kt +++ b/features/stream/src/main/java/com/m3u/features/stream/components/DlnaDevicesBottomSheet.kt @@ -63,7 +63,7 @@ internal fun DlnaDevicesBottomSheet( sheetState = state, onDismissRequest = onDismiss, modifier = modifier, - windowInsets = WindowInsets(0) +// windowInsets = WindowInsets(0) ) { LaunchedEffect(devices, state.isVisible) { if (state.isVisible) state.expand() diff --git a/features/stream/src/main/java/com/m3u/features/stream/components/FormatsBottomSheet.kt b/features/stream/src/main/java/com/m3u/features/stream/components/FormatsBottomSheet.kt index 580aeee2..d38ec580 100644 --- a/features/stream/src/main/java/com/m3u/features/stream/components/FormatsBottomSheet.kt +++ b/features/stream/src/main/java/com/m3u/features/stream/components/FormatsBottomSheet.kt @@ -68,7 +68,7 @@ internal fun FormatsBottomSheet( sheetState = state, onDismissRequest = onDismiss, modifier = modifier, - windowInsets = WindowInsets(0) +// windowInsets = WindowInsets(0) ) { LaunchedEffect(Unit) { maskState.sleep() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 637c33b2..809ba0bf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,9 +3,9 @@ androidx-core = "1.13.1" androidx-core-splashscreen = "1.0.1" androidx-appcompat = "1.6.1" androidx-activity = "1.9.0" -androidx-lifecycle = "2.7.0" -androidx-compose-bom = "2024.05.00" -androidx-compose-material3-adaptive = "1.0.0-alpha06" +androidx-lifecycle = "2.8.0" +androidx-compose-bom = "2024.05.00-alpha02" +androidx-compose-material3-adaptive = "1.0.0-beta01" androidx-constraintlayout-compose = "1.0.1" androidx-navigation = "2.7.7" androidx-hilt = "1.2.0" @@ -18,7 +18,7 @@ androidx-startup = "1.1.1" androidx-paging-compose = "3.3.0" androidx-paging-runtime-ktx = "3.3.0" -google-accompanist = "0.34.0" +google-accompanist = "0.35.1-alpha" google-dagger = "2.50" haze = "0.7.1" @@ -36,10 +36,10 @@ slf4j-api = "2.0.11" squareup-retrofit2 = "2.11.0" squareup-leakcanary = "2.13" -kotlin = "2.0.0-RC2" +kotlin = "2.0.0" android-gradle-plugin = "8.2.2" dagger-plugin = "2.50" -ksp-plugin = "2.0.0-RC2-1.0.20" +ksp-plugin = "2.0.0-1.0.21" androidx-test-ext-junit = "1.1.5" espresso-core = "3.5.1" @@ -62,7 +62,7 @@ androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecyc androidx-lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } androidx-lifecycle-process = { group = "androidx.lifecycle", name = "lifecycle-process", version.ref = "androidx-lifecycle" } -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } +androidx-compose-bom = { group = "dev.chrisbanes.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } androidx-compose-foundation-layout = { group = "androidx.compose.foundation", name = "foundation-layout" } androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } @@ -73,7 +73,9 @@ androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "u androidx-compose-ui-util = { group = "androidx.compose.ui", name = "ui-util" } androidx-compose-material3-window-size-clazz = { group = "androidx.compose.material3", name = "material3-window-size-class" } -androidx-compose-material3-adaptive = { group = "androidx.compose.material3", name = "material3-adaptive", version.ref = "androidx-compose-material3-adaptive" } +androidx-compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive", version.ref = "androidx-compose-material3-adaptive" } +androidx-compose-material3-adaptive-layout = { group = "androidx.compose.material3.adaptive", name = "adaptive-layout", version.ref = "androidx-compose-material3-adaptive" } +androidx-compose-material3-adaptive-navigation = { group = "androidx.compose.material3.adaptive", name = "adaptive-navigation", version.ref = "androidx-compose-material3-adaptive" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "androidx-navigation" } androidx-constraintlayout-compose = { group = "androidx.constraintlayout", name = "constraintlayout-compose", version.ref = "androidx-constraintlayout-compose" } diff --git a/material/src/main/java/com/m3u/material/RecomposeHighlighter.kt b/material/src/main/java/com/m3u/material/RecomposeHighlighter.kt new file mode 100644 index 00000000..c6e023f1 --- /dev/null +++ b/material/src/main/java/com/m3u/material/RecomposeHighlighter.kt @@ -0,0 +1,139 @@ +package com.m3u.material + +import androidx.compose.runtime.Stable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.graphics.lerp +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.platform.InspectorInfo +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import java.util.Objects +import kotlin.math.min + +/** + * A [Modifier] that draws a border around elements that are recomposing. The border increases in + * size and interpolates from red to green as more recompositions occur before a timeout. + */ +@Stable +fun Modifier.recomposeHighlighter(): Modifier = this.then(RecomposeHighlighterElement()) + +private class RecomposeHighlighterElement : ModifierNodeElement() { + + override fun InspectorInfo.inspectableProperties() { + debugInspectorInfo { name = "recomposeHighlighter" } + } + + override fun create(): RecomposeHighlighterModifier = RecomposeHighlighterModifier() + + override fun update(node: RecomposeHighlighterModifier) { + node.incrementCompositions() + } + + // It's never equal, so that every recomposition triggers the update function. + override fun equals(other: Any?): Boolean = false + + override fun hashCode(): Int = Objects.hash(this) +} + +private class RecomposeHighlighterModifier : Modifier.Node(), DrawModifierNode { + + private var timerJob: Job? = null + + /** + * The total number of compositions that have occurred. + */ + private var totalCompositions: Long = 0 + set(value) { + if (field == value) return + restartTimer() + field = value + invalidateDraw() + } + + fun incrementCompositions() { + totalCompositions++ + } + + override fun onAttach() { + super.onAttach() + restartTimer() + } + + override val shouldAutoInvalidate: Boolean = false + + override fun onDetach() { + timerJob?.cancel() + } + + /** + * Start the timeout, and reset everytime there's a recomposition. + */ + private fun restartTimer() { + if (!isAttached) return + + timerJob?.cancel() + timerJob = coroutineScope.launch { + delay(3000) + totalCompositions = 0 + invalidateDraw() + } + } + + override fun ContentDrawScope.draw() { + // Draw actual content. + drawContent() + + // Below is to draw the highlight, if necessary. A lot of the logic is copied from Modifier.border + + val hasValidBorderParams = size.minDimension > 0f + if (!hasValidBorderParams || totalCompositions <= 0) { + return + } + + val (color, strokeWidthPx) = + when (totalCompositions) { + // We need at least one composition to draw, so draw the smallest border + // color in blue. + 1L -> Color.Blue to 1f + // 2 compositions is _probably_ okay. + 2L -> Color.Green to 2.dp.toPx() + // 3 or more compositions before timeout may indicate an issue. lerp the + // color from yellow to red, and continually increase the border size. + else -> { + lerp( + Color.Yellow.copy(alpha = 0.8f), + Color.Red.copy(alpha = 0.5f), + min(1f, (totalCompositions - 1).toFloat() / 100f) + ) to totalCompositions.toInt().dp.toPx() + } + } + + val halfStroke = strokeWidthPx / 2 + val topLeft = Offset(halfStroke, halfStroke) + val borderSize = Size(size.width - strokeWidthPx, size.height - strokeWidthPx) + + val fillArea = (strokeWidthPx * 2) > size.minDimension + val rectTopLeft = if (fillArea) Offset.Zero else topLeft + val size = if (fillArea) size else borderSize + val style = if (fillArea) Fill else Stroke(strokeWidthPx) + + drawRect( + brush = SolidColor(color), + topLeft = rectTopLeft, + size = size, + style = style + ) + } +} diff --git a/material/src/main/java/com/m3u/material/components/BottomSheet.kt b/material/src/main/java/com/m3u/material/components/BottomSheet.kt index c2be1f23..81210892 100644 --- a/material/src/main/java/com/m3u/material/components/BottomSheet.kt +++ b/material/src/main/java/com/m3u/material/components/BottomSheet.kt @@ -13,7 +13,7 @@ import androidx.compose.foundation.layout.navigationBarsIgnoringVisibility import androidx.compose.foundation.layout.padding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.ModalBottomSheetDefaults +import androidx.compose.material3.ModalBottomSheetProperties import androidx.compose.material3.SheetState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -41,8 +41,7 @@ fun BottomSheet( sheetState = sheetState, onDismissRequest = onDismissRequest, modifier = modifier, - windowInsets = WindowInsets(0), - properties = ModalBottomSheetDefaults.properties( + properties = ModalBottomSheetProperties( shouldDismissOnBackPress = shouldDismissOnBackPress ) ) { 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 dbfd4cf7..c6fd7915 100644 --- a/material/src/main/java/com/m3u/material/components/Preferences.kt +++ b/material/src/main/java/com/m3u/material/components/Preferences.kt @@ -6,7 +6,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.selection.toggleable -import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ripple import androidx.compose.material3.Checkbox import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults @@ -107,7 +107,7 @@ fun Preference( enabled = enabled, onClick = onClick, interactionSource = interactionSource, - indication = rememberRipple() + indication = ripple() ) .fillMaxWidth() ) 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 f5e90304..6d41523a 100644 --- a/material/src/main/java/com/m3u/material/components/ThemeSelection.kt +++ b/material/src/main/java/com/m3u/material/components/ThemeSelection.kt @@ -22,12 +22,12 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.LightMode -import androidx.compose.material.ripple.rememberRipple import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedCard import androidx.compose.material3.Text +import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember @@ -168,7 +168,7 @@ fun ThemeSelection( Box( modifier = Modifier.combinedClickable( interactionSource = interactionSource, - indication = rememberRipple(), + indication = ripple(), onClick = { if (selected) return@combinedClickable feedback.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) diff --git a/ui/src/main/java/com/m3u/ui/Destination.kt b/ui/src/main/java/com/m3u/ui/Destination.kt index 56cbda90..0ee4d938 100644 --- a/ui/src/main/java/com/m3u/ui/Destination.kt +++ b/ui/src/main/java/com/m3u/ui/Destination.kt @@ -1,5 +1,6 @@ package com.m3u.ui +import android.os.Parcelable import androidx.annotation.StringRes import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Collections @@ -13,6 +14,7 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.graphics.vector.ImageVector import androidx.navigation.NavHostController import com.m3u.i18n.R.string +import kotlinx.parcelize.Parcelize val LocalNavController = staticCompositionLocalOf { error("Please provide NavHostController") } @@ -50,13 +52,17 @@ sealed interface Destination { } @Immutable -sealed interface SettingDestination { +@Parcelize +sealed interface SettingDestination : Parcelable { @Immutable + @Parcelize data object Default : SettingDestination @Immutable + @Parcelize data object Playlists : SettingDestination @Immutable + @Parcelize data object Appearance : SettingDestination } diff --git a/ui/src/main/java/com/m3u/ui/Pager.kt b/ui/src/main/java/com/m3u/ui/Pager.kt index 3795b819..b07a6d80 100644 --- a/ui/src/main/java/com/m3u/ui/Pager.kt +++ b/ui/src/main/java/com/m3u/ui/Pager.kt @@ -1,7 +1,7 @@ package com.m3u.ui import androidx.compose.foundation.gestures.Orientation -import androidx.compose.foundation.gestures.snapping.SnapFlingBehavior +import androidx.compose.foundation.gestures.TargetedFlingBehavior import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PageInfo @@ -13,7 +13,6 @@ import androidx.compose.foundation.pager.VerticalPager import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection @@ -28,16 +27,17 @@ fun ExtendedHorizontalPager( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), pageSize: PageSize = PageSize.Fill, - beyondBoundsPageCount: Int = PagerDefaults.BeyondBoundsPageCount, + beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount, pageSpacing: Dp = 0.dp, verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, - flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state), + flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state), userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, key: ((index: Int) -> Any)? = null, - pageNestedScrollConnection: NestedScrollConnection = remember(state) { - PagerDefaults.pageNestedScrollConnection(state, Orientation.Horizontal) - }, + pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection( + state, + Orientation.Horizontal + ), pageContent: @Composable PagerScope.(page: Int) -> Unit ) { CompositionLocalProvider( @@ -48,7 +48,7 @@ fun ExtendedHorizontalPager( modifier = modifier, contentPadding = contentPadding, pageSize = pageSize, - beyondBoundsPageCount = beyondBoundsPageCount, + beyondViewportPageCount = beyondViewportPageCount, pageSpacing = pageSpacing, verticalAlignment = verticalAlignment, flingBehavior = flingBehavior, @@ -67,16 +67,17 @@ fun ExtendedVerticalPager( modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), pageSize: PageSize = PageSize.Fill, - beyondBoundsPageCount: Int = PagerDefaults.BeyondBoundsPageCount, + beyondViewportPageCount: Int = PagerDefaults.BeyondViewportPageCount, pageSpacing: Dp = 0.dp, horizontalAlignment: Alignment.Horizontal = Alignment.CenterHorizontally, - flingBehavior: SnapFlingBehavior = PagerDefaults.flingBehavior(state = state), + flingBehavior: TargetedFlingBehavior = PagerDefaults.flingBehavior(state = state), userScrollEnabled: Boolean = true, reverseLayout: Boolean = false, key: ((index: Int) -> Any)? = null, - pageNestedScrollConnection: NestedScrollConnection = remember(state) { - PagerDefaults.pageNestedScrollConnection(state, Orientation.Horizontal) - }, + pageNestedScrollConnection: NestedScrollConnection = PagerDefaults.pageNestedScrollConnection( + state, + Orientation.Vertical + ), pageContent: @Composable PagerScope.(page: Int) -> Unit ) { CompositionLocalProvider( @@ -87,7 +88,7 @@ fun ExtendedVerticalPager( modifier = modifier, contentPadding = contentPadding, pageSize = pageSize, - beyondBoundsPageCount = beyondBoundsPageCount, + beyondViewportPageCount = beyondViewportPageCount, pageSpacing = pageSpacing, horizontalAlignment = horizontalAlignment, flingBehavior = flingBehavior,