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