fix: coroutine-dispatcher.

This commit is contained in:
oxy-macmini
2025-04-20 17:29:08 +08:00
parent 0807da85a2
commit aacb95cfff
29 changed files with 161 additions and 406 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,4 +33,4 @@ fun <S> composableOf(
} else { } else {
null null
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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