mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-05-17 19:35:58 +08:00
build: compose 1.7 and kotlin 2.0
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@ -19,3 +19,4 @@ local.properties
|
||||
jks.txt
|
||||
*.jks
|
||||
/sample/
|
||||
/.kotlin
|
||||
|
@ -154,6 +154,4 @@ dependencies {
|
||||
implementation(libs.androidx.hilt.work)
|
||||
|
||||
debugImplementation(libs.squareup.leakcanary)
|
||||
|
||||
implementation(libs.androidx.compose.material3.adaptive)
|
||||
}
|
||||
|
@ -63,7 +63,7 @@ internal fun RemoteControlSheet(
|
||||
if (visible) {
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
windowInsets = WindowInsets(0),
|
||||
// windowInsets = WindowInsets(0),
|
||||
onDismissRequest = {
|
||||
if (!searchingOrConnecting) onDismissRequest()
|
||||
},
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
@ -71,7 +71,7 @@ internal fun CanvasBottomSheet(
|
||||
) {
|
||||
ModalBottomSheet(
|
||||
sheetState = sheetState,
|
||||
windowInsets = WindowInsets(0),
|
||||
// windowInsets = WindowInsets(0),
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Column(
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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()
|
||||
|
@ -68,7 +68,7 @@ internal fun FormatsBottomSheet(
|
||||
sheetState = state,
|
||||
onDismissRequest = onDismiss,
|
||||
modifier = modifier,
|
||||
windowInsets = WindowInsets(0)
|
||||
// windowInsets = WindowInsets(0)
|
||||
) {
|
||||
LaunchedEffect(Unit) {
|
||||
maskState.sleep()
|
||||
|
@ -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" }
|
||||
|
139
material/src/main/java/com/m3u/material/RecomposeHighlighter.kt
Normal file
139
material/src/main/java/com/m3u/material/RecomposeHighlighter.kt
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
@ -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
|
||||
)
|
||||
) {
|
||||
|
@ -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()
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user