mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-05-17 19:35:58 +08:00
fix: coroutine-dispatcher.
This commit is contained in:
@ -9,8 +9,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.WorkManager
|
||||
import com.m3u.smartphone.ui.common.connect.RemoteControlSheetValue
|
||||
import com.m3u.core.architecture.Publisher
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.preferences.Preferences
|
||||
import com.m3u.data.api.TvApiDelegate
|
||||
import com.m3u.data.tv.model.RemoteDirection
|
||||
@ -21,7 +19,6 @@ import com.m3u.data.repository.programme.ProgrammeRepository
|
||||
import com.m3u.data.service.Messager
|
||||
import com.m3u.data.worker.SubscriptionWorker
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@ -29,7 +26,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
@ -47,7 +43,6 @@ class AppViewModel @Inject constructor(
|
||||
private val workManager: WorkManager,
|
||||
private val preferences: Preferences,
|
||||
private val publisher: Publisher,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
) : ViewModel() {
|
||||
init {
|
||||
refreshProgrammes()
|
||||
@ -72,7 +67,6 @@ class AppViewModel @Inject constructor(
|
||||
flowOf(ConnectionToTvValue.Idle())
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = ConnectionToTvValue.Idle(),
|
||||
|
@ -2,8 +2,6 @@ package com.m3u.smartphone.ui.business.channel
|
||||
|
||||
import android.content.pm.ActivityInfo
|
||||
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
@ -12,13 +10,10 @@ import androidx.compose.animation.core.animateIntAsState
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
@ -30,10 +25,10 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.rounded.VolumeOff
|
||||
@ -55,7 +50,6 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
@ -72,13 +66,16 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.LinkAnnotation
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withLink
|
||||
import androidx.compose.ui.unit.DpSize
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.media3.common.Player
|
||||
import com.m3u.business.channel.PlayerState
|
||||
import com.m3u.core.architecture.preferences.hiltPreferences
|
||||
@ -89,7 +86,6 @@ 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.business.channel.components.CwPositionRewinder
|
||||
import com.m3u.smartphone.ui.business.channel.components.MaskDimension
|
||||
import com.m3u.smartphone.ui.business.channel.components.MaskTextButton
|
||||
import com.m3u.smartphone.ui.business.channel.components.PlayerMask
|
||||
@ -123,9 +119,10 @@ fun ChannelMask(
|
||||
favourite: Boolean,
|
||||
isSeriesPlaylist: Boolean,
|
||||
isPanelExpanded: Boolean,
|
||||
useVertical: Boolean,
|
||||
hasTrack: Boolean,
|
||||
cwPosition: Long,
|
||||
onRewind: () -> Unit,
|
||||
onResetPlayback: () -> Unit,
|
||||
onSpeedUpdated: (Float) -> Unit,
|
||||
onSpeedStart: () -> Unit,
|
||||
onSpeedEnd: () -> Unit,
|
||||
@ -143,7 +140,6 @@ fun ChannelMask(
|
||||
val preferences = hiltPreferences()
|
||||
val helper = LocalHelper.current
|
||||
val spacing = LocalSpacing.current
|
||||
val configuration = LocalConfiguration.current
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
val onBackPressedDispatcher = checkNotNull(
|
||||
@ -222,8 +218,6 @@ fun ChannelMask(
|
||||
|
||||
var volumeBeforeMuted: Float by remember { mutableFloatStateOf(0.4f) }
|
||||
|
||||
val isPanelGestureSupported = configuration.screenWidthDp < configuration.screenHeightDp
|
||||
|
||||
var bufferedPosition: Long? by remember { mutableStateOf(null) }
|
||||
LaunchedEffect(bufferedPosition) {
|
||||
bufferedPosition?.let {
|
||||
@ -252,8 +246,10 @@ fun ChannelMask(
|
||||
Color.Black.copy(alpha = if (isPanelExpanded) 0f else 0.54f)
|
||||
)
|
||||
val playStateDisplayText = ChannelMaskUtils.playStateDisplayText(playerState.playState)
|
||||
val exceptionDisplayText = ChannelMaskUtils.playbackExceptionDisplayText(playerState.playerError)
|
||||
val exceptionDisplayText =
|
||||
ChannelMaskUtils.playbackExceptionDisplayText(playerState.playerError)
|
||||
|
||||
val cwPositionObj = cwPosition.takeIf { it != -1L }?.let { CwPosition(it) }
|
||||
PlayerMask(
|
||||
state = maskState,
|
||||
color = color,
|
||||
@ -305,7 +301,7 @@ fun ChannelMask(
|
||||
)
|
||||
}
|
||||
|
||||
if (!isPanelGestureSupported) {
|
||||
if (!useVertical) {
|
||||
MaskButton(
|
||||
state = maskState,
|
||||
icon = if (isPanelExpanded) Icons.Rounded.Archive
|
||||
@ -386,10 +382,15 @@ fun ChannelMask(
|
||||
}
|
||||
}
|
||||
},
|
||||
control = cwPositionObj?.let {
|
||||
composableOf<RowScope> {
|
||||
CwPositionSliderImpl(it.milliseconds, onResetPlayback = onResetPlayback)
|
||||
}
|
||||
},
|
||||
footer = composableOf<RowScope>(
|
||||
any {
|
||||
suggest { !isPanelExpanded }
|
||||
suggest { !isPanelGestureSupported }
|
||||
suggest { !useVertical }
|
||||
suggest { playStateDisplayText.isNotEmpty() }
|
||||
suggest { exceptionDisplayText.isNotEmpty() }
|
||||
suggestAll {
|
||||
@ -405,7 +406,7 @@ fun ChannelMask(
|
||||
.weight(1f)
|
||||
) {
|
||||
val alpha by animateFloatAsState(
|
||||
if (!isPanelExpanded || !isPanelGestureSupported) 1f else 0f
|
||||
if (!isPanelExpanded || !useVertical) 1f else 0f
|
||||
)
|
||||
Column(Modifier.alpha(alpha)) {
|
||||
Text(
|
||||
@ -476,56 +477,17 @@ fun ChannelMask(
|
||||
)
|
||||
}
|
||||
},
|
||||
slider = {
|
||||
val sliderRole: MaskSlideRole = when {
|
||||
cwPosition != -1L -> MaskSlideRole.CwPosition(cwPosition)
|
||||
isProgressEnabled && isStaticAndSeekable -> MaskSlideRole.Slide
|
||||
else -> MaskSlideRole.None
|
||||
}
|
||||
AnimatedContent(
|
||||
targetState = sliderRole,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) { role ->
|
||||
when (role) {
|
||||
is MaskSlideRole.CwPosition -> {
|
||||
CwPositionSliderImpl(
|
||||
position = role.milliseconds,
|
||||
onResetPlayback = onRewind,
|
||||
modifier = Modifier.animateEnterExit(
|
||||
enter = fadeIn() + scaleIn(initialScale = 0.85f),
|
||||
exit = fadeOut() + scaleOut(targetScale = 0.85f)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
MaskSlideRole.Slide -> {
|
||||
SliderImpl(
|
||||
contentDuration = contentDuration,
|
||||
contentPosition = contentPosition,
|
||||
bufferedPosition = bufferedPosition,
|
||||
onBufferedPositionChanged = {
|
||||
bufferedPosition = it
|
||||
maskState.wake()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
MaskSlideRole.None -> {}
|
||||
slider = composableOf(isProgressEnabled && isStaticAndSeekable) {
|
||||
SliderImpl(
|
||||
contentDuration = contentDuration,
|
||||
contentPosition = contentPosition,
|
||||
bufferedPosition = bufferedPosition,
|
||||
isPanelExpanded = isPanelExpanded,
|
||||
onBufferedPositionChanged = {
|
||||
bufferedPosition = it
|
||||
maskState.wake()
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = true,
|
||||
enter = slideInVertically(),
|
||||
exit = slideOutVertically(),
|
||||
modifier = Modifier.padding(top = 4.dp)
|
||||
) {
|
||||
|
||||
}
|
||||
when {
|
||||
isProgressEnabled && isStaticAndSeekable -> {
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
onDimensionChanged = onDimensionChanged,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
@ -539,6 +501,7 @@ private fun SliderImpl(
|
||||
onBufferedPositionChanged: (Long) -> Unit,
|
||||
contentPosition: Long,
|
||||
contentDuration: Long,
|
||||
isPanelExpanded: Boolean,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val spacing = LocalSpacing.current
|
||||
@ -550,10 +513,9 @@ private fun SliderImpl(
|
||||
val fontWeight by animateIntAsState(
|
||||
targetValue = if (bufferedPosition != null) 800
|
||||
else 400,
|
||||
label = "position-text-font-weight"
|
||||
)
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.medium),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.small),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = modifier
|
||||
) {
|
||||
@ -566,23 +528,19 @@ private fun SliderImpl(
|
||||
color = LocalContentColor.current.copy(alpha = 0.75f),
|
||||
maxLines = 1,
|
||||
fontFamily = FontFamilies.JetbrainsMono,
|
||||
fontSize = 12.sp,
|
||||
fontWeight = FontWeight(fontWeight),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.basicMarquee()
|
||||
)
|
||||
val sliderThumbWidthDp by animateDpAsState(
|
||||
targetValue = 4.dp,
|
||||
label = "slider-thumb-width-dp"
|
||||
)
|
||||
val sliderThumbWidthDp by animateDpAsState(4.dp)
|
||||
val sliderInteractionSource = remember { MutableInteractionSource() }
|
||||
Slider(
|
||||
value = animContentPosition,
|
||||
valueRange = 0f..contentDuration
|
||||
.coerceAtLeast(0L)
|
||||
.toFloat(),
|
||||
onValueChange = {
|
||||
onBufferedPositionChanged(it.roundToLong())
|
||||
},
|
||||
onValueChange = { onBufferedPositionChanged(it.roundToLong()) },
|
||||
thumb = {
|
||||
SliderDefaults.Thumb(
|
||||
interactionSource = sliderInteractionSource,
|
||||
@ -600,38 +558,44 @@ private fun CwPositionSliderImpl(
|
||||
modifier: Modifier = Modifier,
|
||||
onResetPlayback: () -> Unit
|
||||
) {
|
||||
val spacing = LocalSpacing.current
|
||||
val time = remember(position) {
|
||||
position.toDuration(DurationUnit.MILLISECONDS).toComponents { hours, minutes, seconds, _ ->
|
||||
buildString {
|
||||
if (hours > 0) {
|
||||
append("$hours:")
|
||||
position.toDuration(DurationUnit.MILLISECONDS).toComponents { h, m, s, _ ->
|
||||
listOf(h, m, s)
|
||||
.dropWhile { it.toInt() == 0 }
|
||||
.joinToString(":") {
|
||||
if (it.toInt() >= 10) it.toString()
|
||||
else "0$it"
|
||||
}
|
||||
if (minutes < 10) {
|
||||
append("0")
|
||||
}
|
||||
append("$minutes:")
|
||||
if (seconds < 10) {
|
||||
append("0")
|
||||
}
|
||||
append("$seconds")
|
||||
}
|
||||
}
|
||||
}
|
||||
Column(modifier) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
CwPositionRewinder(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(string.feat_channel_cw_position_title, time),
|
||||
)
|
||||
},
|
||||
action = {
|
||||
TextButton(onClick = onResetPlayback) {
|
||||
Text(
|
||||
text = stringResource(string.feat_channel_cw_position_button)
|
||||
)
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Text(
|
||||
// text = stringResource(string.feat_channel_cw_position_title, time),
|
||||
// modifier = Modifier.weight(1f)
|
||||
// )
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
withLink(
|
||||
LinkAnnotation.Clickable(
|
||||
tag = stringResource(string.feat_channel_cw_position_button),
|
||||
linkInteractionListener = { onResetPlayback() }
|
||||
),
|
||||
) {
|
||||
append(stringResource(string.feat_channel_cw_position_button))
|
||||
}
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
modifier = Modifier
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = LocalContentColor.current.copy(0.65f),
|
||||
shape = CircleShape
|
||||
)
|
||||
.padding(spacing.small)
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -658,7 +622,6 @@ private fun MaskCenterButton(
|
||||
}
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isScaled) 0.65f else 1f,
|
||||
label = "MaskCenterButton-scale",
|
||||
animationSpec = spring(
|
||||
dampingRatio = Spring.DampingRatioMediumBouncy,
|
||||
stiffness = Spring.StiffnessMediumLow
|
||||
@ -708,10 +671,7 @@ private fun MaskNavigateButton(
|
||||
val isScaled by remember {
|
||||
derivedStateOf { isPressed || isHovered || isDragged }
|
||||
}
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isScaled) 0.85f else 1f,
|
||||
label = "MaskCenterButton-scale"
|
||||
)
|
||||
val scale by animateFloatAsState(if (isScaled) 0.85f else 1f)
|
||||
MaskCircleButton(
|
||||
state = state,
|
||||
isSmallDimension = true,
|
||||
@ -759,8 +719,5 @@ private enum class MaskNavigateRole {
|
||||
Next, Previous
|
||||
}
|
||||
|
||||
private sealed class MaskSlideRole {
|
||||
data object None : MaskSlideRole()
|
||||
data class CwPosition(val milliseconds: Long) : MaskSlideRole()
|
||||
data object Slide : MaskSlideRole()
|
||||
}
|
||||
data class CwPosition(val milliseconds: Long)
|
||||
data object Slide
|
@ -6,7 +6,6 @@ import android.graphics.Rect
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
@ -18,7 +17,6 @@ import androidx.compose.material.icons.automirrored.rounded.VolumeUp
|
||||
import androidx.compose.material.icons.rounded.DarkMode
|
||||
import androidx.compose.material.icons.rounded.LightMode
|
||||
import androidx.compose.material.icons.rounded.Speed
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
@ -31,7 +29,6 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
@ -179,6 +176,7 @@ fun ChannelRoute(
|
||||
}
|
||||
.launchIn(this)
|
||||
snapshotFlow { pullPanelLayoutState.fraction }
|
||||
.drop(1)
|
||||
.onEach { maskState.sleep() }
|
||||
.launchIn(this)
|
||||
}
|
||||
@ -283,7 +281,7 @@ fun ChannelRoute(
|
||||
speed = it
|
||||
},
|
||||
cwPosition = viewModel.cwPosition,
|
||||
onRewind = viewModel::onRewind,
|
||||
onResetPlayback = viewModel::onResetPlayback,
|
||||
onPreviousChannelClick = viewModel::getPreviousChannel,
|
||||
onNextChannelClick = viewModel::getNextChannel,
|
||||
onEnterPipMode = {
|
||||
@ -344,7 +342,7 @@ private fun ChannelPlayer(
|
||||
brightness: Float,
|
||||
speed: Float,
|
||||
cwPosition: Long,
|
||||
onRewind: () -> Unit,
|
||||
onResetPlayback: () -> Unit,
|
||||
onFavorite: () -> Unit,
|
||||
openDlnaDevices: () -> Unit,
|
||||
openChooseFormat: () -> Unit,
|
||||
@ -455,9 +453,10 @@ private fun ChannelPlayer(
|
||||
maskState = maskState,
|
||||
favourite = favourite,
|
||||
isSeriesPlaylist = isSeriesPlaylist,
|
||||
useVertical = useVertical,
|
||||
hasTrack = hasTrack,
|
||||
cwPosition = cwPosition,
|
||||
onRewind = onRewind,
|
||||
onResetPlayback = onResetPlayback,
|
||||
isPanelExpanded = isPanelExpanded,
|
||||
onFavorite = onFavorite,
|
||||
openDlnaDevices = openDlnaDevices,
|
||||
|
@ -1,85 +0,0 @@
|
||||
package com.m3u.smartphone.ui.business.channel.components
|
||||
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.graphics.takeOrElse
|
||||
import androidx.compose.ui.semantics.onClick
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m3u.smartphone.ui.material.components.FontFamilies
|
||||
|
||||
@Composable
|
||||
fun CwPositionRewinder(
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color = Color.Unspecified,
|
||||
contentColor: Color = Color.Unspecified,
|
||||
borderColor: Color = Color.Unspecified,
|
||||
text: @Composable () -> Unit,
|
||||
action: (@Composable RowScope.() -> Unit)? = null,
|
||||
) {
|
||||
val lContainerColor = containerColor.takeOrElse { MaterialTheme.colorScheme.surfaceVariant }
|
||||
val lContentColor = contentColor.takeOrElse {
|
||||
MaterialTheme.colorScheme.contentColorFor(lContainerColor)
|
||||
.takeOrElse { MaterialTheme.colorScheme.onSurfaceVariant }
|
||||
}
|
||||
val lBorderColor = borderColor.takeOrElse { MaterialTheme.colorScheme.outline }
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides lContentColor,
|
||||
LocalTextStyle provides MaterialTheme.typography.bodyMedium.copy(
|
||||
fontFamily = FontFamilies.LexendExa
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.drawBehind {
|
||||
drawRoundRect(
|
||||
color = lContainerColor,
|
||||
cornerRadius = CornerRadius(16.dp.toPx())
|
||||
)
|
||||
drawRoundRect(
|
||||
color = lBorderColor,
|
||||
cornerRadius = CornerRadius(16.dp.toPx()),
|
||||
style = Stroke(
|
||||
width = 2.dp.toPx(),
|
||||
cap = Stroke.DefaultCap,
|
||||
miter = Stroke.DefaultMiter,
|
||||
pathEffect = null
|
||||
)
|
||||
)
|
||||
}
|
||||
.semantics(mergeDescendants = true) {
|
||||
onClick { true }
|
||||
}
|
||||
.then(modifier),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Row(Modifier.weight(1f)) { text() }
|
||||
if (action != null) {
|
||||
Spacer(Modifier.size(8.dp))
|
||||
action.invoke(this)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
} else {
|
||||
Spacer(Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package com.m3u.smartphone.ui.material.components.mask
|
||||
|
||||
import android.util.Log
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
@ -42,7 +43,7 @@ private class MaskStateCoroutineImpl(
|
||||
private var currentTime: Long by mutableLongStateOf(systemClock)
|
||||
private var lastTime: Long by mutableLongStateOf(0L)
|
||||
private var keys by mutableStateOf<Set<Any>>(emptySet())
|
||||
override val locked: Boolean by derivedStateOf { keys.isNotEmpty() }
|
||||
override val locked: Boolean = keys.isNotEmpty()
|
||||
|
||||
override val visible: Boolean by derivedStateOf {
|
||||
val before = (locked || (currentTime - lastTime <= minDuration))
|
||||
@ -62,10 +63,12 @@ private class MaskStateCoroutineImpl(
|
||||
private val systemClock: Long get() = System.currentTimeMillis() / 1000
|
||||
|
||||
override fun wake(duration: Duration) {
|
||||
Log.e("TAG", "ChannelPlayer: weak", )
|
||||
lastTime = currentTime + duration.inWholeMilliseconds / 1000
|
||||
}
|
||||
|
||||
override fun sleep() {
|
||||
Log.e("TAG", "ChannelPlayer: sleep", )
|
||||
lastTime = 0
|
||||
val iterator = unlockedJobs.iterator()
|
||||
while (iterator.hasNext()) {
|
||||
|
@ -99,10 +99,10 @@ class ChannelViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun onRewind() {
|
||||
fun onResetPlayback() {
|
||||
val channelUrl = channel.value?.url ?: return
|
||||
viewModelScope.launch {
|
||||
playerManager.onRewind(channelUrl)
|
||||
playerManager.onResetPlayback(channelUrl)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,8 +15,6 @@ import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.cachedIn
|
||||
import com.m3u.core.Contracts
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -33,7 +31,6 @@ import com.m3u.data.repository.media.MediaRepository
|
||||
import com.m3u.data.repository.playlist.PlaylistRepository
|
||||
import com.m3u.data.service.PlayerManager
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@ -41,7 +38,6 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
@ -55,26 +51,16 @@ class FavoriteViewModel @Inject constructor(
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val playerManager: PlayerManager,
|
||||
preferences: Preferences,
|
||||
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
|
||||
delegate: Logger
|
||||
) : ViewModel() {
|
||||
private val logger = delegate.install(Profiles.VIEWMODEL_FAVOURITE)
|
||||
|
||||
private val zappingMode = snapshotFlow { preferences.zappingMode }
|
||||
.flowOn(ioDispatcher)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = Preferences.DEFAULT_ZAPPING_MODE,
|
||||
started = SharingStarted.WhileSubscribed(5_000)
|
||||
)
|
||||
|
||||
val zapping: StateFlow<Channel?> = combine(
|
||||
zappingMode,
|
||||
snapshotFlow { preferences.zappingMode },
|
||||
playerManager.channel
|
||||
) { zappingMode, channel ->
|
||||
channel.takeIf { zappingMode }
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = null,
|
||||
@ -94,7 +80,6 @@ class FavoriteViewModel @Inject constructor(
|
||||
|
||||
val sort = sortIndex
|
||||
.map { sorts[it] }
|
||||
.flowOn(ioDispatcher)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = Sort.UNSPECIFIED,
|
||||
|
@ -6,8 +6,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.Default
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -26,7 +24,7 @@ import com.m3u.data.repository.programme.ProgrammeRepository
|
||||
import com.m3u.data.service.PlayerManager
|
||||
import com.m3u.data.worker.SubscriptionWorker
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@ -50,7 +48,6 @@ class ForyouViewModel @Inject constructor(
|
||||
programmeRepository: ProgrammeRepository,
|
||||
private val playerManager: PlayerManager,
|
||||
preferences: Preferences,
|
||||
@Dispatcher(Default) defaultDispatcher: CoroutineDispatcher,
|
||||
workManager: WorkManager,
|
||||
delegate: Logger
|
||||
) : ViewModel() {
|
||||
@ -88,7 +85,6 @@ class ForyouViewModel @Inject constructor(
|
||||
|
||||
private val unseensDuration = snapshotFlow { preferences.unseensMilliseconds }
|
||||
.map { it.toDuration(DurationUnit.MILLISECONDS) }
|
||||
.flowOn(defaultDispatcher)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Lazily,
|
||||
@ -99,13 +95,12 @@ class ForyouViewModel @Inject constructor(
|
||||
unseensDuration.flatMapLatest { channelRepository.observeAllUnseenFavorites(it) },
|
||||
channelRepository.observePlayedRecently(),
|
||||
) { channels, playedRecently ->
|
||||
playerManager.cwPositionObserver
|
||||
listOfNotNull<Recommend.Spec>(
|
||||
playedRecently?.let { Recommend.CwSpec(it, playerManager.getCwPosition(it.url)) },
|
||||
*(channels.map { channel -> Recommend.UnseenSpec(channel) }.take(8).toTypedArray())
|
||||
)
|
||||
}
|
||||
.flowOn(defaultDispatcher)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(1_000L),
|
||||
|
@ -6,8 +6,6 @@ import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -23,6 +21,7 @@ import com.m3u.data.repository.programme.ProgrammeRepository
|
||||
import com.m3u.data.worker.SubscriptionWorker
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
@ -46,7 +45,6 @@ class PlaylistConfigurationViewModel @Inject constructor(
|
||||
private val programmeRepository: ProgrammeRepository,
|
||||
private val xtreamParser: XtreamParser,
|
||||
private val workManager: WorkManager,
|
||||
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher,
|
||||
savedStateHandle: SavedStateHandle,
|
||||
delegate: Logger
|
||||
) : ViewModel() {
|
||||
@ -107,7 +105,7 @@ class PlaylistConfigurationViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = null,
|
||||
|
@ -21,8 +21,6 @@ import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import com.m3u.core.Contracts
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -50,6 +48,7 @@ import com.m3u.business.playlist.PlaylistMessage.ChannelCoverSaved
|
||||
import com.m3u.core.wrapper.Sort
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@ -84,7 +83,6 @@ class PlaylistViewModel @Inject constructor(
|
||||
private val playerManager: PlayerManager,
|
||||
preferences: Preferences,
|
||||
workManager: WorkManager,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
delegate: Logger
|
||||
) : ViewModel() {
|
||||
private val logger = delegate.install(Profiles.VIEWMODEL_PLAYLIST)
|
||||
@ -129,7 +127,7 @@ class PlaylistViewModel @Inject constructor(
|
||||
)
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = false,
|
||||
|
@ -13,8 +13,6 @@ import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import androidx.work.workDataOf
|
||||
import com.m3u.core.architecture.Publisher
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -31,12 +29,11 @@ import com.m3u.data.parser.xtream.XtreamInput
|
||||
import com.m3u.data.repository.playlist.PlaylistRepository
|
||||
import com.m3u.data.repository.channel.ChannelRepository
|
||||
import com.m3u.data.service.Messager
|
||||
import com.m3u.data.service.PlayerManager
|
||||
import com.m3u.data.worker.BackupWorker
|
||||
import com.m3u.data.worker.RestoreWorker
|
||||
import com.m3u.data.worker.SubscriptionWorker
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
@ -60,7 +57,6 @@ class SettingViewModel @Inject constructor(
|
||||
publisher: Publisher,
|
||||
// FIXME: do not use dao in viewmodel
|
||||
private val colorSchemeDao: ColorSchemeDao,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
delegate: Logger
|
||||
) : ViewModel() {
|
||||
private val logger = delegate.install(Profiles.VIEWMODEL_SETTING)
|
||||
@ -89,7 +85,7 @@ class SettingViewModel @Inject constructor(
|
||||
.filter { it.hiddenCategories.isNotEmpty() }
|
||||
.flatMap { playlist -> playlist.hiddenCategories.map { playlist to it } }
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
initialValue = emptyList(),
|
||||
@ -106,7 +102,7 @@ class SettingViewModel @Inject constructor(
|
||||
colorSchemeDao.observeAll().catch { emit(emptyList()) },
|
||||
snapshotFlow { preferences.followSystemTheme }
|
||||
) { all, followSystemTheme -> if (followSystemTheme) all.filter { !it.isDark } else all }
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
@ -254,7 +250,7 @@ class SettingViewModel @Inject constructor(
|
||||
}
|
||||
BackingUpAndRestoringState.of(backingUp, restoring)
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.Default)
|
||||
.stateIn(
|
||||
scope = viewModelScope,
|
||||
// determine ui button enabled or not
|
||||
|
@ -16,7 +16,7 @@ abstract class Suggester {
|
||||
val completedResult: Boolean
|
||||
get() {
|
||||
complete()
|
||||
return result!!
|
||||
return requireNotNull(result) { "suggester hasn't any conditions." }
|
||||
}
|
||||
|
||||
|
||||
|
@ -33,4 +33,4 @@ fun <S> composableOf(
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package com.m3u.core.architecture.dispatcher
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object CoroutineDispatcherModule {
|
||||
|
||||
@Provides
|
||||
@Dispatcher(M3uDispatchers.Default)
|
||||
fun provideDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
@Provides
|
||||
@Dispatcher(M3uDispatchers.IO)
|
||||
fun provideIODispatcher(): CoroutineDispatcher = Dispatchers.IO
|
||||
|
||||
@Provides
|
||||
@Dispatcher(M3uDispatchers.Main)
|
||||
fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
package com.m3u.core.architecture.dispatcher
|
||||
|
||||
import javax.inject.Qualifier
|
||||
import kotlin.annotation.AnnotationRetention.RUNTIME
|
||||
|
||||
@Qualifier
|
||||
@Retention(RUNTIME)
|
||||
annotation class Dispatcher(val dispatcher: M3uDispatchers)
|
||||
|
||||
enum class M3uDispatchers {
|
||||
Default,
|
||||
IO,
|
||||
Main
|
||||
}
|
@ -2,7 +2,7 @@ package com.m3u.data.parser
|
||||
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.execute
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -15,10 +15,9 @@ class ParserUtils(
|
||||
val json: Json,
|
||||
val okHttpClient: OkHttpClient,
|
||||
val logger: Logger,
|
||||
val ioDispatcher: CoroutineDispatcher
|
||||
) {
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend inline fun <reified T> newCall(url: String): T? = withContext(ioDispatcher) {
|
||||
suspend inline fun <reified T> newCall(url: String): T? = withContext(Dispatchers.IO) {
|
||||
logger.execute {
|
||||
okHttpClient.newCall(
|
||||
Request.Builder().url(url).build()
|
||||
@ -33,7 +32,7 @@ class ParserUtils(
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
suspend inline fun <reified T> newCallOrThrow(url: String): T =
|
||||
withContext(ioDispatcher) {
|
||||
withContext(Dispatchers.IO) {
|
||||
okHttpClient.newCall(
|
||||
Request.Builder().url(url).build()
|
||||
)
|
||||
|
@ -1,12 +1,10 @@
|
||||
package com.m3u.data.parser.epg
|
||||
|
||||
import android.util.Xml
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
@ -15,7 +13,6 @@ import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class EpgParserImpl @Inject constructor(
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
delegate: Logger
|
||||
) : EpgParser {
|
||||
private val logger = delegate.install(Profiles.PARSER_EPG)
|
||||
@ -38,7 +35,7 @@ internal class EpgParserImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
private val ns: String? = null
|
||||
private fun XmlPullParser.readProgramme(): EpgProgramme {
|
||||
|
@ -1,12 +1,10 @@
|
||||
package com.m3u.data.parser.m3u
|
||||
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
import com.m3u.core.architecture.logger.post
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
@ -14,7 +12,6 @@ import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class M3UParserImpl @Inject constructor(
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
delegate: Logger
|
||||
) : M3UParser {
|
||||
private val logger = delegate.install(Profiles.PARSER_M3U)
|
||||
@ -105,5 +102,5 @@ internal class M3UParserImpl @Inject constructor(
|
||||
emit(entry)
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.Default)
|
||||
}
|
||||
|
@ -1,26 +1,23 @@
|
||||
package com.m3u.data.parser.xtream
|
||||
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
import com.m3u.data.api.OkhttpClient
|
||||
import com.m3u.data.database.model.DataSource
|
||||
import com.m3u.data.parser.ParserUtils
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.asFlow
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import java.time.Duration
|
||||
import javax.inject.Inject
|
||||
|
||||
internal class XtreamParserImpl @Inject constructor(
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
@OkhttpClient(true) okHttpClient: OkHttpClient,
|
||||
delegate: Logger
|
||||
) : XtreamParser {
|
||||
@ -33,19 +30,12 @@ internal class XtreamParserImpl @Inject constructor(
|
||||
explicitNulls = false
|
||||
isLenient = true
|
||||
}
|
||||
private val okHttpClient = okHttpClient
|
||||
.newBuilder()
|
||||
.callTimeout(Duration.ofMillis(Int.MAX_VALUE.toLong()))
|
||||
.connectTimeout(Duration.ofMillis(Int.MAX_VALUE.toLong()))
|
||||
.readTimeout(Duration.ofMillis(Int.MAX_VALUE.toLong()))
|
||||
.build()
|
||||
|
||||
private val utils by lazy {
|
||||
ParserUtils(
|
||||
json = json,
|
||||
okHttpClient = okHttpClient,
|
||||
logger = logger,
|
||||
ioDispatcher = ioDispatcher
|
||||
logger = logger
|
||||
)
|
||||
}
|
||||
|
||||
@ -94,6 +84,7 @@ internal class XtreamParserImpl @Inject constructor(
|
||||
.collect { serial -> send(serial) }
|
||||
}
|
||||
}
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
override suspend fun getXtreamOutput(input: XtreamInput): XtreamOutput {
|
||||
val (basicUrl, username, password, type) = input
|
||||
|
@ -13,8 +13,6 @@ import coil.request.ErrorResult
|
||||
import coil.request.ImageRequest
|
||||
import coil.request.SuccessResult
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.execute
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -23,7 +21,7 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.ktor.util.cio.writeChannel
|
||||
import io.ktor.utils.io.ByteReadChannel
|
||||
import io.ktor.utils.io.copyAndClose
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
@ -35,7 +33,6 @@ private const val BITMAP_QUALITY = 100
|
||||
internal class MediaRepositoryImpl @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
delegate: Logger,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher
|
||||
) : MediaRepository {
|
||||
private val logger = delegate.install(Profiles.REPOS_MEDIA)
|
||||
private val applicationName = "M3U"
|
||||
@ -48,7 +45,7 @@ internal class MediaRepositoryImpl @Inject constructor(
|
||||
applicationName
|
||||
)
|
||||
|
||||
override suspend fun savePicture(url: String): File = withContext(ioDispatcher) {
|
||||
override suspend fun savePicture(url: String): File = withContext(Dispatchers.IO) {
|
||||
val drawable = checkNotNull(loadDrawable(url))
|
||||
val bitmap = drawable.toBitmap()
|
||||
val name = "Picture_${System.currentTimeMillis()}.png"
|
||||
|
@ -5,8 +5,6 @@ import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.WorkManager
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.execute
|
||||
@ -44,7 +42,7 @@ import com.m3u.data.repository.createCoroutineCache
|
||||
import com.m3u.data.worker.SubscriptionWorker
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.channelFlow
|
||||
@ -83,7 +81,6 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
private val preferences: Preferences,
|
||||
private val workManager: WorkManager,
|
||||
@ApplicationContext private val context: Context,
|
||||
@Dispatcher(M3uDispatchers.IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
) : PlaylistRepository {
|
||||
private val logger = delegate.install(Profiles.REPOS_PLAYLIST)
|
||||
|
||||
@ -158,7 +155,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
}
|
||||
.onEach(cache::push)
|
||||
.onCompletion { cache.flush() }
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect()
|
||||
}
|
||||
|
||||
@ -169,7 +166,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
password: String,
|
||||
type: String?,
|
||||
callback: (count: Int) -> Unit
|
||||
): Unit = withContext(ioDispatcher) {
|
||||
): Unit = withContext(Dispatchers.IO) {
|
||||
val input = XtreamInput(basicUrl, username, password, type)
|
||||
val (
|
||||
liveCategories,
|
||||
@ -347,17 +344,16 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
.collect()
|
||||
}
|
||||
|
||||
override suspend fun insertEpgAsPlaylist(title: String, epg: String): Unit =
|
||||
withContext(ioDispatcher) {
|
||||
// just save epg playlist to db
|
||||
playlistDao.insertOrReplace(
|
||||
Playlist(
|
||||
title = title,
|
||||
url = epg,
|
||||
source = DataSource.EPG
|
||||
)
|
||||
override suspend fun insertEpgAsPlaylist(title: String, epg: String) {
|
||||
// just save epg playlist to db
|
||||
playlistDao.insertOrReplace(
|
||||
Playlist(
|
||||
title = title,
|
||||
url = epg,
|
||||
source = DataSource.EPG
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun refresh(url: String) = logger.sandBox {
|
||||
val playlist = checkNotNull(get(url)) { "Cannot find playlist: $url" }
|
||||
@ -388,7 +384,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun backupOrThrow(uri: Uri): Unit = withContext(ioDispatcher) {
|
||||
override suspend fun backupOrThrow(uri: Uri): Unit = withContext(Dispatchers.IO) {
|
||||
val json = Json {
|
||||
prettyPrint = false
|
||||
}
|
||||
@ -420,7 +416,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun restoreOrThrow(uri: Uri) = logger.sandBox {
|
||||
withContext(ioDispatcher) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
}
|
||||
@ -644,9 +640,9 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
|
||||
private suspend fun String.actualUrl(): String {
|
||||
if (!isSupportedAndroidUrl()) return this
|
||||
val uri = Uri.parse(this)
|
||||
val uri = this.toUri()
|
||||
if (uri.scheme == ContentResolver.SCHEME_FILE) return uri.toString()
|
||||
return withContext(ioDispatcher) {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val contentResolver = context.contentResolver
|
||||
val filename = uri.readFileName(contentResolver) ?: filenameWithTimezone
|
||||
val destinationFile = File(context.filesDir, filename)
|
||||
@ -672,7 +668,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
|
||||
}
|
||||
|
||||
private fun openAndroidInput(url: String): InputStream? {
|
||||
val uri = Uri.parse(url)
|
||||
val uri = url.toUri()
|
||||
return context.contentResolver.openInputStream(uri)
|
||||
}
|
||||
}
|
@ -3,8 +3,6 @@ package com.m3u.data.repository.programme
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.execute
|
||||
@ -21,7 +19,7 @@ import com.m3u.data.database.model.epgUrlsOrXtreamXmlUrl
|
||||
import com.m3u.data.parser.epg.EpgParser
|
||||
import com.m3u.data.parser.epg.EpgProgramme
|
||||
import com.m3u.data.parser.epg.toProgramme
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -46,7 +44,6 @@ internal class ProgrammeRepositoryImpl @Inject constructor(
|
||||
private val programmeDao: ProgrammeDao,
|
||||
private val epgParser: EpgParser,
|
||||
@OkhttpClient(true) private val okHttpClient: OkHttpClient,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
delegate: Logger
|
||||
) : ProgrammeRepository {
|
||||
private val logger = delegate.install(Profiles.REPOS_PROGRAMME)
|
||||
@ -191,7 +188,7 @@ internal class ProgrammeRepositoryImpl @Inject constructor(
|
||||
.collect { send(it) }
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
/**
|
||||
* Attempts to find the first valid EPG URL from a list of URLs.
|
||||
|
@ -3,8 +3,6 @@ package com.m3u.data.repository.tv
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import com.m3u.core.architecture.Publisher
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -17,9 +15,9 @@ import com.m3u.data.tv.Utils
|
||||
import com.m3u.data.tv.http.HttpServer
|
||||
import com.m3u.data.tv.model.TvInfo
|
||||
import com.m3u.data.tv.nsd.NsdDeviceManager
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@ -46,11 +44,10 @@ class TvRepositoryImpl @Inject constructor(
|
||||
logger: Logger,
|
||||
preferences: Preferences,
|
||||
publisher: Publisher,
|
||||
@Dispatcher(IO) ioDispatcher: CoroutineDispatcher
|
||||
) : TvRepository() {
|
||||
private val logger = logger.install(Profiles.REPOS_LEANBACK)
|
||||
private val tv = publisher.tv
|
||||
private val coroutineScope = CoroutineScope(ioDispatcher)
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||
|
||||
init {
|
||||
snapshotFlow { preferences.remoteControl }
|
||||
|
@ -48,7 +48,7 @@ interface PlayerManager {
|
||||
suspend fun recordVideo(uri: Uri)
|
||||
|
||||
val cwPositionObserver: SharedFlow<Long>
|
||||
suspend fun onRewind(channelUrl: String)
|
||||
suspend fun onResetPlayback(channelUrl: String)
|
||||
suspend fun getCwPosition(channelUrl: String): Long
|
||||
suspend fun reloadThumbnail(channelUrl: String): Uri?
|
||||
suspend fun syncThumbnail(channelUrl: String): Uri?
|
||||
|
@ -3,17 +3,16 @@ package com.m3u.data.service.internal
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import com.jakewharton.disklrucache.DiskLruCache
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
internal class ChannelPreferenceProvider(
|
||||
directory: File,
|
||||
appVersion: Int,
|
||||
ioDispatcher: CoroutineDispatcher
|
||||
appVersion: Int
|
||||
) {
|
||||
private val limitedParallelism = ioDispatcher.limitedParallelism(1, "channel-preference")
|
||||
private val limitedParallelism = Dispatchers.IO.limitedParallelism(1, "channel-preference")
|
||||
private val cache = DiskLruCache.open(directory, appVersion, 3, 4 * 1024 * 1024) // 4mb
|
||||
|
||||
suspend operator fun get(
|
||||
|
@ -1,24 +1,16 @@
|
||||
package com.m3u.data.service.internal
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.Main
|
||||
import com.m3u.data.tv.model.RemoteDirection
|
||||
import com.m3u.data.service.DPadReactionService
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import com.m3u.data.tv.model.RemoteDirection
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@Immutable
|
||||
class DPadReactionServiceImpl @Inject constructor(
|
||||
@Dispatcher(Main) private val mainDispatcher: CoroutineDispatcher
|
||||
) : DPadReactionService {
|
||||
class DPadReactionServiceImpl @Inject constructor() : DPadReactionService {
|
||||
override val incoming = MutableSharedFlow<RemoteDirection>()
|
||||
|
||||
override suspend fun emit(remoteDirection: RemoteDirection) {
|
||||
withContext(mainDispatcher) {
|
||||
incoming.emit(remoteDirection)
|
||||
}
|
||||
incoming.emit(remoteDirection)
|
||||
}
|
||||
}
|
@ -1,12 +1,10 @@
|
||||
package com.m3u.data.service.internal
|
||||
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.wrapper.Message
|
||||
import com.m3u.data.service.Messager
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
@ -14,14 +12,12 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
class MessagerImpl @Inject constructor(
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher
|
||||
) : Messager {
|
||||
class MessagerImpl @Inject constructor() : Messager {
|
||||
private val _message: MutableStateFlow<Message> = MutableStateFlow(Message.Dynamic.EMPTY)
|
||||
override val message: StateFlow<Message> get() = _message.asStateFlow()
|
||||
|
||||
private var job: Job? = null
|
||||
private val coroutineScope = CoroutineScope(ioDispatcher)
|
||||
private val coroutineScope = CoroutineScope(SupervisorJob())
|
||||
|
||||
override fun emit(message: Message) {
|
||||
job?.cancel()
|
||||
|
@ -51,9 +51,6 @@ import androidx.media3.transformer.InAppMp4Muxer
|
||||
import androidx.media3.transformer.TransformationRequest
|
||||
import androidx.media3.transformer.Transformer
|
||||
import com.m3u.core.architecture.Publisher
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.Main
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
@ -73,8 +70,8 @@ import com.m3u.data.service.MediaCommand
|
||||
import com.m3u.data.service.PlayerManager
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
@ -108,8 +105,6 @@ import javax.inject.Inject
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class PlayerManagerImpl @Inject constructor(
|
||||
@Dispatcher(Main) private val mainDispatcher: CoroutineDispatcher,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
|
||||
@ApplicationContext private val context: Context,
|
||||
@OkhttpClient(false) private val okHttpClient: OkHttpClient,
|
||||
private val preferences: Preferences,
|
||||
@ -119,13 +114,12 @@ class PlayerManagerImpl @Inject constructor(
|
||||
publisher: Publisher,
|
||||
delegate: Logger
|
||||
) : PlayerManager, Player.Listener, MediaSession.Callback {
|
||||
private val mainCoroutineScope = CoroutineScope(mainDispatcher)
|
||||
private val ioCoroutineScope = CoroutineScope(ioDispatcher)
|
||||
private val mainCoroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
private val ioCoroutineScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
private val channelPreferenceProvider = ChannelPreferenceProvider(
|
||||
directory = context.cacheDir.resolve("channel-preferences"),
|
||||
appVersion = publisher.versionCode,
|
||||
ioDispatcher = ioDispatcher
|
||||
appVersion = publisher.versionCode
|
||||
)
|
||||
|
||||
private val continueWatchingCondition = ContinueWatchingCondition.getInstance<Player>()
|
||||
@ -428,7 +422,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
delay(1.seconds)
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
.flowOn(Dispatchers.IO)
|
||||
|
||||
override suspend fun reloadThumbnail(channelUrl: String): Uri? {
|
||||
val channelPreference = getChannelPreference(channelUrl)
|
||||
@ -442,8 +436,9 @@ class PlayerManagerImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
}
|
||||
override suspend fun syncThumbnail(channelUrl: String): Uri? = withContext(ioDispatcher) {
|
||||
val thumbnail = codecs.getThumbnail(context, channelUrl.toUri())?: return@withContext null
|
||||
|
||||
override suspend fun syncThumbnail(channelUrl: String): Uri? = withContext(Dispatchers.IO) {
|
||||
val thumbnail = codecs.getThumbnail(context, channelUrl.toUri()) ?: return@withContext null
|
||||
val filename = UUID.randomUUID().toString() + ".jpeg"
|
||||
val file = File(thumbnailDir, filename)
|
||||
while (!file.createNewFile()) {
|
||||
@ -462,6 +457,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
)
|
||||
uri
|
||||
}
|
||||
|
||||
private fun createPlayer(
|
||||
mediaSourceFactory: MediaSource.Factory,
|
||||
tunneling: Boolean
|
||||
@ -621,7 +617,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
}
|
||||
|
||||
override suspend fun recordVideo(uri: Uri) {
|
||||
withContext(mainDispatcher) {
|
||||
withContext(Dispatchers.Main) {
|
||||
try {
|
||||
val currentPlayer = player.value ?: return@withContext
|
||||
val tracksGroup = currentPlayer.currentTracks.groups.first {
|
||||
@ -700,7 +696,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
)
|
||||
.build()
|
||||
|
||||
withContext(mainDispatcher) {
|
||||
withContext(Dispatchers.Main) {
|
||||
transformer.start(
|
||||
MediaItem.fromUri(channel.value?.url.orEmpty()),
|
||||
uri.path.orEmpty()
|
||||
@ -714,7 +710,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
|
||||
override val cwPositionObserver = MutableSharedFlow<Long>(replay = 1)
|
||||
|
||||
override suspend fun onRewind(channelUrl: String) {
|
||||
override suspend fun onResetPlayback(channelUrl: String) {
|
||||
cwPositionObserver.emit(-1L)
|
||||
resetContinueWatching(channelUrl, ignorePositionCondition = true)
|
||||
val currentPlayer = player.value ?: return
|
||||
@ -794,7 +790,7 @@ class PlayerManagerImpl @Inject constructor(
|
||||
cwPositionObserver.emit(-1L)
|
||||
return
|
||||
}
|
||||
withContext(mainDispatcher) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (continueWatchingCondition.isRestoringSupported(player)) {
|
||||
logger.post { "restoreContinueWatching, $cwPosition" }
|
||||
cwPositionObserver.emit(cwPosition)
|
||||
@ -803,12 +799,19 @@ class PlayerManagerImpl @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun resetContinueWatching(channelUrl: String, ignorePositionCondition: Boolean = false) {
|
||||
private suspend fun resetContinueWatching(
|
||||
channelUrl: String,
|
||||
ignorePositionCondition: Boolean = false
|
||||
) {
|
||||
logger.post { "resetContinueWatching, channelUrl=$channelUrl, ignorePositionCondition=$ignorePositionCondition" }
|
||||
val channelPreference = getChannelPreference(channelUrl)
|
||||
val player = this@PlayerManagerImpl.player.value
|
||||
withContext(mainDispatcher) {
|
||||
if (player != null && continueWatchingCondition.isResettingSupported(player, ignorePositionCondition)) {
|
||||
withContext(Dispatchers.Main) {
|
||||
if (player != null && continueWatchingCondition.isResettingSupported(
|
||||
player,
|
||||
ignorePositionCondition
|
||||
)
|
||||
) {
|
||||
addChannelPreference(
|
||||
channelUrl,
|
||||
channelPreference?.copy(cwPosition = -1L) ?: ChannelPreference(cwPosition = -1L)
|
||||
|
@ -2,27 +2,22 @@ package com.m3u.data.tv.nsd
|
||||
|
||||
import android.net.nsd.NsdManager
|
||||
import android.net.nsd.NsdServiceInfo
|
||||
import com.m3u.core.architecture.dispatcher.Dispatcher
|
||||
import com.m3u.core.architecture.dispatcher.M3uDispatchers.IO
|
||||
import com.m3u.core.architecture.logger.Logger
|
||||
import com.m3u.core.architecture.logger.Profiles
|
||||
import com.m3u.core.architecture.logger.install
|
||||
import com.m3u.data.tv.Utils
|
||||
import com.m3u.data.tv.nsd.NsdDeviceManager.Companion.META_DATA_PIN
|
||||
import com.m3u.data.tv.nsd.NsdDeviceManager.Companion.SERVICE_TYPE
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.channels.trySendBlocking
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import javax.inject.Inject
|
||||
|
||||
class NsdDeviceManagerImpl @Inject constructor(
|
||||
private val nsdManager: NsdManager,
|
||||
delegate: Logger,
|
||||
@Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher
|
||||
) : NsdDeviceManager {
|
||||
private val logger = delegate.install(Profiles.SERVICE_NSD)
|
||||
|
||||
@ -91,7 +86,6 @@ class NsdDeviceManagerImpl @Inject constructor(
|
||||
nsdManager.stopServiceDiscovery(listener)
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
|
||||
override fun broadcast(
|
||||
name: String,
|
||||
@ -139,5 +133,4 @@ class NsdDeviceManagerImpl @Inject constructor(
|
||||
trySendBlocking(null)
|
||||
}
|
||||
}
|
||||
.flowOn(ioDispatcher)
|
||||
}
|
Reference in New Issue
Block a user