build: compose 1.7 and kotlin 2.0

This commit is contained in:
oxy
2024-05-26 21:18:04 +08:00
parent 4cb7a440b7
commit 6f74e04c1d
22 changed files with 297 additions and 236 deletions

1
.gitignore vendored
View File

@ -19,3 +19,4 @@ local.properties
jks.txt
*.jks
/sample/
/.kotlin

View File

@ -154,6 +154,4 @@ dependencies {
implementation(libs.androidx.hilt.work)
debugImplementation(libs.squareup.leakcanary)
implementation(libs.androidx.compose.material3.adaptive)
}

View File

@ -63,7 +63,7 @@ internal fun RemoteControlSheet(
if (visible) {
ModalBottomSheet(
sheetState = sheetState,
windowInsets = WindowInsets(0),
// windowInsets = WindowInsets(0),
onDismissRequest = {
if (!searchingOrConnecting) onDismissRequest()
},

View File

@ -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,

View File

@ -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)

View File

@ -347,7 +347,7 @@ class PlaylistViewModel @Inject constructor(
)
@OptIn(FlowPreview::class)
private val categories = flatmapCombined(playlistUrl, query) { playlistUrl, query ->
private val categories: Flow<List<String>> = flatmapCombined(playlistUrl, query) { playlistUrl, query ->
playlistRepository.observeCategoriesByPlaylistUrlIgnoreHidden(playlistUrl, query)
}
.let { flow ->
@ -360,8 +360,7 @@ class PlaylistViewModel @Inject constructor(
internal val channels: StateFlow<List<Channel>> = combine(
playlistUrl,
categories,
query,
sort
query, sort
) { playlistUrl, categories, query, sort ->
ChannelParameters(
playlistUrl = playlistUrl,

View File

@ -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(

View File

@ -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)
}

View File

@ -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<SettingDestination>()
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<Rect> {
return when (hingePolicy) {
HingePolicy.AvoidSeparating -> posture.separatingVerticalHingeBounds
HingePolicy.AvoidOccluding -> posture.occludingVerticalHingeBounds
HingePolicy.AlwaysAvoid -> posture.allVerticalHingeBounds
else -> emptyList()
}
}

View File

@ -71,7 +71,7 @@ internal fun CanvasBottomSheet(
) {
ModalBottomSheet(
sheetState = sheetState,
windowInsets = WindowInsets(0),
// windowInsets = WindowInsets(0),
onDismissRequest = onDismissRequest,
) {
Column(

View File

@ -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,

View File

@ -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,

View File

@ -80,13 +80,7 @@ class StreamViewModel @Inject constructor(
internal val stream: StateFlow<Stream?> = playerManager.stream
internal val playlist: StateFlow<Playlist?> = playerManager.playlist
internal val isSeriesPlaylist: StateFlow<Boolean> = playlist
.map { it?.isSeries ?: false }
.stateIn(
scope = viewModelScope,
initialValue = false,
started = SharingStarted.WhileSubscribed(5_000L)
)
internal val isSeriesPlaylist: Flow<Boolean> = playlist.map { it?.isSeries ?: false }
internal val isProgrammeSupported: Flow<Boolean> = playlist.map {
it ?: return@map false
@ -98,26 +92,14 @@ class StreamViewModel @Inject constructor(
}
}
internal val tracks: StateFlow<Map<Int, List<Format>>> = playerManager
.tracks
internal val tracks: Flow<Map<Int, List<Format>>> = playerManager.tracks
.map { all ->
all
.mapValues { (_, formats) -> formats }
.toMap()
}
.stateIn(
scope = viewModelScope,
initialValue = emptyMap(),
started = SharingStarted.WhileSubscribed(5_000L)
)
internal val currentTracks: StateFlow<Map<@C.TrackType Int, Format?>> = playerManager
.currentTracks
.stateIn(
scope = viewModelScope,
initialValue = emptyMap(),
started = SharingStarted.WhileSubscribed(5_000L)
)
internal val currentTracks: Flow<Map<@C.TrackType Int, Format?>> = 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<PagingData<Stream>> = 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<PagingData<Stream>> = playlist.flatMapLatest { playlist ->
playlist ?: return@flatMapLatest flowOf(PagingData.empty())
Pager(PagingConfig(10)) {
streamRepository.pagingAllByPlaylistUrl(

View File

@ -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()

View File

@ -68,7 +68,7 @@ internal fun FormatsBottomSheet(
sheetState = state,
onDismissRequest = onDismiss,
modifier = modifier,
windowInsets = WindowInsets(0)
// windowInsets = WindowInsets(0)
) {
LaunchedEffect(Unit) {
maskState.sleep()

View File

@ -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" }

View File

@ -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<RecomposeHighlighterModifier>() {
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
)
}
}

View File

@ -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
)
) {

View File

@ -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()
)

View File

@ -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)

View File

@ -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<NavHostController> { 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
}

View File

@ -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,