refactor: migrate shared-preferences to data-store.

This commit is contained in:
oxy-macmini
2025-04-25 23:27:35 +08:00
parent 720f4d5ef5
commit fefff76e2e
38 changed files with 525 additions and 529 deletions

View File

@ -4,7 +4,6 @@ import android.app.Application
import androidx.hilt.work.HiltWorkerFactory
import androidx.work.Configuration
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.extension.runtime.Utils
import com.m3u.smartphone.ui.business.crash.CrashHandler
import dagger.hilt.android.HiltAndroidApp
@ -22,9 +21,6 @@ class M3UApplication : Application(), Configuration.Provider {
@Logger.MessageImpl
lateinit var messager: Logger
@Inject
lateinit var preferences: Preferences
// private val coroutineScope = CoroutineScope(SupervisorJob())
override fun onCreate() {

View File

@ -34,10 +34,11 @@ import androidx.navigation.compose.rememberNavController
import androidx.navigation.navOptions
import com.m3u.smartphone.ui.common.connect.RemoteControlSheet
import com.m3u.smartphone.ui.common.connect.RemoteControlSheetValue
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.data.tv.model.RemoteDirection
import androidx.compose.material3.Icon
import androidx.compose.ui.platform.LocalContext
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.smartphone.ui.business.channel.PlayerActivity
import com.m3u.smartphone.ui.material.model.LocalSpacing
import com.m3u.smartphone.ui.common.AppNavHost
@ -112,7 +113,9 @@ private fun AppImpl(
) {
val context = LocalContext.current
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val zappingMode by preferenceOf(PreferencesKeys.ZAPPING_MODE)
val remoteControl by preferenceOf(PreferencesKeys.REMOTE_CONTROL)
val entry by navController.currentBackStackEntryAsState()
@ -123,7 +126,7 @@ private fun AppImpl(
}
val navigateToChannel: () -> Unit = {
if (!preferences.zappingMode || !PlayerActivity.isInPipMode) {
if (!zappingMode || !PlayerActivity.isInPipMode) {
val options = ActivityOptions.makeCustomAnimation(
context,
0,
@ -168,7 +171,7 @@ private fun AppImpl(
) {
SnackHost(Modifier.weight(1f))
AnimatedVisibility(
visible = preferences.remoteControl,
visible = remoteControl,
enter = scaleIn(initialScale = 0.65f) + fadeIn(),
exit = scaleOut(targetScale = 0.65f) + fadeOut()
) {

View File

@ -9,7 +9,9 @@ import androidx.lifecycle.viewModelScope
import androidx.work.WorkManager
import com.m3u.smartphone.ui.common.connect.RemoteControlSheetValue
import com.m3u.core.architecture.Publisher
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asStateFlow
import com.m3u.data.api.TvApiDelegate
import com.m3u.data.tv.model.RemoteDirection
import com.m3u.data.repository.tv.ConnectionToTvValue
@ -41,8 +43,8 @@ class AppViewModel @Inject constructor(
private val tvRepository: TvRepository,
private val tvApi: TvApiDelegate,
private val workManager: WorkManager,
private val preferences: Preferences,
private val publisher: Publisher,
private val settings: Settings
) : ViewModel() {
init {
refreshProgrammes()
@ -127,7 +129,8 @@ class AppViewModel @Inject constructor(
tvCodeOnSmartphone.emit(code)
}
checkTvCodeOnSmartphoneJob?.cancel()
checkTvCodeOnSmartphoneJob = snapshotFlow { preferences.remoteControl }
checkTvCodeOnSmartphoneJob = settings
.asStateFlow(PreferencesKeys.REMOTE_CONTROL)
.onEach { remoteControl ->
if (!remoteControl) {
forgetTvCodeOnSmartphone()

View File

@ -78,7 +78,8 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.media3.common.Player
import com.m3u.business.channel.PlayerState
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.foundation.suggest.any
import com.m3u.core.foundation.suggest.suggestAll
import com.m3u.core.foundation.ui.composableOf
@ -137,7 +138,6 @@ fun ChannelMask(
onDimensionChanged: (MaskDimension) -> Unit,
modifier: Modifier = Modifier,
) {
val preferences = hiltPreferences()
val helper = LocalHelper.current
val spacing = LocalSpacing.current
val coroutineScope = rememberCoroutineScope()
@ -167,7 +167,11 @@ fun ChannelMask(
}
}
val isProgressEnabled = preferences.slider
val slider by preferenceOf(PreferencesKeys.SLIDER)
val screencast by preferenceOf(PreferencesKeys.SCREENCAST)
val alwaysShowReplay by preferenceOf(PreferencesKeys.ALWAYS_SHOW_REPLAY)
val screenRotating by preferenceOf(PreferencesKeys.SCREEN_ROTATING)
val isStaticAndSeekable by remember(
playerState.player,
playerState.playState
@ -196,9 +200,9 @@ fun ChannelMask(
val contentPosition by produceState(
initialValue = -1L,
isStaticAndSeekable,
isProgressEnabled
slider
) {
while (isProgressEnabled && isStaticAndSeekable) {
while (slider && isStaticAndSeekable) {
delay(50.milliseconds)
value = playerState.player?.currentPosition ?: -1L
}
@ -207,9 +211,9 @@ fun ChannelMask(
val contentDuration by produceState(
initialValue = -1L,
isStaticAndSeekable,
isProgressEnabled
slider
) {
while (isProgressEnabled && isStaticAndSeekable) {
while (slider && isStaticAndSeekable) {
delay(50.milliseconds)
value = playerState.player?.duration?.absoluteValue ?: -1L
}
@ -311,7 +315,7 @@ fun ChannelMask(
)
}
if (preferences.screencast) {
if (screencast) {
MaskButton(
state = maskState,
icon = Icons.Rounded.Cast,
@ -333,7 +337,7 @@ fun ChannelMask(
val centerRole = MaskCenterRole.of(
playerState.playState,
playerState.isPlaying,
preferences.alwaysShowReplay,
alwaysShowReplay,
playerState.playerError
)
Box(Modifier.size(36.dp)) {
@ -395,7 +399,7 @@ fun ChannelMask(
suggest { exceptionDisplayText.isNotEmpty() }
suggestAll {
suggest { isStaticAndSeekable }
suggest { isProgressEnabled }
suggest { slider }
}
}
) {
@ -429,7 +433,7 @@ fun ChannelMask(
}
if (playStateDisplayText.isNotEmpty()
|| exceptionDisplayText.isNotEmpty()
|| (isStaticAndSeekable && isProgressEnabled)
|| (isStaticAndSeekable && slider)
) {
Spacer(
modifier = Modifier.height(spacing.small)
@ -463,7 +467,7 @@ fun ChannelMask(
ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
}
if (preferences.screenRotating && !autoRotating) {
if (screenRotating && !autoRotating) {
MaskButton(
state = maskState,
icon = Icons.Rounded.ScreenRotationAlt,
@ -477,7 +481,7 @@ fun ChannelMask(
)
}
},
slider = composableOf(isProgressEnabled && isStaticAndSeekable) {
slider = composableOf(slider && isStaticAndSeekable) {
SliderImpl(
contentDuration = contentDuration,
contentPosition = contentPosition,

View File

@ -45,7 +45,8 @@ import androidx.paging.compose.collectAsLazyPagingItems
import com.google.accompanist.permissions.rememberPermissionState
import com.m3u.business.channel.ChannelViewModel
import com.m3u.business.channel.PlayerState
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.util.basic.isNotEmpty
import com.m3u.core.util.basic.title
import com.m3u.data.database.model.AdjacentChannels
@ -83,11 +84,13 @@ fun ChannelRoute(
val openInExternalPlayerString = stringResource(string.feat_channel_open_in_external_app)
val helper = LocalHelper.current
val preferences = hiltPreferences()
val context = LocalContext.current
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
val isPanelEnabled by preferenceOf(PreferencesKeys.PLAYER_PANEL)
val zappingMode by preferenceOf(PreferencesKeys.ZAPPING_MODE)
val requestIgnoreBatteryOptimizations =
rememberPermissionState(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
@ -121,7 +124,6 @@ fun ChannelRoute(
var choosing by remember { mutableStateOf(false) }
val useVertical = PullPanelLayoutDefaults.UseVertical
val isPanelEnabled = preferences.panel
val maskState = rememberMaskState()
val pullPanelLayoutState = rememberPullPanelLayoutState()
@ -151,9 +153,9 @@ fun ChannelRoute(
}
}
LaunchedEffect(preferences.zappingMode, playerState.videoSize) {
LaunchedEffect(zappingMode, playerState.videoSize) {
val videoSize = playerState.videoSize
if (isAutoZappingMode && preferences.zappingMode && !isPipMode) {
if (isAutoZappingMode && zappingMode && !isPipMode) {
maskState.sleep()
val rect = if (videoSize.isNotEmpty) videoSize
else Rect(0, 0, 1920, 1080)
@ -368,7 +370,10 @@ private fun ChannelPlayer(
val currentBrightness by rememberUpdatedState(brightness)
val currentVolume by rememberUpdatedState(volume)
val currentSpeed by rememberUpdatedState(speed)
val preferences = hiltPreferences()
val clipMode by preferenceOf(PreferencesKeys.CLIP_MODE)
val brightnessGesture by preferenceOf(PreferencesKeys.BRIGHTNESS_GESTURE)
val volumeGesture by preferenceOf(PreferencesKeys.VOLUME_GESTURE)
val useVertical = with(windowInfo.containerSize) { width < height }
@ -380,7 +385,7 @@ private fun ChannelPlayer(
Box(modifier) {
val state = rememberPlayerState(
player = playerState.player,
clipMode = preferences.clipMode
clipMode = clipMode
)
var dimension: MaskDimension by remember { mutableStateOf(MaskDimension()) }
val topPadding = with(density) { dimension.top.takeOrElse { 0.dp }.toPx() }
@ -406,7 +411,7 @@ private fun ChannelPlayer(
.fillMaxHeight(0.7f)
.fillMaxWidth(0.18f)
.align(Alignment.CenterStart),
enabled = preferences.brightnessGesture
enabled = brightnessGesture
)
VerticalGestureArea(
@ -423,7 +428,7 @@ private fun ChannelPlayer(
.align(Alignment.CenterEnd)
.fillMaxHeight(0.7f)
.fillMaxWidth(0.18f),
enabled = preferences.volumeGesture
enabled = volumeGesture
)
ChannelMask(

View File

@ -60,7 +60,8 @@ import androidx.compose.ui.util.fastForEach
import androidx.compose.ui.zIndex
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.paging.compose.LazyPagingItems
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.data.database.model.Programme
import com.m3u.data.database.model.ProgrammeRange
import com.m3u.data.database.model.ProgrammeRange.Companion.HOUR_LENGTH
@ -320,8 +321,8 @@ private fun ProgrammeCell(
) {
val currentOnPressed by rememberUpdatedState(onPressed)
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val clockMode = preferences.twelveHourClock
val clockMode by preferenceOf(PreferencesKeys.CLOCK_MODE)
val content = @Composable {
Column(
modifier = Modifier
@ -421,8 +422,9 @@ private fun CurrentTimelineCell(
modifier: Modifier = Modifier
) {
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val twelveHourClock = preferences.twelveHourClock
val twelveHourClock by preferenceOf(PreferencesKeys.CLOCK_MODE)
val color = MaterialTheme.colorScheme.error
val contentColor = MaterialTheme.colorScheme.onError
val currentMilliseconds by rememberUpdatedState(milliseconds)

View File

@ -1,33 +0,0 @@
package com.m3u.smartphone.ui.business.extension
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.PaddingValues
import androidx.navigation.NavController
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavOptions
import androidx.navigation.compose.composable
fun NavGraphBuilder.extensionScreen(
contentPadding: PaddingValues = PaddingValues(),
) {
composable(
route = "extension_route",
enterTransition = { slideInVertically { it } },
exitTransition = { fadeOut() },
popEnterTransition = { fadeIn() },
popExitTransition = { slideOutVertically { it } }
) {
ExtensionRoute(
contentPadding = contentPadding
)
}
}
fun NavController.navigateToExtension(
navOptions: NavOptions? = null,
) {
this.navigate("extension_route", navOptions)
}

View File

@ -26,7 +26,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import com.m3u.business.favorite.FavoriteViewModel
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.mutablePreferenceOf
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.foundation.ui.thenIf
import com.m3u.core.util.basic.title
import com.m3u.core.wrapper.Sort
@ -57,11 +59,13 @@ fun FavoriteRoute(
val title = stringResource(R.string.ui_title_favourite)
val helper = LocalHelper.current
val preferences = hiltPreferences()
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
var rowCount by mutablePreferenceOf(PreferencesKeys.ROW_COUNT)
val godMode by preferenceOf(PreferencesKeys.GOD_MODE)
val channels = viewModel.channels.collectAsLazyPagingItems()
val episodes by viewModel.episodes.collectAsStateWithLifecycle()
val zapping by viewModel.zapping.collectAsStateWithLifecycle()
@ -95,7 +99,7 @@ fun FavoriteRoute(
FavoriteScreen(
contentPadding = contentPadding,
rowCount = preferences.rowCount,
rowCount = rowCount,
channels = channels,
zapping = zapping,
recently = sort == Sort.RECENTLY,
@ -117,14 +121,14 @@ fun FavoriteRoute(
onLongClickChannel = { mediaSheetValue = MediaSheetValue.FavoriteScreen(it) },
modifier = Modifier
.fillMaxSize()
.thenIf(preferences.godMode) {
.thenIf(godMode) {
Modifier.interceptVolumeEvent { event ->
preferences.rowCount = when (event) {
rowCount = when (event) {
KeyEvent.KEYCODE_VOLUME_UP ->
(preferences.rowCount - 1).coerceAtLeast(1)
(rowCount - 1).coerceAtLeast(1)
KeyEvent.KEYCODE_VOLUME_DOWN ->
(preferences.rowCount + 1).coerceAtMost(2)
(rowCount + 1).coerceAtMost(2)
else -> return@interceptVolumeEvent
}

View File

@ -33,7 +33,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.m3u.business.foryou.ForyouViewModel
import com.m3u.business.foryou.Recommend
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.mutablePreferenceOf
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.foundation.ui.composableOf
import com.m3u.core.foundation.ui.thenIf
import com.m3u.core.util.basic.title
@ -70,9 +72,11 @@ fun ForyouRoute(
viewModel: ForyouViewModel = hiltViewModel()
) {
val helper = LocalHelper.current
val preferences = hiltPreferences()
val coroutineScope = rememberCoroutineScope()
var rowCount by mutablePreferenceOf(PreferencesKeys.ROW_COUNT)
val godMode by preferenceOf(PreferencesKeys.GOD_MODE)
val title = stringResource(string.ui_title_foryou)
val playlists by viewModel.playlists.collectAsStateWithLifecycle()
@ -106,7 +110,7 @@ fun ForyouRoute(
subscribingPlaylistUrls = subscribingPlaylistUrls,
refreshingEpgUrls = refreshingEpgUrls,
specs = specs,
rowCount = preferences.rowCount,
rowCount = rowCount,
contentPadding = contentPadding,
navigateToPlaylist = navigateToPlaylist,
onPlayChannel = { channel ->
@ -128,11 +132,11 @@ fun ForyouRoute(
onUnsubscribePlaylist = viewModel::onUnsubscribePlaylist,
modifier = Modifier
.fillMaxSize()
.thenIf(preferences.godMode) {
.thenIf(godMode) {
Modifier.interceptVolumeEvent { event ->
preferences.rowCount = when (event) {
KeyEvent.KEYCODE_VOLUME_UP -> (preferences.rowCount - 1).coerceAtLeast(1)
KeyEvent.KEYCODE_VOLUME_DOWN -> (preferences.rowCount + 1).coerceAtMost(2)
rowCount = when (event) {
KeyEvent.KEYCODE_VOLUME_UP -> (rowCount - 1).coerceAtLeast(1)
KeyEvent.KEYCODE_VOLUME_DOWN -> (rowCount + 1).coerceAtMost(2)
else -> return@interceptVolumeEvent
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@ -25,7 +26,8 @@ import androidx.compose.ui.unit.IntOffset
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.smartphone.ui.material.transformation.BlurTransformation
import com.m3u.smartphone.ui.common.helper.LocalHelper
import com.m3u.smartphone.ui.common.helper.Metadata
@ -39,10 +41,18 @@ internal fun HeadlineBackground(modifier: Modifier = Modifier) {
val helper = LocalHelper.current
val colorScheme = MaterialTheme.colorScheme
val preferences = hiltPreferences()
val darkMode by preferenceOf(PreferencesKeys.DARK_MODE)
val followSystemTheme by preferenceOf(PreferencesKeys.FOLLOW_SYSTEM_THEME)
val noPictureMode by preferenceOf(PreferencesKeys.NO_PICTURE_MODE)
val colorfulBackground by preferenceOf(PreferencesKeys.COLORFUL_BACKGROUND)
val isSystemInDarkTheme = isSystemInDarkTheme()
val useDarkTheme =
preferences.darkMode || (preferences.followSystemTheme && isSystemInDarkTheme())
val useDarkTheme by remember {
derivedStateOf {
darkMode || (followSystemTheme && isSystemInDarkTheme)
}
}
val url = Metadata.headlineUrl
val fraction = Metadata.headlineFraction
@ -60,7 +70,7 @@ internal fun HeadlineBackground(modifier: Modifier = Modifier) {
animationSpec = tween(800)
)
if (!preferences.noPictureMode && !preferences.colorfulBackground) {
if (!noPictureMode && !colorfulBackground) {
AsyncImage(
model = remember(url) {
ImageRequest.Builder(context)

View File

@ -1,6 +1,5 @@
package com.m3u.smartphone.ui.business.foryou.components.recommend
import android.util.Log
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@ -26,6 +25,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -43,7 +43,8 @@ import androidx.compose.ui.util.lerp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.m3u.business.foryou.Recommend
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.foundation.components.AbsoluteSmoothCornerShape
import com.m3u.core.foundation.ui.composableOf
import com.m3u.core.util.basic.title
@ -113,9 +114,8 @@ private fun RecommendItemContent(
) {
val context = LocalContext.current
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val noPictureMode = preferences.noPictureMode
val noPictureMode by preferenceOf(PreferencesKeys.NO_PICTURE_MODE)
Box(Modifier.fillMaxSize()) {
val info = @Composable {

View File

@ -20,7 +20,6 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@ -50,14 +49,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.compose.LifecycleResumeEffect
@ -66,7 +63,9 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.google.accompanist.permissions.rememberPermissionState
import com.m3u.business.playlist.PlaylistViewModel
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.mutablePreferenceOf
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.foundation.ui.thenIf
import com.m3u.core.util.basic.title
import com.m3u.core.wrapper.Event
@ -115,12 +114,15 @@ internal fun PlaylistRoute(
contentPadding: PaddingValues = PaddingValues()
) {
val context = LocalContext.current
val preferences = hiltPreferences()
val helper = LocalHelper.current
val coroutineScope = rememberCoroutineScope()
val colorScheme = MaterialTheme.colorScheme
val lifecycleOwner = LocalLifecycleOwner.current
val autoRefreshChannels by preferenceOf(PreferencesKeys.AUTO_REFRESH_CHANNELS)
var rowCount by mutablePreferenceOf(PreferencesKeys.ROW_COUNT)
var godMode by mutablePreferenceOf(PreferencesKeys.GOD_MODE)
val zapping by viewModel.zapping.collectAsStateWithLifecycle()
val playlistUrl by viewModel.playlistUrl.collectAsStateWithLifecycle()
val playlist by viewModel.playlist.collectAsStateWithLifecycle()
@ -199,8 +201,8 @@ internal fun PlaylistRoute(
}
}
LaunchedEffect(preferences.autoRefreshChannels, playlistUrl) {
if (playlistUrl.isNotEmpty() && preferences.autoRefreshChannels) {
LaunchedEffect(autoRefreshChannels, playlistUrl) {
if (playlistUrl.isNotEmpty() && autoRefreshChannels) {
viewModel.refresh()
}
}
@ -213,7 +215,7 @@ internal fun PlaylistRoute(
title = playlist?.title.orEmpty(),
query = query,
onQuery = { viewModel.query.value = it },
rowCount = preferences.rowCount,
rowCount = rowCount,
zapping = zapping,
categoryWithChannels = channels,
pinnedCategories = pinnedCategories,
@ -275,14 +277,14 @@ internal fun PlaylistRoute(
},
modifier = Modifier
.fillMaxSize()
.thenIf(preferences.godMode) {
.thenIf(godMode) {
Modifier.interceptVolumeEvent { event ->
preferences.rowCount = when (event) {
rowCount = when (event) {
KeyEvent.KEYCODE_VOLUME_UP ->
(preferences.rowCount - 1).coerceAtLeast(1)
(rowCount - 1).coerceAtLeast(1)
KeyEvent.KEYCODE_VOLUME_DOWN ->
(preferences.rowCount + 1).coerceAtMost(2)
(rowCount + 1).coerceAtMost(2)
else -> return@interceptVolumeEvent
}

View File

@ -13,17 +13,20 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.paging.compose.collectAsLazyPagingItems
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.Programme
import com.m3u.business.playlist.PlaylistViewModel
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.foundation.components.CircularProgressIndicator
import com.m3u.smartphone.ui.material.components.VerticalDraggableScrollbar
import com.m3u.smartphone.ui.material.ktx.plus
@ -48,12 +51,17 @@ internal fun ChannelGallery(
contentPadding: PaddingValues = PaddingValues(0.dp),
) {
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val actualRowCount = when {
preferences.noPictureMode -> rowCount
isVodOrSeriesPlaylist -> rowCount + 2
else -> rowCount
val noPictureMode by preferenceOf(PreferencesKeys.NO_PICTURE_MODE)
val actualRowCount by remember(isVodOrSeriesPlaylist, rowCount) {
derivedStateOf {
when {
noPictureMode -> rowCount
isVodOrSeriesPlaylist -> rowCount + 2
else -> rowCount
}
}
}
val channels = categoryWithChannels?.channels?.collectAsLazyPagingItems()

View File

@ -21,6 +21,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.movableContentOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -41,7 +42,8 @@ import coil.compose.AsyncImage
import coil.compose.SubcomposeAsyncImage
import coil.request.ImageRequest
import coil.size.Size
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.foundation.components.CircularProgressIndicator
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.Programme
@ -73,14 +75,13 @@ internal fun ChannelItem(
) {
val context = LocalContext.current
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val favourite = channel.favourite
val recentlyString = stringResource(string.ui_sort_recently)
val neverPlayedString = stringResource(string.ui_sort_never_played)
val noPictureMode = preferences.noPictureMode
val noPictureMode by preferenceOf(PreferencesKeys.NO_PICTURE_MODE)
val star = remember(favourite) {
movableContentOf {
@ -238,8 +239,8 @@ internal fun ChannelItem(
internal fun Programme.readText(
timeColor: Color = MaterialTheme.colorScheme.secondary
): AnnotatedString = buildAnnotatedString {
val preferences = hiltPreferences()
val clockMode = preferences.twelveHourClock
val clockMode by preferenceOf(PreferencesKeys.CLOCK_MODE)
val start = Instant.fromEpochMilliseconds(start)
.toLocalDateTime(TimeZone.currentSystemDefault())
.formatEOrSh(clockMode)

View File

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ChangeCircle
import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
@ -31,7 +30,8 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.m3u.business.setting.BackingUpAndRestoringState
import com.m3u.business.setting.SettingViewModel
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.util.basic.title
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.ColorScheme
@ -43,7 +43,6 @@ import com.m3u.smartphone.ui.business.setting.fragments.AppearanceFragment
import com.m3u.smartphone.ui.business.setting.fragments.OptionalFragment
import com.m3u.smartphone.ui.business.setting.fragments.SubscriptionsFragment
import com.m3u.smartphone.ui.business.setting.fragments.preferences.PreferencesFragment
import com.m3u.smartphone.ui.common.helper.Action
import com.m3u.smartphone.ui.common.helper.Fob
import com.m3u.smartphone.ui.common.helper.Metadata
import com.m3u.smartphone.ui.common.internal.Events
@ -57,7 +56,6 @@ import kotlinx.coroutines.launch
@Composable
fun SettingRoute(
contentPadding: PaddingValues,
navigateToExtension: () -> Unit,
modifier: Modifier = Modifier,
viewModel: SettingViewModel = hiltViewModel()
) {
@ -125,7 +123,6 @@ fun SettingRoute(
onDeleteEpgPlaylist = { viewModel.deleteEpgPlaylist(it) },
modifier = modifier.fillMaxSize(),
contentPadding = contentPadding,
navigateToExtension = navigateToExtension,
)
CanvasBottomSheet(
sheetState = sheetState,
@ -167,19 +164,17 @@ private fun SettingScreen(
restoreSchemes: () -> Unit,
epgs: List<Playlist>,
onDeleteEpgPlaylist: (String) -> Unit,
navigateToExtension: () -> Unit,
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues()
) {
val coroutineScope = rememberCoroutineScope()
val preferences = hiltPreferences()
val defaultTitle = stringResource(string.ui_title_setting)
val playlistTitle = stringResource(string.feat_setting_playlist_management)
val appearanceTitle = stringResource(string.feat_setting_appearance)
val optionalTitle = stringResource(string.feat_setting_optional_features)
val colorArgb = preferences.argb
val colorArgb by preferenceOf(PreferencesKeys.COLOR_ARGB)
val navigator = rememberListDetailPaneScaffoldNavigator<SettingDestination>()
val destination = navigator.currentDestination?.contentKey ?: SettingDestination.Default
@ -210,17 +205,8 @@ private fun SettingScreen(
}
}
}
Metadata.actions = listOf(
Action(
icon = Icons.Rounded.Extension,
contentDescription = "extension"
) {
navigateToExtension()
}
)
onPauseOrDispose {
Metadata.fob = null
Metadata.actions = emptyList()
}
}

View File

@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@ -31,11 +30,14 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.m3u.core.architecture.preferences.ClipMode
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.mutablePreferenceOf
import com.m3u.core.util.basic.title
import com.m3u.data.database.model.ColorScheme
import com.m3u.i18n.R.string
@ -46,8 +48,6 @@ import com.m3u.smartphone.ui.material.components.TextPreference
import com.m3u.smartphone.ui.material.components.ThemeSelection
import com.m3u.smartphone.ui.material.ktx.Edge
import com.m3u.smartphone.ui.material.ktx.blurEdges
import com.m3u.smartphone.ui.material.ktx.minus
import com.m3u.smartphone.ui.material.ktx.only
import com.m3u.smartphone.ui.material.ktx.plus
import com.m3u.smartphone.ui.material.model.LocalSpacing
@ -61,10 +61,16 @@ internal fun AppearanceFragment(
contentPadding: PaddingValues = PaddingValues()
) {
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val isDarkMode = preferences.darkMode
val useDynamicColors = preferences.useDynamicColors
var isDarkMode by mutablePreferenceOf(PreferencesKeys.DARK_MODE)
var useDynamicColors by mutablePreferenceOf(PreferencesKeys.USE_DYNAMIC_COLORS)
var argb by mutablePreferenceOf(PreferencesKeys.COLOR_ARGB)
var clipMode by mutablePreferenceOf(PreferencesKeys.CLIP_MODE)
var compactDimension by mutablePreferenceOf(PreferencesKeys.COMPACT_DIMENSION)
var noPictureMode by mutablePreferenceOf(PreferencesKeys.NO_PICTURE_MODE)
var followSystemTheme by mutablePreferenceOf(PreferencesKeys.FOLLOW_SYSTEM_THEME)
var colorfulBackground by mutablePreferenceOf(PreferencesKeys.COLORFUL_BACKGROUND)
var godMode by mutablePreferenceOf(PreferencesKeys.GOD_MODE)
val colorScheme = MaterialTheme.colorScheme
@ -137,9 +143,9 @@ internal fun AppearanceFragment(
isDark = colorScheme.isDark,
selected = selected,
onClick = {
preferences.useDynamicColors = false
preferences.argb = colorScheme.argb
preferences.darkMode = colorScheme.isDark
useDynamicColors = false
argb = colorScheme.argb
isDarkMode = colorScheme.isDark
},
onLongClick = { openColorCanvas(colorScheme) },
)
@ -168,14 +174,14 @@ internal fun AppearanceFragment(
TextPreference(
title = stringResource(string.feat_setting_clip_mode).title(),
icon = Icons.Rounded.FitScreen,
trailing = when (preferences.clipMode) {
trailing = when (clipMode) {
ClipMode.ADAPTIVE -> stringResource(string.feat_setting_clip_mode_adaptive)
ClipMode.CLIP -> stringResource(string.feat_setting_clip_mode_clip)
ClipMode.STRETCHED -> stringResource(string.feat_setting_clip_mode_stretched)
else -> ""
}.title(),
onClick = {
preferences.clipMode = when (preferences.clipMode) {
clipMode = when (clipMode) {
ClipMode.ADAPTIVE -> ClipMode.CLIP
ClipMode.CLIP -> ClipMode.STRETCHED
ClipMode.STRETCHED -> ClipMode.ADAPTIVE
@ -188,8 +194,8 @@ internal fun AppearanceFragment(
SwitchSharedPreference(
title = string.feat_setting_compact_dimension,
icon = Icons.Rounded.FormatSize,
checked = preferences.compactDimension,
onChanged = { preferences.compactDimension = !preferences.compactDimension }
checked = compactDimension,
onChanged = { compactDimension = !compactDimension }
)
}
item {
@ -197,16 +203,16 @@ internal fun AppearanceFragment(
title = string.feat_setting_no_picture_mode,
content = string.feat_setting_no_picture_mode_description,
icon = Icons.Rounded.HideImage,
checked = preferences.noPictureMode,
onChanged = { preferences.noPictureMode = !preferences.noPictureMode }
checked = noPictureMode,
onChanged = { noPictureMode = !noPictureMode }
)
}
item {
SwitchSharedPreference(
title = string.feat_setting_follow_system_theme,
icon = Icons.Rounded.DarkMode,
checked = preferences.followSystemTheme,
onChanged = { preferences.followSystemTheme = !preferences.followSystemTheme },
checked = followSystemTheme,
onChanged = { followSystemTheme = !followSystemTheme },
)
}
item {
@ -217,7 +223,7 @@ internal fun AppearanceFragment(
content = string.feat_setting_use_dynamic_colors_unavailable.takeUnless { useDynamicColorsAvailable },
icon = Icons.Rounded.ColorLens,
checked = useDynamicColors,
onChanged = { preferences.useDynamicColors = !useDynamicColors },
onChanged = { useDynamicColors = !useDynamicColors },
enabled = useDynamicColorsAvailable
)
}
@ -225,9 +231,9 @@ internal fun AppearanceFragment(
SwitchSharedPreference(
title = string.feat_setting_colorful_background,
icon = Icons.Rounded.Stars,
checked = preferences.colorfulBackground,
checked = colorfulBackground,
onChanged = {
preferences.colorfulBackground = !preferences.colorfulBackground
colorfulBackground = !colorfulBackground
}
)
}
@ -243,8 +249,8 @@ internal fun AppearanceFragment(
title = string.feat_setting_god_mode,
content = string.feat_setting_god_mode_description,
icon = Icons.Rounded.DeviceHub,
checked = preferences.godMode,
onChanged = { preferences.godMode = !preferences.godMode }
checked = godMode,
onChanged = { godMode = !godMode }
)
}
}

View File

@ -16,7 +16,6 @@ import androidx.compose.material.icons.rounded.PictureInPicture
import androidx.compose.material.icons.rounded.Recommend
import androidx.compose.material.icons.rounded.Refresh
import androidx.compose.material.icons.rounded.ReplayCircleFilled
import androidx.compose.material.icons.rounded.Save
import androidx.compose.material.icons.rounded.ScreenRotation
import androidx.compose.material.icons.rounded.SettingsEthernet
import androidx.compose.material.icons.rounded.SettingsRemote
@ -24,20 +23,24 @@ import androidx.compose.material.icons.rounded.Sync
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Unarchive
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.m3u.core.architecture.preferences.ConnectTimeout
import com.m3u.core.architecture.preferences.PlaylistStrategy
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.ReconnectMode
import com.m3u.core.architecture.preferences.UnseensMilliseconds
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.mutablePreferenceOf
import com.m3u.core.util.basic.title
import com.m3u.i18n.R.string
import com.m3u.smartphone.ui.business.setting.components.SwitchSharedPreference
import com.m3u.smartphone.ui.material.components.TextPreference
import com.m3u.smartphone.ui.material.ktx.plus
import com.m3u.smartphone.ui.material.model.LocalSpacing
import com.m3u.smartphone.ui.business.setting.components.SwitchSharedPreference
import kotlin.time.DurationUnit
import kotlin.time.toDuration
@ -47,119 +50,120 @@ internal fun OptionalFragment(
modifier: Modifier = Modifier
) {
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing.small),
contentPadding = contentPadding + PaddingValues(spacing.medium),
modifier = modifier.fillMaxSize()
) {
item {
var tunneling by mutablePreferenceOf(PreferencesKeys.TUNNELING)
SwitchSharedPreference(
title = string.feat_setting_tunneling,
content = string.feat_setting_tunneling_description,
icon = Icons.Rounded.FlashOn,
checked = preferences.tunneling,
onChanged = { preferences.tunneling = !preferences.tunneling }
checked = tunneling,
onChanged = { tunneling = !tunneling }
)
}
item {
var fullInfoPlayer by mutablePreferenceOf(PreferencesKeys.FULL_INFO_PLAYER)
SwitchSharedPreference(
title = string.feat_setting_full_info_player,
content = string.feat_setting_full_info_player_description,
icon = Icons.Rounded.Details,
checked = preferences.fullInfoPlayer,
onChanged = { preferences.fullInfoPlayer = !preferences.fullInfoPlayer }
checked = fullInfoPlayer,
onChanged = { fullInfoPlayer = !fullInfoPlayer }
)
}
item {
var slider by mutablePreferenceOf(PreferencesKeys.SLIDER)
SwitchSharedPreference(
title = string.feat_setting_slider,
icon = Icons.Rounded.SettingsEthernet,
checked = preferences.slider,
onChanged = { preferences.slider = !preferences.slider }
checked = slider,
onChanged = { slider = !slider }
)
}
item {
var zappingMode by mutablePreferenceOf(PreferencesKeys.ZAPPING_MODE)
SwitchSharedPreference(
title = string.feat_setting_zapping_mode,
content = string.feat_setting_zapping_mode_description,
icon = Icons.Rounded.PictureInPicture,
checked = preferences.zappingMode,
onChanged = { preferences.zappingMode = !preferences.zappingMode }
checked = zappingMode,
onChanged = { zappingMode = !zappingMode }
)
}
item {
var brightnessGesture by mutablePreferenceOf(PreferencesKeys.BRIGHTNESS_GESTURE)
SwitchSharedPreference(
title = string.feat_setting_gesture_brightness,
icon = Icons.Rounded.BrightnessMedium,
checked = preferences.brightnessGesture,
onChanged = { preferences.brightnessGesture = !preferences.brightnessGesture }
checked = brightnessGesture,
onChanged = { brightnessGesture = !brightnessGesture }
)
}
item {
var volumeGesture by mutablePreferenceOf(PreferencesKeys.VOLUME_GESTURE)
SwitchSharedPreference(
title = string.feat_setting_gesture_volume,
icon = Icons.AutoMirrored.Rounded.VolumeUp,
checked = preferences.volumeGesture,
onChanged = { preferences.volumeGesture = !preferences.volumeGesture }
checked = volumeGesture,
onChanged = { volumeGesture = !volumeGesture }
)
}
item {
var alwaysShowReplay by mutablePreferenceOf(PreferencesKeys.ALWAYS_SHOW_REPLAY)
SwitchSharedPreference(
title = string.feat_setting_always_replay,
icon = Icons.Rounded.ReplayCircleFilled,
checked = preferences.alwaysShowReplay,
onChanged = { preferences.alwaysShowReplay = !preferences.alwaysShowReplay }
checked = alwaysShowReplay,
onChanged = { alwaysShowReplay = !alwaysShowReplay }
)
}
item {
var panel by mutablePreferenceOf(PreferencesKeys.PLAYER_PANEL)
SwitchSharedPreference(
title = string.feat_setting_player_panel,
content = string.feat_setting_player_panel_description,
icon = Icons.Rounded.Unarchive,
checked = preferences.panel,
onChanged = { preferences.panel = !preferences.panel }
)
}
item {
SwitchSharedPreference(
title = string.feat_setting_cache,
content = string.feat_setting_cache_description,
checked = preferences.cache,
icon = Icons.Rounded.Save,
onChanged = { preferences.cache = !preferences.cache }
checked = panel,
onChanged = { panel = !panel }
)
}
item {
var screenRotating by mutablePreferenceOf(PreferencesKeys.SCREEN_ROTATING)
SwitchSharedPreference(
title = string.feat_setting_screen_rotating,
content = string.feat_setting_screen_rotating_description,
icon = Icons.Rounded.ScreenRotation,
checked = preferences.screenRotating,
onChanged = { preferences.screenRotating = !preferences.screenRotating }
checked = screenRotating,
onChanged = { screenRotating = !screenRotating }
)
}
item {
var screencast by mutablePreferenceOf(PreferencesKeys.SCREENCAST)
SwitchSharedPreference(
title = string.feat_setting_screencast,
icon = Icons.Rounded.Cast,
checked = preferences.screencast,
onChanged = { preferences.screencast = !preferences.screencast }
checked = screencast,
onChanged = { screencast = !screencast }
)
}
item {
var reconnectMode by mutablePreferenceOf(PreferencesKeys.RECONNECT_MODE)
TextPreference(
title = stringResource(string.feat_setting_reconnect_mode).title(),
icon = Icons.Rounded.Loop,
trailing = when (preferences.reconnectMode) {
trailing = when (reconnectMode) {
ReconnectMode.RETRY -> stringResource(string.feat_setting_reconnect_mode_retry)
ReconnectMode.RECONNECT -> stringResource(string.feat_setting_reconnect_mode_reconnect)
else -> stringResource(string.feat_setting_reconnect_mode_no)
},
onClick = {
preferences.reconnectMode = when (preferences.reconnectMode) {
reconnectMode = when (reconnectMode) {
ReconnectMode.RETRY -> ReconnectMode.RECONNECT
ReconnectMode.RECONNECT -> ReconnectMode.NO
else -> ReconnectMode.RETRY
@ -168,16 +172,17 @@ internal fun OptionalFragment(
)
}
item {
var playlistStrategy by mutablePreferenceOf(PreferencesKeys.PLAYLIST_STRATEGY)
TextPreference(
title = stringResource(string.feat_setting_sync_mode).title(),
icon = Icons.Rounded.Sync,
trailing = when (preferences.playlistStrategy) {
trailing = when (playlistStrategy) {
PlaylistStrategy.ALL -> stringResource(string.feat_setting_sync_mode_all)
PlaylistStrategy.KEEP -> stringResource(string.feat_setting_sync_mode_keep)
else -> ""
}.title(),
onClick = {
preferences.playlistStrategy = when (preferences.playlistStrategy) {
playlistStrategy = when (playlistStrategy) {
PlaylistStrategy.ALL -> PlaylistStrategy.KEEP
else -> PlaylistStrategy.ALL
}
@ -185,12 +190,13 @@ internal fun OptionalFragment(
)
}
item {
var connectTimeout by mutablePreferenceOf(PreferencesKeys.CONNECT_TIMEOUT)
TextPreference(
title = stringResource(string.feat_setting_connect_timeout).title(),
icon = Icons.Rounded.Timer,
trailing = "${preferences.connectTimeout / 1000}s",
trailing = "${connectTimeout / 1000}s",
onClick = {
preferences.connectTimeout = when (preferences.connectTimeout) {
connectTimeout = when (connectTimeout) {
ConnectTimeout.LONG -> ConnectTimeout.SHORT
ConnectTimeout.SHORT -> ConnectTimeout.LONG
else -> ConnectTimeout.SHORT
@ -199,21 +205,22 @@ internal fun OptionalFragment(
)
}
item {
val unseensMilliseconds = preferences.unseensMilliseconds
val unseensMillisecondsText = remember(unseensMilliseconds) {
val duration = unseensMilliseconds
.toDuration(DurationUnit.MILLISECONDS)
if (unseensMilliseconds > UnseensMilliseconds.DAYS_30) "Never"
else duration
.toString()
.title()
var unseensMilliseconds by mutablePreferenceOf(PreferencesKeys.UNSEENS_MILLISECONDS)
val unseensMillisecondsText by remember {
derivedStateOf {
val duration = unseensMilliseconds.toDuration(DurationUnit.MILLISECONDS)
if (unseensMilliseconds > UnseensMilliseconds.DAYS_30) "Never"
else duration
.toString()
.title()
}
}
TextPreference(
title = stringResource(string.feat_setting_unseen_limit).title(),
icon = Icons.Rounded.Recommend,
trailing = unseensMillisecondsText,
onClick = {
preferences.unseensMilliseconds = when (unseensMilliseconds) {
unseensMilliseconds = when (unseensMilliseconds) {
UnseensMilliseconds.DAYS_3 -> UnseensMilliseconds.DAYS_7
UnseensMilliseconds.DAYS_7 -> UnseensMilliseconds.DAYS_30
UnseensMilliseconds.DAYS_30 -> UnseensMilliseconds.NEVER
@ -223,30 +230,33 @@ internal fun OptionalFragment(
)
}
item {
var autoRefreshChannels by mutablePreferenceOf(PreferencesKeys.AUTO_REFRESH_CHANNELS)
SwitchSharedPreference(
title = string.feat_setting_auto_refresh_channels,
content = string.feat_setting_auto_refresh_channels_description,
icon = Icons.Rounded.Refresh,
checked = preferences.autoRefreshChannels,
onChanged = { preferences.autoRefreshChannels = !preferences.autoRefreshChannels }
checked = autoRefreshChannels,
onChanged = { autoRefreshChannels = !autoRefreshChannels }
)
}
item {
var twelveHourClock by mutablePreferenceOf(PreferencesKeys.CLOCK_MODE)
SwitchSharedPreference(
title = string.feat_setting_epg_clock_mode,
icon = Icons.Rounded.AccessTime,
checked = preferences.twelveHourClock,
onChanged = { preferences.twelveHourClock = !preferences.twelveHourClock }
checked = twelveHourClock,
onChanged = { twelveHourClock = !twelveHourClock }
)
}
item {
var remoteControl by mutablePreferenceOf(PreferencesKeys.REMOTE_CONTROL)
SwitchSharedPreference(
title = string.feat_setting_remote_control,
content = string.feat_setting_remote_control_description,
icon = Icons.Rounded.SettingsRemote,
checked = preferences.remoteControl,
onChanged = { preferences.remoteControl = !preferences.remoteControl }
checked = remoteControl,
onChanged = { remoteControl = !remoteControl }
)
}
}

View File

@ -33,7 +33,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.res.stringResource
import com.google.accompanist.permissions.rememberPermissionState
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.data.database.model.DataSource
import com.m3u.data.database.model.Playlist
import com.m3u.data.database.model.Channel
@ -41,6 +40,9 @@ import com.m3u.business.setting.BackingUpAndRestoringState
import com.m3u.i18n.R.string
import com.m3u.smartphone.ui.material.components.HorizontalPagerIndicator
import androidx.compose.material3.Icon
import androidx.compose.runtime.getValue
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.smartphone.ui.material.components.PlaceholderField
import com.m3u.smartphone.ui.material.ktx.checkPermissionOrRationale
import com.m3u.smartphone.ui.material.ktx.textHorizontalLabel
@ -167,11 +169,10 @@ private fun MainContentImpl(
modifier: Modifier = Modifier
) {
val spacing = LocalSpacing.current
val preferences = hiltPreferences()
val clipboardManager = LocalClipboardManager.current
val helper = LocalHelper.current
val remoteControl = preferences.remoteControl
val remoteControl by preferenceOf(PreferencesKeys.REMOTE_CONTROL)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(spacing.small),

View File

@ -8,18 +8,18 @@ import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import com.m3u.business.playlist.configuration.navigateToPlaylistConfiguration
import com.m3u.business.playlist.navigateToPlaylist
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.core.wrapper.eventOf
import com.m3u.smartphone.ui.business.channel.PlayerActivity
import com.m3u.smartphone.ui.business.configuration.playlistConfigurationScreen
import com.m3u.smartphone.ui.business.extension.extensionScreen
import com.m3u.smartphone.ui.business.extension.navigateToExtension
import com.m3u.smartphone.ui.business.playlist.playlistScreen
import com.m3u.smartphone.ui.common.internal.Events
import com.m3u.smartphone.ui.material.components.Destination
@ -35,7 +35,8 @@ fun AppNavHost(
startDestination: String = Destination.Root.Foryou.name
) {
val context = LocalContext.current
val preferences = hiltPreferences()
val zappingMode by preferenceOf(PreferencesKeys.ZAPPING_MODE)
NavHost(
navController = navController,
@ -56,14 +57,11 @@ fun AppNavHost(
},
navigateToPlaylistConfiguration = {
navController.navigateToPlaylistConfiguration(it.url)
},
navigateToExtension = {
navController.navigateToExtension()
}
)
playlistScreen(
navigateToChannel = {
if (preferences.zappingMode && PlayerActivity.isInPipMode) return@playlistScreen
if (zappingMode && PlayerActivity.isInPipMode) return@playlistScreen
val options = ActivityOptions.makeCustomAnimation(
context,
0,
@ -79,6 +77,5 @@ fun AppNavHost(
contentPadding = contentPadding
)
playlistConfigurationScreen(contentPadding)
extensionScreen(contentPadding)
}
}

View File

@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import com.m3u.data.database.model.Playlist
import com.m3u.smartphone.ui.business.extension.ExtensionRoute
import com.m3u.smartphone.ui.material.ktx.Edge
import com.m3u.smartphone.ui.material.ktx.blurEdge
import com.m3u.smartphone.ui.business.favourite.FavoriteRoute
@ -20,7 +21,6 @@ fun NavGraphBuilder.rootGraph(
contentPadding: PaddingValues,
navigateToPlaylist: (Playlist) -> Unit,
navigateToChannel: () -> Unit,
navigateToExtension: () -> Unit,
navigateToSettingPlaylistManagement: () -> Unit,
navigateToPlaylistConfiguration: (Playlist) -> Unit,
) {
@ -60,6 +60,22 @@ fun NavGraphBuilder.rootGraph(
)
}
composable(
route = Destination.Root.Extension.name,
enterTransition = { fadeIn() },
exitTransition = { fadeOut() }
) {
ExtensionRoute(
contentPadding = contentPadding,
modifier = Modifier
.fillMaxSize()
.blurEdge(
edge = Edge.Bottom,
color = MaterialTheme.colorScheme.background
)
)
}
composable(
route = Destination.Root.Setting.name,
enterTransition = { fadeIn() },
@ -67,7 +83,6 @@ fun NavGraphBuilder.rootGraph(
) {
SettingRoute(
contentPadding = contentPadding,
navigateToExtension = navigateToExtension,
modifier = Modifier
.fillMaxSize()
.blurEdge(

View File

@ -31,7 +31,8 @@ import androidx.graphics.shapes.CornerRounding
import androidx.graphics.shapes.RoundedPolygon
import androidx.graphics.shapes.star
import androidx.graphics.shapes.toPath
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
data class StarSpec(
val numVertices: Int,
@ -87,10 +88,12 @@ fun StarBackground(
modifier: Modifier = Modifier,
colors: StarColors = StarColors.defaults(),
) {
val preferences = hiltPreferences()
val colorfulBackground by preferenceOf(PreferencesKeys.COLORFUL_BACKGROUND)
val specs = remember(colors) { createStarSpecs(colors) }
AnimatedVisibility(
visible = preferences.colorfulBackground,
visible = colorfulBackground,
enter = fadeIn() + scaleIn(initialScale = 2.3f),
exit = fadeOut() + scaleOut(targetScale = 2.3f),
modifier = modifier

View File

@ -5,8 +5,11 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.preferenceOf
import com.m3u.smartphone.ui.common.helper.Helper
import com.m3u.smartphone.ui.common.helper.LocalHelper
import com.m3u.smartphone.ui.material.LocalM3UHapticFeedback
@ -24,19 +27,29 @@ fun Toolkit(
alwaysUseDarkTheme: Boolean = false,
content: @Composable () -> Unit
) {
val preferences = hiltPreferences()
val prevTypography = MaterialTheme.typography
val smartphoneTypography: Material3Typography = remember(prevTypography) {
prevTypography.withFontFamily(FontFamilies.GoogleSans)
}
val useDarkTheme = when {
alwaysUseDarkTheme -> true
preferences.followSystemTheme -> isSystemInDarkTheme()
else -> preferences.darkMode
val followSystemTheme by preferenceOf(PreferencesKeys.FOLLOW_SYSTEM_THEME)
val darkMode by preferenceOf(PreferencesKeys.DARK_MODE)
val compactDimension by preferenceOf(PreferencesKeys.COMPACT_DIMENSION)
val argb by preferenceOf(PreferencesKeys.COLOR_ARGB)
val useDynamicColors by preferenceOf(PreferencesKeys.USE_DYNAMIC_COLORS)
val isSystemInDarkTheme = isSystemInDarkTheme()
val useDarkTheme by remember {
derivedStateOf {
when {
alwaysUseDarkTheme -> true
followSystemTheme -> isSystemInDarkTheme
else -> darkMode
}
}
}
val spacing = if (preferences.compactDimension) Spacing.COMPACT
val spacing = if (compactDimension) Spacing.COMPACT
else Spacing.REGULAR
CompositionLocalProvider(
LocalHelper provides helper,
@ -44,9 +57,9 @@ fun Toolkit(
LocalSpacing provides spacing
) {
Theme(
argb = preferences.argb,
argb = argb,
useDarkTheme = useDarkTheme,
useDynamicColors = preferences.useDynamicColors,
useDynamicColors = useDynamicColors,
typography = smartphoneTypography
) {
LaunchedEffect(useDarkTheme) {

View File

@ -4,9 +4,11 @@ import android.os.Parcelable
import androidx.annotation.StringRes
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Collections
import androidx.compose.material.icons.outlined.Extension
import androidx.compose.material.icons.outlined.Home
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.rounded.Collections
import androidx.compose.material.icons.rounded.Extension
import androidx.compose.material.icons.rounded.Home
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.runtime.Immutable
@ -32,6 +34,11 @@ sealed interface Destination {
unselectedIcon = Icons.Outlined.Collections,
iconTextId = string.ui_destination_favourite
),
Extension(
selectedIcon = Icons.Rounded.Extension,
unselectedIcon = Icons.Outlined.Extension,
iconTextId = string.ui_destination_extension
),
Setting(
selectedIcon = Icons.Rounded.Settings,
unselectedIcon = Icons.Outlined.Settings,

View File

@ -12,9 +12,11 @@ import androidx.lifecycle.viewModelScope
import com.m3u.extension.api.CallTokenConst
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import java.util.UUID
import javax.inject.Inject
@ -63,6 +65,7 @@ class ExtensionViewModel @Inject constructor(
.toList()
emit(extensions)
}
.flowOn(Dispatchers.Default)
.stateIn(
scope = viewModelScope,
initialValue = emptyList(),

View File

@ -3,7 +3,6 @@ package com.m3u.business.favorite
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.compose.runtime.snapshotFlow
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
@ -18,7 +17,9 @@ import com.m3u.core.Contracts
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asStateFlow
import com.m3u.core.wrapper.Resource
import com.m3u.core.wrapper.Sort
import com.m3u.core.wrapper.mapResource
@ -50,13 +51,13 @@ class FavoriteViewModel @Inject constructor(
private val channelRepository: ChannelRepository,
private val mediaRepository: MediaRepository,
private val playerManager: PlayerManager,
preferences: Preferences,
settings: Settings,
delegate: Logger
) : ViewModel() {
private val logger = delegate.install(Profiles.VIEWMODEL_FAVOURITE)
val zapping: StateFlow<Channel?> = combine(
snapshotFlow { preferences.zappingMode },
settings.asStateFlow(PreferencesKeys.ZAPPING_MODE),
playerManager.channel
) { zappingMode, channel ->
channel.takeIf { zappingMode }

View File

@ -1,6 +1,5 @@
package com.m3u.business.foryou
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
@ -9,7 +8,9 @@ import androidx.work.WorkQuery
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asStateFlow
import com.m3u.core.wrapper.Resource
import com.m3u.core.wrapper.asResource
import com.m3u.core.wrapper.mapResource
@ -47,7 +48,7 @@ class ForyouViewModel @Inject constructor(
channelRepository: ChannelRepository,
programmeRepository: ProgrammeRepository,
private val playerManager: PlayerManager,
preferences: Preferences,
settings: Settings,
workManager: WorkManager,
delegate: Logger
) : ViewModel() {
@ -83,7 +84,7 @@ class ForyouViewModel @Inject constructor(
val refreshingEpgUrls: Flow<List<String>> = programmeRepository.refreshingEpgUrls
private val unseensDuration = snapshotFlow { preferences.unseensMilliseconds }
private val unseensDuration = settings.asStateFlow(PreferencesKeys.UNSEENS_MILLISECONDS)
.map { it.toDuration(DurationUnit.MILLISECONDS) }
.stateIn(
scope = viewModelScope,

View File

@ -5,7 +5,6 @@ import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.snapshotFlow
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
@ -20,14 +19,18 @@ import androidx.paging.cachedIn
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.m3u.business.playlist.PlaylistMessage.ChannelCoverSaved
import com.m3u.core.Contracts
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asStateFlow
import com.m3u.core.util.coroutine.flatmapCombined
import com.m3u.core.wrapper.Event
import com.m3u.core.wrapper.Resource
import com.m3u.core.wrapper.Sort
import com.m3u.core.wrapper.handledEvent
import com.m3u.core.wrapper.mapResource
import com.m3u.core.wrapper.resource
@ -36,16 +39,14 @@ import com.m3u.data.database.model.Playlist
import com.m3u.data.database.model.Programme
import com.m3u.data.database.model.isSeries
import com.m3u.data.parser.xtream.XtreamChannelInfo
import com.m3u.data.repository.channel.ChannelRepository
import com.m3u.data.repository.media.MediaRepository
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.repository.channel.ChannelRepository
import com.m3u.data.repository.programme.ProgrammeRepository
import com.m3u.data.service.MediaCommand
import com.m3u.data.service.Messager
import com.m3u.data.service.PlayerManager
import com.m3u.data.worker.SubscriptionWorker
import com.m3u.business.playlist.PlaylistMessage.ChannelCoverSaved
import com.m3u.core.wrapper.Sort
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
@ -80,7 +81,7 @@ class PlaylistViewModel @Inject constructor(
private val programmeRepository: ProgrammeRepository,
private val messager: Messager,
private val playerManager: PlayerManager,
preferences: Preferences,
settings: Settings,
workManager: WorkManager,
delegate: Logger
) : ViewModel() {
@ -99,7 +100,7 @@ class PlaylistViewModel @Inject constructor(
)
val zapping: StateFlow<Channel?> = combine(
snapshotFlow { preferences.zappingMode },
settings.asStateFlow(PreferencesKeys.ZAPPING_MODE),
playerManager.channel,
playlistUrl.flatMapLatest { channelRepository.observeAllByPlaylistUrl(it) }
) { zappingMode, channel, channels ->
@ -361,6 +362,7 @@ class PlaylistViewModel @Inject constructor(
suspend fun reloadThumbnail(channelUrl: String): Uri? {
return playerManager.reloadThumbnail(channelUrl)
}
suspend fun syncThumbnail(channelUrl: String): Uri? {
return playerManager.syncThumbnail(channelUrl)
}

View File

@ -3,7 +3,6 @@ package com.m3u.business.setting
import android.net.Uri
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshotFlow
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.OneTimeWorkRequestBuilder
@ -16,18 +15,21 @@ import com.m3u.core.architecture.Publisher
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asStateFlow
import com.m3u.core.architecture.preferences.set
import com.m3u.core.util.basic.startWithHttpScheme
import com.m3u.data.api.TvApiDelegate
import com.m3u.data.database.dao.ColorSchemeDao
import com.m3u.data.database.example.ColorSchemeExample
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.ColorScheme
import com.m3u.data.database.model.DataSource
import com.m3u.data.database.model.Playlist
import com.m3u.data.database.model.Channel
import com.m3u.data.parser.xtream.XtreamInput
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.repository.channel.ChannelRepository
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.service.Messager
import com.m3u.data.worker.BackupWorker
import com.m3u.data.worker.RestoreWorker
@ -51,7 +53,7 @@ class SettingViewModel @Inject constructor(
private val playlistRepository: PlaylistRepository,
private val channelRepository: ChannelRepository,
private val workManager: WorkManager,
private val preferences: Preferences,
private val settings: Settings,
private val messager: Messager,
private val tvApi: TvApiDelegate,
publisher: Publisher,
@ -100,7 +102,7 @@ class SettingViewModel @Inject constructor(
val colorSchemes: StateFlow<List<ColorScheme>> = combine(
colorSchemeDao.observeAll().catch { emit(emptyList()) },
snapshotFlow { preferences.followSystemTheme }
settings.asStateFlow(PreferencesKeys.FOLLOW_SYSTEM_THEME)
) { all, followSystemTheme -> if (followSystemTheme) all.filter { !it.isDark } else all }
.flowOn(Dispatchers.Default)
.stateIn(
@ -311,8 +313,7 @@ class SettingViewModel @Inject constructor(
argb: Int,
isDark: Boolean
) {
preferences.argb = argb
preferences.darkMode = isDark
settings[PreferencesKeys.DARK_MODE] = isDark
viewModelScope.launch {
if (prev != null) {
colorSchemeDao.delete(prev)

View File

@ -36,4 +36,6 @@ dependencies {
api(libs.androidx.paging.runtime.ktx)
api(libs.androidx.paging.compose)
api("androidx.datastore:datastore-preferences:1.1.4")
}

View File

@ -1,198 +1,170 @@
@file:Suppress("INVISIBLE_REFERENCE", "UNCHECKED_CAST")
package com.m3u.core.architecture.preferences
import android.content.Context
import android.content.SharedPreferences
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.produceState
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalContext
import com.m3u.core.util.context.booleanAsState
import com.m3u.core.util.context.intAsState
import com.m3u.core.util.context.longAsState
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Inject
import javax.inject.Singleton
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import java.util.concurrent.ConcurrentHashMap
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
typealias Settings = DataStore<Preferences>
val Context.settings: Settings by preferencesDataStore("settings")
@Composable
fun hiltPreferences(): Preferences {
val context = LocalContext.current
return remember {
val applicationContext = context.applicationContext ?: throw IllegalStateException()
EntryPointAccessors
.fromApplication<PreferencesEntryPoint>(applicationContext)
.preferences
fun <T> preferenceOf(
key: Preferences.Key<T>,
initial: T = PREFERENCES[key] as T,
dataStore: Settings = LocalContext.current.settings
): State<T> = produceState(initial, key1 = dataStore) {
dataStore.data.map { it[key] ?: initial }.collect {
value = it
}
}
@EntryPoint
@InstallIn(SingletonComponent::class)
private interface PreferencesEntryPoint {
val preferences: Preferences
}
@Stable
@Singleton
class Preferences @Inject constructor(
@ApplicationContext context: Context
) {
private val sharedPreferences: SharedPreferences =
context.getSharedPreferences(SHARED_SETTINGS, Context.MODE_PRIVATE)
@PlaylistStrategy
var playlistStrategy: Int by
sharedPreferences.intAsState(DEFAULT_PLAYLIST_STRATEGY, PLAYLIST_STRATEGY)
var rowCount: Int by
sharedPreferences.intAsState(DEFAULT_ROW_COUNT, ROW_COUNT)
@ConnectTimeout
var connectTimeout: Long by
sharedPreferences.longAsState(DEFAULT_CONNECT_TIMEOUT, CONNECT_TIMEOUT)
var godMode: Boolean by
sharedPreferences.booleanAsState(DEFAULT_GOD_MODE, GOD_MODE)
@ClipMode
var clipMode: Int by
sharedPreferences.intAsState(DEFAULT_CLIP_MODE, CLIP_MODE)
var autoRefreshChannels: Boolean by
sharedPreferences.booleanAsState(DEFAULT_AUTO_REFRESH_CHANNELS, AUTO_REFRESH_CHANNELS)
var fullInfoPlayer: Boolean by
sharedPreferences.booleanAsState(DEFAULT_FULL_INFO_PLAYER, FULL_INFO_PLAYER)
var noPictureMode: Boolean by
sharedPreferences.booleanAsState(DEFAULT_NO_PICTURE_MODE, NO_PICTURE_MODE)
var darkMode: Boolean by
sharedPreferences.booleanAsState(DEFAULT_DARK_MODE, DARK_MODE)
var useDynamicColors: Boolean by
sharedPreferences.booleanAsState(DEFAULT_USE_DYNAMIC_COLORS, USE_DYNAMIC_COLORS)
var followSystemTheme: Boolean by
sharedPreferences.booleanAsState(DEFAULT_FOLLOW_SYSTEM_THEME, FOLLOW_SYSTEM_THEME)
var zappingMode: Boolean by
sharedPreferences.booleanAsState(DEFAULT_ZAPPING_MODE, ZAPPING_MODE)
var brightnessGesture: Boolean by
sharedPreferences.booleanAsState(DEFAULT_BRIGHTNESS_GESTURE, BRIGHTNESS_GESTURE)
var volumeGesture: Boolean by
sharedPreferences.booleanAsState(DEFAULT_VOLUME_GESTURE, VOLUME_GESTURE)
var screencast: Boolean by
sharedPreferences.booleanAsState(DEFAULT_SCREENCAST, SCREENCAST)
var screenRotating: Boolean by
sharedPreferences.booleanAsState(DEFAULT_SCREEN_ROTATING, SCREEN_ROTATING)
@UnseensMilliseconds
var unseensMilliseconds: Long by
sharedPreferences.longAsState(DEFAULT_UNSEENS_MILLISECONDS, UNSEENS_MILLISECONDS)
var reconnectMode: Int by
sharedPreferences.intAsState(DEFAULT_RECONNECT_MODE, RECONNECT_MODE)
var argb: Int by
sharedPreferences.intAsState(DEFAULT_COLOR_ARGB, COLOR_ARGB)
var tunneling: Boolean by
sharedPreferences.booleanAsState(DEFAULT_TUNNELING, TUNNELING)
var remoteControl: Boolean by
sharedPreferences.booleanAsState(DEFAULT_REMOTE_CONTROL, REMOTE_CONTROL)
var twelveHourClock: Boolean by
sharedPreferences.booleanAsState(DEFAULT_12_H_CLOCK_MODE, CLOCK_MODE)
var slider: Boolean by
sharedPreferences.booleanAsState(DEFAULT_SLIDER, SLIDER)
var alwaysShowReplay: Boolean by
sharedPreferences.booleanAsState(DEFAULT_ALWAYS_SHOW_REFRESH, ALWAYS_SHOW_REFRESH)
var paging: Boolean by
sharedPreferences.booleanAsState(DEFAULT_PAGING, PAGING)
var panel: Boolean by sharedPreferences.booleanAsState(DEFAULT_PLAYER_PANEL, PLAYER_PANEL)
var cache: Boolean by sharedPreferences.booleanAsState(DEFAULT_CACHE, CACHE)
var randomlyInFavorite: Boolean by
sharedPreferences.booleanAsState(DEFAULT_RANDOMLY_IN_FAVOURITE, RANDOMLY_IN_FAVOURITE)
var colorfulBackground by
sharedPreferences.booleanAsState(DEFAULT_COLORFUL_BACKGROUND, COLORFUL_BACKGROUND)
var compactDimension by
sharedPreferences.booleanAsState(DEFAULT_COMPACT_DIMENSION, COMPACT_DIMENSION)
companion object {
private const val SHARED_SETTINGS = "shared_settings"
@PlaylistStrategy
const val DEFAULT_PLAYLIST_STRATEGY = PlaylistStrategy.KEEP
const val DEFAULT_ROW_COUNT = 1
@ConnectTimeout
const val DEFAULT_CONNECT_TIMEOUT = ConnectTimeout.SHORT
const val DEFAULT_GOD_MODE = false
@ClipMode
const val DEFAULT_CLIP_MODE = ClipMode.ADAPTIVE
const val DEFAULT_AUTO_REFRESH_CHANNELS = false
const val DEFAULT_FULL_INFO_PLAYER = false
const val DEFAULT_NO_PICTURE_MODE = false
const val DEFAULT_DARK_MODE = true
const val DEFAULT_USE_DYNAMIC_COLORS = false
const val DEFAULT_FOLLOW_SYSTEM_THEME = false
const val DEFAULT_ZAPPING_MODE = false
const val DEFAULT_BRIGHTNESS_GESTURE = true
const val DEFAULT_VOLUME_GESTURE = true
const val DEFAULT_SCREENCAST = true
const val DEFAULT_SCREEN_ROTATING = false
@UnseensMilliseconds
const val DEFAULT_UNSEENS_MILLISECONDS = UnseensMilliseconds.DAYS_3
const val DEFAULT_RECONNECT_MODE = ReconnectMode.NO
const val DEFAULT_COLOR_ARGB = 0x5E6738
const val DEFAULT_TUNNELING = false
const val DEFAULT_REMOTE_CONTROL = false
const val DEFAULT_SLIDER = true
const val DEFAULT_ALWAYS_SHOW_REFRESH = false
const val DEFAULT_PAGING = true
const val DEFAULT_PLAYER_PANEL = true
const val DEFAULT_CACHE = false
const val DEFAULT_RANDOMLY_IN_FAVOURITE = false
const val DEFAULT_12_H_CLOCK_MODE = false
const val DEFAULT_COLORFUL_BACKGROUND = false
const val DEFAULT_COMPACT_DIMENSION = false
const val PLAYLIST_STRATEGY = "playlist-strategy"
const val ROW_COUNT = "rowCount"
const val CONNECT_TIMEOUT = "connect-timeout"
const val GOD_MODE = "god-mode"
const val CLIP_MODE = "clip-mode"
const val AUTO_REFRESH_CHANNELS = "auto-refresh-channels"
const val FULL_INFO_PLAYER = "full-info-player"
const val NO_PICTURE_MODE = "no-picture-mode"
const val DARK_MODE = "dark-mode"
const val USE_DYNAMIC_COLORS = "use-dynamic-colors"
const val FOLLOW_SYSTEM_THEME = "follow-system-theme"
const val ZAPPING_MODE = "zapping-mode"
const val BRIGHTNESS_GESTURE = "brightness-gesture"
const val VOLUME_GESTURE = "volume-gesture"
const val SCREENCAST = "screencast"
const val SCREEN_ROTATING = "screen-rotating"
const val UNSEENS_MILLISECONDS = "unseens-milliseconds"
const val RECONNECT_MODE = "reconnect-mode"
const val COLOR_ARGB = "color-argb"
const val TUNNELING = "tunneling"
const val CLOCK_MODE = "12h-clock-mode"
const val REMOTE_CONTROL = "remote-control"
const val SLIDER = "slider"
const val ALWAYS_SHOW_REFRESH = "always-show-refresh"
const val PAGING = "paging"
const val PLAYER_PANEL = "player_panel"
const val CACHE = "cache"
const val RANDOMLY_IN_FAVOURITE = "randomly-in-favourite"
const val COLORFUL_BACKGROUND = "colorful-background"
const val COMPACT_DIMENSION = "compact-dimension"
@Composable
fun <T> mutablePreferenceOf(
key: Preferences.Key<T>,
initial: T = remember(key) { PREFERENCES[key] as T },
dataStore: Settings = LocalContext.current.settings
): MutableState<T> {
val coroutineScope = rememberCoroutineScope()
val state = produceState(initial, key1 = dataStore) {
dataStore.data.map { it[key] ?: initial }.collect {
value = it
}
}
return object : MutableState<T> {
override fun component1(): T = this.value
override fun component2(): (T) -> Unit = { this.value = it }
override var value: T
get() = state.value
set(value) {
coroutineScope.launch {
dataStore.edit {
it[key] = value
}
}
}
} as MutableState<T>
}
@Suppress("UNCHECKED_CAST")
fun <T> Settings.asStateFlow(
key: Preferences.Key<T>,
initial: T = PREFERENCES[key] as T,
coroutineScope: CoroutineScope = MainScope(),
started: SharingStarted = SharingStarted.Lazily
): StateFlow<T> = data
.map { it[key] ?: initial }
.stateIn(coroutineScope, started, initial)
operator fun <T> Settings.get(key: Preferences.Key<T>): T = asStateFlow(
key,
PREFERENCES[key] as T,
MainScope(),
SharingStarted.Lazily
).value
operator fun <T> Settings.set(key: Preferences.Key<T>, value: T) = runBlocking { // FIXME
edit { it[key] = value }
}
fun <T> Settings.asReadOnlyProperty(
key: Preferences.Key<T>,
initial: T = PREFERENCES[key] as T,
coroutineScope: CoroutineScope = MainScope(),
started: SharingStarted = SharingStarted.Lazily
): ReadOnlyProperty<Any, T> = object : ReadOnlyProperty<Any, T> {
private val state = asStateFlow(key, initial, coroutineScope, started)
override fun getValue(thisRef: Any, property: KProperty<*>): T = state.value
}
private val PREFERENCES: Map<Preferences.Key<*>, *> = listOf(
PreferencesKeys.PLAYLIST_STRATEGY to PlaylistStrategy.ALL,
PreferencesKeys.ROW_COUNT to 1,
PreferencesKeys.CONNECT_TIMEOUT to ConnectTimeout.SHORT,
PreferencesKeys.GOD_MODE to false,
PreferencesKeys.CLIP_MODE to ClipMode.ADAPTIVE,
PreferencesKeys.AUTO_REFRESH_CHANNELS to false,
PreferencesKeys.FULL_INFO_PLAYER to false,
PreferencesKeys.NO_PICTURE_MODE to false,
PreferencesKeys.DARK_MODE to true,
PreferencesKeys.USE_DYNAMIC_COLORS to false,
PreferencesKeys.FOLLOW_SYSTEM_THEME to false,
PreferencesKeys.ZAPPING_MODE to false,
PreferencesKeys.BRIGHTNESS_GESTURE to true,
PreferencesKeys.VOLUME_GESTURE to true,
PreferencesKeys.SCREENCAST to true,
PreferencesKeys.SCREEN_ROTATING to false,
PreferencesKeys.UNSEENS_MILLISECONDS to UnseensMilliseconds.DAYS_3,
PreferencesKeys.RECONNECT_MODE to ReconnectMode.NO,
PreferencesKeys.COLOR_ARGB to 0x5E6738,
PreferencesKeys.TUNNELING to false,
PreferencesKeys.CLOCK_MODE to false,
PreferencesKeys.REMOTE_CONTROL to false,
PreferencesKeys.SLIDER to true,
PreferencesKeys.ALWAYS_SHOW_REPLAY to false,
PreferencesKeys.PLAYER_PANEL to true,
PreferencesKeys.COLORFUL_BACKGROUND to false,
PreferencesKeys.COMPACT_DIMENSION to false
)
.associateBy { it.key }
.mapValues { it.value.value }
object PreferencesKeys {
val PLAYLIST_STRATEGY = intPreferencesKey("playlist-strategy")
val ROW_COUNT = intPreferencesKey("rowCount")
val CONNECT_TIMEOUT = longPreferencesKey("connect-timeout")
val GOD_MODE = booleanPreferencesKey("god-mode")
val CLIP_MODE = intPreferencesKey("clip-mode")
val AUTO_REFRESH_CHANNELS = booleanPreferencesKey("auto-refresh-channels")
val FULL_INFO_PLAYER = booleanPreferencesKey("full-info-player")
val NO_PICTURE_MODE = booleanPreferencesKey("no-picture-mode")
val DARK_MODE = booleanPreferencesKey("dark-mode")
val USE_DYNAMIC_COLORS = booleanPreferencesKey("use-dynamic-colors")
val FOLLOW_SYSTEM_THEME = booleanPreferencesKey("follow-system-theme")
val ZAPPING_MODE = booleanPreferencesKey("zapping-mode")
val BRIGHTNESS_GESTURE = booleanPreferencesKey("brightness-gesture")
val VOLUME_GESTURE = booleanPreferencesKey("volume-gesture")
val SCREENCAST = booleanPreferencesKey("screencast")
val SCREEN_ROTATING = booleanPreferencesKey("screen-rotating")
val UNSEENS_MILLISECONDS = longPreferencesKey("unseens-milliseconds")
val RECONNECT_MODE = intPreferencesKey("reconnect-mode")
val COLOR_ARGB = intPreferencesKey("color-argb")
val TUNNELING = booleanPreferencesKey("tunneling")
val CLOCK_MODE = booleanPreferencesKey("12h-clock-mode")
val REMOTE_CONTROL = booleanPreferencesKey("remote-control")
val SLIDER = booleanPreferencesKey("slider")
val ALWAYS_SHOW_REPLAY = booleanPreferencesKey("always-show-replay")
val PLAYER_PANEL = booleanPreferencesKey("player_panel")
val COLORFUL_BACKGROUND = booleanPreferencesKey("colorful-background")
val COMPACT_DIMENSION = booleanPreferencesKey("compact-dimension")
}

View File

@ -27,8 +27,6 @@ interface ChannelRepository {
category: String,
): Flow<AdjacentChannels>
suspend fun getRandomIgnoreSeriesAndHidden(): Channel?
suspend fun getByPlaylistUrl(playlistUrl: String): List<Channel>
suspend fun favouriteOrUnfavourite(id: Int)
suspend fun hide(id: Int, target: Boolean)

View File

@ -1,19 +1,17 @@
package com.m3u.data.repository.channel
import androidx.paging.PagingData
import androidx.paging.PagingSource
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.execute
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.logger.sandBox
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.wrapper.Sort
import com.m3u.data.database.dao.ChannelDao
import com.m3u.data.database.dao.PlaylistDao
import com.m3u.data.database.model.AdjacentChannels
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.isSeries
import com.m3u.core.wrapper.Sort
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.datetime.Clock
@ -23,7 +21,7 @@ import kotlin.time.Duration
internal class ChannelRepositoryImpl @Inject constructor(
private val channelDao: ChannelDao,
private val playlistDao: PlaylistDao,
private val preferences: Preferences,
private val settings: Settings,
logger: Logger,
) : ChannelRepository {
private val logger = logger.install(Profiles.REPOS_CHANNEL)
@ -66,16 +64,6 @@ internal class ChannelRepositoryImpl @Inject constructor(
category = category
)
override suspend fun getRandomIgnoreSeriesAndHidden(): Channel? = logger.execute {
val playlists = playlistDao.getAll()
val seriesPlaylistUrls = playlists
.filter { it.isSeries }
.map { it.url }
.toTypedArray()
if (!preferences.randomlyInFavorite) channelDao.randomIgnoreSeriesAndHidden(*seriesPlaylistUrls)
else channelDao.randomIgnoreSeriesInFavorite(*seriesPlaylistUrls)
}
override suspend fun getByPlaylistUrl(playlistUrl: String): List<Channel> = logger.execute {
channelDao.getByPlaylistUrl(playlistUrl)
} ?: emptyList()

View File

@ -12,7 +12,9 @@ import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.logger.post
import com.m3u.core.architecture.logger.sandBox
import com.m3u.core.architecture.preferences.PlaylistStrategy
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asReadOnlyProperty
import com.m3u.core.util.basic.startsWithAny
import com.m3u.core.util.copyToFile
import com.m3u.core.util.readFileName
@ -78,11 +80,12 @@ internal class PlaylistRepositoryImpl @Inject constructor(
@OkhttpClient(true) private val okHttpClient: OkHttpClient,
private val m3uParser: M3UParser,
private val xtreamParser: XtreamParser,
private val preferences: Preferences,
private val workManager: WorkManager,
@ApplicationContext private val context: Context,
settings: Settings
) : PlaylistRepository {
private val logger = delegate.install(Profiles.REPOS_PLAYLIST)
private val playlistStrategy by settings.asReadOnlyProperty(PreferencesKeys.PLAYLIST_STRATEGY)
override suspend fun m3uOrThrow(
title: String,
@ -98,20 +101,20 @@ internal class PlaylistRepositoryImpl @Inject constructor(
actualUrl: $actualUrl
""".trimIndent()
}
val favOrHiddenRelationIds = when (preferences.playlistStrategy) {
val favOrHiddenRelationIds = when (playlistStrategy) {
PlaylistStrategy.ALL -> emptyList()
else -> {
channelDao.getFavOrHiddenRelationIdsByPlaylistUrl(url)
}
}
val favOrHiddenUrls = when (preferences.playlistStrategy) {
val favOrHiddenUrls = when (playlistStrategy) {
PlaylistStrategy.ALL -> emptyList()
else -> {
channelDao.getFavOrHiddenUrlsByPlaylistUrlNotContainsRelationId(url)
}
}
when (preferences.playlistStrategy) {
when (playlistStrategy) {
PlaylistStrategy.ALL -> {
channelDao.deleteByPlaylistUrl(url)
}
@ -240,7 +243,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
val requiredVods = type == null || type == DataSource.Xtream.TYPE_VOD
val requiredSeries = type == null || type == DataSource.Xtream.TYPE_SERIES
if (requiredLives) {
when (preferences.playlistStrategy) {
when (playlistStrategy) {
PlaylistStrategy.ALL -> {
channelDao.deleteByPlaylistUrl(livePlaylist.url)
}
@ -252,7 +255,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
playlistDao.insertOrReplace(livePlaylist)
}
if (requiredVods) {
when (preferences.playlistStrategy) {
when (playlistStrategy) {
PlaylistStrategy.ALL -> {
channelDao.deleteByPlaylistUrl(vodPlaylist.url)
}
@ -264,7 +267,7 @@ internal class PlaylistRepositoryImpl @Inject constructor(
playlistDao.insertOrReplace(vodPlaylist)
}
if (requiredSeries) {
when (preferences.playlistStrategy) {
when (playlistStrategy) {
PlaylistStrategy.ALL -> {
channelDao.deleteByPlaylistUrl(seriesPlaylist.url)
}

View File

@ -1,12 +1,13 @@
package com.m3u.data.repository.tv
import android.net.nsd.NsdServiceInfo
import androidx.compose.runtime.snapshotFlow
import com.m3u.core.architecture.Publisher
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asStateFlow
import com.m3u.core.util.coroutine.timeout
import com.m3u.core.wrapper.Resource
import com.m3u.core.wrapper.asResource
@ -42,7 +43,7 @@ class TvRepositoryImpl @Inject constructor(
private val httpServer: HttpServer,
private val tvApi: TvApiDelegate,
logger: Logger,
preferences: Preferences,
settings: Settings,
publisher: Publisher,
) : TvRepository() {
private val logger = logger.install(Profiles.REPOS_LEANBACK)
@ -50,7 +51,8 @@ class TvRepositoryImpl @Inject constructor(
private val coroutineScope = CoroutineScope(SupervisorJob())
init {
snapshotFlow { preferences.remoteControl }
settings
.asStateFlow(PreferencesKeys.REMOTE_CONTROL)
.onEach { remoteControl ->
when {
!remoteControl -> closeBroadcastOnTv()
@ -180,5 +182,6 @@ class TvRepositoryImpl @Inject constructor(
_connected.value = null
}
private fun NsdServiceInfo.getAttribute(key: String): String? = attributes[key]?.decodeToString()
private fun NsdServiceInfo.getAttribute(key: String): String? =
attributes[key]?.decodeToString()
}

View File

@ -1,4 +1,5 @@
@file:Suppress("unused")
package com.m3u.data.service
import android.app.NotificationManager
@ -15,12 +16,14 @@ import androidx.media3.exoplayer.offline.DownloadManager
import androidx.work.WorkManager
import com.m3u.core.architecture.FileProvider
import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.settings
import com.m3u.data.logger.MessageLogger
import com.m3u.data.logger.StubLogger
import com.m3u.data.service.internal.MessagerImpl
import com.m3u.data.service.internal.PlayerManagerImpl
import com.m3u.data.service.internal.DPadReactionServiceImpl
import com.m3u.data.service.internal.FileProviderImpl
import com.m3u.data.service.internal.MessagerImpl
import com.m3u.data.service.internal.PlayerManagerImpl
import com.m3u.data.tv.http.HttpServer
import com.m3u.data.tv.http.HttpServerImpl
import com.m3u.data.tv.nsd.NsdDeviceManager
@ -105,6 +108,12 @@ object ProvidedServicesModule {
@ApplicationContext context: Context
): StandaloneDatabaseProvider = StandaloneDatabaseProvider(context)
@Provides
@Singleton
fun provideSettings(
@ApplicationContext context: Context
): Settings = context.settings
@Provides
@Singleton
fun provideCache(

View File

@ -4,7 +4,6 @@ import android.content.Context
import android.graphics.Bitmap
import android.graphics.Rect
import android.net.Uri
import androidx.compose.runtime.snapshotFlow
import androidx.core.net.toUri
import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
@ -18,7 +17,6 @@ import androidx.media3.common.Tracks
import androidx.media3.common.VideoSize
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.cache.Cache
import androidx.media3.datasource.cache.CacheDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.datasource.rtmp.RtmpDataSource
import androidx.media3.exoplayer.ExoPlayer
@ -55,8 +53,10 @@ import com.m3u.core.architecture.logger.Logger
import com.m3u.core.architecture.logger.Profiles
import com.m3u.core.architecture.logger.install
import com.m3u.core.architecture.logger.post
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.core.architecture.preferences.PreferencesKeys
import com.m3u.core.architecture.preferences.ReconnectMode
import com.m3u.core.architecture.preferences.Settings
import com.m3u.core.architecture.preferences.asReadOnlyProperty
import com.m3u.data.SSLs
import com.m3u.data.api.OkhttpClient
import com.m3u.data.codec.Codecs
@ -73,8 +73,6 @@ import io.ktor.http.Url
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.Flow
@ -82,9 +80,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
@ -107,10 +103,10 @@ import kotlin.time.Duration.Companion.seconds
class PlayerManagerImpl @Inject constructor(
@ApplicationContext private val context: Context,
@OkhttpClient(false) private val okHttpClient: OkHttpClient,
private val preferences: Preferences,
private val playlistRepository: PlaylistRepository,
private val channelRepository: ChannelRepository,
private val cache: Cache,
settings: Settings,
publisher: Publisher,
delegate: Logger
) : PlayerManager, Player.Listener, MediaSession.Callback {
@ -179,11 +175,6 @@ class PlayerManagerImpl @Inject constructor(
private val playbackPosition = MutableStateFlow(-1L)
private var currentConnectTimeout = preferences.connectTimeout
private var currentTunneling = preferences.tunneling
private var currentCache = preferences.cache
private var observePreferencesChangingJob: Job? = null
init {
mainCoroutineScope.launch {
playbackState.collectLatest { state ->
@ -242,19 +233,6 @@ class PlayerManagerImpl @Inject constructor(
licenseKey = licenseKey,
applyContinueWatching = applyContinueWatching
)
observePreferencesChangingJob?.cancel()
observePreferencesChangingJob = mainCoroutineScope.launch {
observePreferencesChanging { timeout, tunneling, cache ->
if (timeout != currentConnectTimeout || tunneling != currentTunneling || cache != currentCache) {
logger.post { "preferences changed, replaying..." }
replay()
currentConnectTimeout = timeout
currentTunneling = tunneling
currentCache = cache
}
}
}
}
}
@ -267,7 +245,7 @@ class PlayerManagerImpl @Inject constructor(
applyContinueWatching: Boolean
) {
val rtmp: Boolean = Url(url).protocol.name == "rtmp"
val tunneling: Boolean = preferences.tunneling
val tunneling: Boolean = tunneling
val mimeType = when (val chain = chain) {
is MimetypeChain.Remembered -> chain.mimeType
@ -370,8 +348,6 @@ class PlayerManagerImpl @Inject constructor(
override fun release() {
logger.post { "release" }
observePreferencesChangingJob?.cancel()
observePreferencesChangingJob = null
extractor = null
player.update {
it ?: return
@ -495,25 +471,13 @@ class PlayerManagerImpl @Inject constructor(
private fun createHttpDataSourceFactory(userAgent: String?): DataSource.Factory {
val upstream = OkHttpDataSource.Factory(okHttpClient)
.setUserAgent(userAgent)
return if (preferences.cache) {
CacheDataSource.Factory()
.setUpstreamDataSourceFactory(upstream)
.setCache(cache)
.setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
} else upstream
}
private suspend fun observePreferencesChanging(
onChanged: suspend (timeout: Long, tunneling: Boolean, cache: Boolean) -> Unit
): Unit = coroutineScope {
combine(
snapshotFlow { preferences.connectTimeout },
snapshotFlow { preferences.tunneling },
snapshotFlow { preferences.cache }
) { timeout, tunneling, cache ->
onChanged(timeout, tunneling, cache)
}
.collect()
// return if (cache) {
// CacheDataSource.Factory()
// .setUpstreamDataSourceFactory(upstream)
// .setCache(cache)
// .setFlags(CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR)
// }
return upstream
}
override fun onVideoSizeChanged(videoSize: VideoSize) {
@ -748,8 +712,11 @@ class PlayerManagerImpl @Inject constructor(
}
}
private val tunneling by settings.asReadOnlyProperty(PreferencesKeys.TUNNELING)
private val reconnectMode by settings.asReadOnlyProperty(PreferencesKeys.RECONNECT_MODE)
private suspend fun onPlaybackEnded() {
if (preferences.reconnectMode == ReconnectMode.RECONNECT) {
if (reconnectMode == ReconnectMode.RECONNECT) {
mainCoroutineScope.launch { replay() }
}
val channelUrl = chain.url

View File

@ -2,7 +2,6 @@ package com.m3u.data.tv.http.endpoint
import android.content.Context
import androidx.work.WorkManager
import com.m3u.core.architecture.preferences.Preferences
import com.m3u.data.database.model.DataSource
import com.m3u.data.repository.playlist.PlaylistRepository
import com.m3u.data.worker.SubscriptionWorker
@ -17,7 +16,6 @@ import javax.inject.Singleton
@Singleton
data class Playlists @Inject constructor(
private val workManager: WorkManager,
private val preferences: Preferences,
private val playlistRepository: PlaylistRepository,
@ApplicationContext private val context: Context
) : Endpoint {

View File

@ -6,6 +6,7 @@
<string name="ui_destination_foryou">For You</string>
<string name="ui_destination_favourite">Favorite</string>
<string name="ui_destination_extension">Extension</string>
<string name="ui_destination_setting">Settings</string>
<string name="ui_destination_playlist">Playlist</string>