mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-05-17 19:35:58 +08:00
fix:
1. volume button is invisible. 2. play control is too big. 3. initial volume is incorrect. 4. channel gallery in player panel has been redesigned. 5. remove test code. 6. handle exception when read-write channel preference.
This commit is contained in:
@ -73,21 +73,21 @@ import androidx.compose.ui.unit.dp
|
||||
import androidx.media3.common.Player
|
||||
import com.m3u.business.channel.PlayerState
|
||||
import com.m3u.core.architecture.preferences.hiltPreferences
|
||||
import com.m3u.core.foundation.ui.thenIf
|
||||
import com.m3u.core.util.basic.isNotEmpty
|
||||
import com.m3u.data.database.model.AdjacentChannels
|
||||
import com.m3u.i18n.R.string
|
||||
import com.m3u.smartphone.ui.material.model.LocalSpacing
|
||||
import com.m3u.smartphone.ui.business.channel.components.MaskTextButton
|
||||
import com.m3u.smartphone.ui.business.channel.components.PlayerMask
|
||||
import com.m3u.smartphone.ui.common.helper.LocalHelper
|
||||
import com.m3u.smartphone.ui.material.components.FontFamilies
|
||||
import com.m3u.smartphone.ui.material.components.Image
|
||||
import com.m3u.smartphone.ui.common.helper.LocalHelper
|
||||
import com.m3u.smartphone.ui.material.components.mask.MaskButton
|
||||
import com.m3u.smartphone.ui.material.components.mask.MaskCircleButton
|
||||
import com.m3u.smartphone.ui.material.components.mask.MaskPanel
|
||||
import com.m3u.smartphone.ui.material.components.mask.MaskState
|
||||
import com.m3u.smartphone.ui.material.effects.currentBackStackEntry
|
||||
import com.m3u.core.foundation.ui.thenIf
|
||||
import com.m3u.smartphone.ui.material.model.LocalSpacing
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.absoluteValue
|
||||
@ -319,7 +319,7 @@ internal fun ChannelMask(
|
||||
preferences.alwaysShowReplay,
|
||||
playerState.playerError
|
||||
)
|
||||
Box(Modifier.size(48.dp)) {
|
||||
Box(Modifier.size(36.dp)) {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = !isPanelExpanded && adjacentChannels?.prevId != null,
|
||||
enter = fadeIn() + slideInHorizontally(initialOffsetX = { -it / 6 }),
|
||||
@ -334,7 +334,7 @@ internal fun ChannelMask(
|
||||
}
|
||||
}
|
||||
|
||||
Box(Modifier.size(64.dp)) {
|
||||
Box(Modifier.size(52.dp)) {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = !isPanelExpanded && centerRole != MaskCenterRole.Loading,
|
||||
enter = fadeIn(),
|
||||
@ -350,12 +350,12 @@ internal fun ChannelMask(
|
||||
)
|
||||
}
|
||||
}
|
||||
Box(Modifier.size(48.dp)) {
|
||||
Box(Modifier.size(36.dp)) {
|
||||
androidx.compose.animation.AnimatedVisibility(
|
||||
visible = !isPanelExpanded && adjacentChannels?.nextId != null,
|
||||
enter = fadeIn() + slideInHorizontally(initialOffsetX = { it / 6 }),
|
||||
exit = fadeOut() + slideOutHorizontally(targetOffsetX = { it / 6 }),
|
||||
modifier = Modifier.size(48.dp)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
MaskNavigateButton(
|
||||
state = maskState,
|
||||
@ -381,7 +381,10 @@ internal fun ChannelMask(
|
||||
.semantics(mergeDescendants = true) { }
|
||||
.weight(1f)
|
||||
) {
|
||||
if (!isPanelExpanded || !isPanelGestureSupported) {
|
||||
val alpha by animateFloatAsState(
|
||||
if (!isPanelExpanded || !isPanelGestureSupported) 1f else 0f
|
||||
)
|
||||
Column(Modifier.alpha(alpha)) {
|
||||
Text(
|
||||
text = playlistTitle.trim().uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
@ -434,26 +437,26 @@ internal fun ChannelMask(
|
||||
)
|
||||
}
|
||||
}
|
||||
val autoRotating by ChannelMaskUtils.IsAutoRotatingEnabled
|
||||
LaunchedEffect(autoRotating) {
|
||||
if (autoRotating) {
|
||||
helper.screenOrientation =
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
if (preferences.screenRotating && !autoRotating) {
|
||||
MaskButton(
|
||||
state = maskState,
|
||||
icon = Icons.Rounded.ScreenRotationAlt,
|
||||
onClick = {
|
||||
helper.screenOrientation = when (helper.screenOrientation) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
},
|
||||
contentDescription = stringResource(string.feat_channel_tooltip_screen_rotating)
|
||||
)
|
||||
val autoRotating by ChannelMaskUtils.IsAutoRotatingEnabled
|
||||
LaunchedEffect(autoRotating) {
|
||||
if (autoRotating) {
|
||||
helper.screenOrientation =
|
||||
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
if (preferences.screenRotating && !autoRotating) {
|
||||
MaskButton(
|
||||
state = maskState,
|
||||
icon = Icons.Rounded.ScreenRotationAlt,
|
||||
onClick = {
|
||||
helper.screenOrientation = when (helper.screenOrientation) {
|
||||
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE -> ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
else -> ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
||||
}
|
||||
},
|
||||
contentDescription = stringResource(string.feat_channel_tooltip_screen_rotating)
|
||||
)
|
||||
}
|
||||
},
|
||||
slider = {
|
||||
when {
|
||||
|
@ -226,8 +226,7 @@ fun ChannelRoute(
|
||||
ChannelPlayer(
|
||||
isSeriesPlaylist = isSeriesPlaylist,
|
||||
openDlnaDevices = {
|
||||
// viewModel.recordVideo()
|
||||
createRecordFileLauncher.launch("record_${Clock.System.now().toEpochMilliseconds()}.mp4")
|
||||
// createRecordFileLauncher.launch("record_${Clock.System.now().toEpochMilliseconds()}.mp4")
|
||||
viewModel.openDlnaDevices()
|
||||
pullPanelLayoutState.collapse()
|
||||
},
|
||||
|
@ -16,6 +16,9 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import com.m3u.smartphone.ui.material.components.mask.MaskState
|
||||
import com.m3u.smartphone.ui.material.components.FontFamilies
|
||||
|
||||
@ -58,12 +61,14 @@ fun MaskTextButton(
|
||||
state.wake()
|
||||
onClick()
|
||||
},
|
||||
enabled = enabled
|
||||
enabled = enabled,
|
||||
colors = IconButtonDefaults.iconButtonColors(
|
||||
contentColor = tint.takeOrElse { LocalContentColor.current }
|
||||
),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = tint
|
||||
contentDescription = contentDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ internal fun PlayerMask(
|
||||
body: @Composable RowScope.() -> Unit,
|
||||
footer: @Composable RowScope.() -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
control: @Composable RowScope.() -> Unit = {},
|
||||
slider: (@Composable () -> Unit)? = null
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
@ -78,6 +79,12 @@ internal fun PlayerMask(
|
||||
bottom = spacing.small
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.Bottom,
|
||||
content = control
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.medium),
|
||||
|
@ -33,7 +33,11 @@ import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.NotificationsActive
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
@ -58,26 +62,26 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import com.m3u.core.foundation.components.CircularProgressIndicator
|
||||
import com.m3u.core.foundation.ui.thenIf
|
||||
import com.m3u.core.util.collections.indexOf
|
||||
import com.m3u.data.database.model.Channel
|
||||
import com.m3u.data.database.model.Episode
|
||||
import com.m3u.data.database.model.Programme
|
||||
import com.m3u.data.database.model.ProgrammeRange
|
||||
import com.m3u.data.service.MediaCommand
|
||||
import com.m3u.smartphone.TimeUtils.formatEOrSh
|
||||
import com.m3u.smartphone.TimeUtils.toEOrSh
|
||||
import com.m3u.smartphone.ui.common.helper.LocalHelper
|
||||
import com.m3u.smartphone.ui.material.components.Background
|
||||
import com.m3u.core.foundation.components.CircularProgressIndicator
|
||||
import androidx.compose.material3.IconButton
|
||||
import com.m3u.smartphone.ui.material.components.FontFamilies
|
||||
import com.m3u.smartphone.ui.material.effects.BackStackEntry
|
||||
import com.m3u.smartphone.ui.material.effects.BackStackHandler
|
||||
import com.m3u.smartphone.ui.material.ktx.Edge
|
||||
import com.m3u.smartphone.ui.material.ktx.blurEdges
|
||||
import com.m3u.core.foundation.ui.thenIf
|
||||
import com.m3u.smartphone.ui.material.ktx.composableOf
|
||||
import com.m3u.smartphone.ui.material.model.LocalSpacing
|
||||
import com.m3u.smartphone.ui.material.shape.AbsoluteSmoothCornerShape
|
||||
import com.m3u.smartphone.ui.material.components.FontFamilies
|
||||
import com.m3u.smartphone.ui.common.helper.LocalHelper
|
||||
import com.m3u.smartphone.TimeUtils.formatEOrSh
|
||||
import com.m3u.smartphone.TimeUtils.toEOrSh
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
@ -364,8 +368,12 @@ private fun ChannelGallery(
|
||||
val isPlaying = channel.id == channelId
|
||||
ChannelGalleryItem(
|
||||
channel = channel,
|
||||
isPlaying = isPlaying
|
||||
isPlaying = isPlaying,
|
||||
isRoundedShape = !vertical
|
||||
)
|
||||
if (vertical) {
|
||||
HorizontalDivider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -388,9 +396,10 @@ private fun ChannelGallery(
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
verticalArrangement = Arrangement.spacedBy(spacing.medium),
|
||||
contentPadding = PaddingValues(spacing.medium),
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.blurEdges(MaterialTheme.colorScheme.surface, listOf(Edge.Top, Edge.Bottom))
|
||||
.then(modifier),
|
||||
content = content
|
||||
)
|
||||
}
|
||||
@ -413,32 +422,18 @@ private sealed class ChannelGalleryValue {
|
||||
private fun ChannelGalleryItem(
|
||||
channel: Channel,
|
||||
isPlaying: Boolean,
|
||||
isRoundedShape: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val spacing = LocalSpacing.current
|
||||
val helper = LocalHelper.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = if (!isPlaying)
|
||||
MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.medium)
|
||||
else MaterialTheme.colorScheme.onSurface,
|
||||
contentColor = if (!isPlaying) MaterialTheme.colorScheme.onSurface
|
||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.small)
|
||||
),
|
||||
shape = AbsoluteRoundedCornerShape(spacing.medium),
|
||||
elevation = CardDefaults.cardElevation(spacing.none),
|
||||
onClick = {
|
||||
if (isPlaying) return@Card
|
||||
coroutineScope.launch {
|
||||
helper.play(
|
||||
MediaCommand.Common(channel.id)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
) {
|
||||
val containerColor = if (!isPlaying) MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.medium)
|
||||
else MaterialTheme.colorScheme.onSurface
|
||||
val contentColor = if (!isPlaying) MaterialTheme.colorScheme.onSurface
|
||||
else MaterialTheme.colorScheme.surfaceColorAtElevation(spacing.small)
|
||||
val text = composableOf {
|
||||
Text(
|
||||
text = channel.title,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
@ -446,6 +441,39 @@ private fun ChannelGalleryItem(
|
||||
modifier = Modifier.padding(spacing.medium)
|
||||
)
|
||||
}
|
||||
val onClick = lambda@{
|
||||
if (isPlaying) return@lambda
|
||||
coroutineScope.launch {
|
||||
helper.play(
|
||||
MediaCommand.Common(channel.id)
|
||||
)
|
||||
}
|
||||
}
|
||||
if (isRoundedShape) {
|
||||
Card(
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor
|
||||
),
|
||||
shape = AbsoluteRoundedCornerShape(spacing.medium),
|
||||
elevation = CardDefaults.cardElevation(spacing.none),
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
) {
|
||||
text()
|
||||
}
|
||||
} else {
|
||||
ListItem(
|
||||
headlineContent = {
|
||||
text()
|
||||
},
|
||||
colors = ListItemDefaults.colors(
|
||||
containerColor = containerColor,
|
||||
headlineColor = contentColor
|
||||
),
|
||||
modifier = Modifier.clickable(onClick = onClick).then(modifier)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
@ -13,6 +13,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.BrokenImage
|
||||
import androidx.compose.material.icons.rounded.Star
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
@ -41,14 +42,14 @@ import coil.compose.SubcomposeAsyncImage
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Size
|
||||
import com.m3u.core.architecture.preferences.hiltPreferences
|
||||
import com.m3u.data.database.model.Programme
|
||||
import com.m3u.data.database.model.Channel
|
||||
import com.m3u.i18n.R.string
|
||||
import com.m3u.core.foundation.components.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import com.m3u.data.database.model.Channel
|
||||
import com.m3u.data.database.model.Programme
|
||||
import com.m3u.i18n.R.string
|
||||
import com.m3u.smartphone.TimeUtils.formatEOrSh
|
||||
import com.m3u.smartphone.ui.material.ktx.composableOf
|
||||
import com.m3u.smartphone.ui.material.model.LocalSpacing
|
||||
import com.m3u.smartphone.ui.material.shape.AbsoluteSmoothCornerShape
|
||||
import com.m3u.smartphone.TimeUtils.formatEOrSh
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.datetime.TimeZone
|
||||
@ -175,16 +176,14 @@ internal fun ChannelItem(
|
||||
fontWeight = FontWeight.Bold,
|
||||
)
|
||||
},
|
||||
leadingContent = if (!noPictureMode) {
|
||||
{
|
||||
AsyncImage(
|
||||
model = channel.cover,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(56.dp)
|
||||
)
|
||||
}
|
||||
} else null,
|
||||
leadingContent = composableOf(!noPictureMode) {
|
||||
AsyncImage(
|
||||
model = channel.cover,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(56.dp)
|
||||
)
|
||||
},
|
||||
supportingContent = {
|
||||
when {
|
||||
recently -> {
|
||||
@ -220,9 +219,7 @@ internal fun ChannelItem(
|
||||
}
|
||||
}
|
||||
},
|
||||
trailingContent = {
|
||||
star()
|
||||
},
|
||||
trailingContent = star,
|
||||
colors = ListItemDefaults.colors(Color.Transparent),
|
||||
modifier = Modifier
|
||||
.combinedClickable(
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.m3u.smartphone.ui.material.components
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||
@ -57,6 +58,7 @@ fun PullPanelLayout(
|
||||
BackHandler(state.value == PullPanelLayoutValue.EXPANDED) {
|
||||
state.collapse()
|
||||
}
|
||||
@SuppressLint("UnusedBoxWithConstraintsScope")
|
||||
BoxWithConstraints {
|
||||
var offset: Float by remember(state, constraints.maxHeight) {
|
||||
mutableFloatStateOf(
|
||||
|
@ -82,7 +82,11 @@ class ChannelViewModel @Inject constructor(
|
||||
var devices by mutableStateOf(emptyList<Device>())
|
||||
|
||||
private val _volume: MutableStateFlow<Float> by lazy {
|
||||
MutableStateFlow(audioManager.getStreamVolume(AudioManager.STREAM_MUSIC) / 100f)
|
||||
MutableStateFlow(
|
||||
with(audioManager) {
|
||||
getStreamVolume(AudioManager.STREAM_MUSIC) * 1f / getStreamMaxVolume(AudioManager.STREAM_MUSIC)
|
||||
}
|
||||
)
|
||||
}
|
||||
val volume = _volume.asStateFlow()
|
||||
|
||||
|
@ -14,19 +14,24 @@ internal class ChannelPreferenceProvider(
|
||||
private val cache = DiskLruCache.open(directory, appVersion, 2, 4 * 1024 * 1024) // 4mb
|
||||
|
||||
suspend operator fun get(channelUrl: String): ChannelPreference? = withContext(ioDispatcher) {
|
||||
val key = encodeKey(channelUrl)
|
||||
val snapshot = cache.get(key) ?: return@withContext null
|
||||
ChannelPreference(
|
||||
cwPosition = snapshot.getString(0).toLong(),
|
||||
mineType = snapshot.getString(1)
|
||||
)
|
||||
runCatching {
|
||||
val key = encodeKey(channelUrl)
|
||||
val snapshot = cache.get(key) ?: return@withContext null
|
||||
ChannelPreference(
|
||||
cwPosition = snapshot.getString(0).toLong(),
|
||||
mineType = snapshot.getString(1)
|
||||
)
|
||||
}
|
||||
.getOrNull()
|
||||
}
|
||||
suspend operator fun set(channelUrl: String, value: ChannelPreference) = withContext(ioDispatcher) {
|
||||
val key = encodeKey(channelUrl)
|
||||
val editor = cache.edit(key) ?: return@withContext
|
||||
editor.set(0, value.cwPosition.toString())
|
||||
editor.set(1, value.mineType)
|
||||
editor.commit()
|
||||
runCatching {
|
||||
val key = encodeKey(channelUrl)
|
||||
val editor = cache.edit(key) ?: return@withContext
|
||||
editor.set(0, value.cwPosition.toString())
|
||||
editor.set(1, value.mineType)
|
||||
editor.commit()
|
||||
}
|
||||
}
|
||||
|
||||
// [a-z0-9_-]{1,64}
|
||||
|
Reference in New Issue
Block a user