mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-05-17 19:35:58 +08:00
refactor: migrate shared-preferences to data-store.
This commit is contained in:
@ -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() {
|
||||
|
@ -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()
|
||||
) {
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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(
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -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(),
|
||||
|
@ -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 }
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -36,4 +36,6 @@ dependencies {
|
||||
|
||||
api(libs.androidx.paging.runtime.ktx)
|
||||
api(libs.androidx.paging.compose)
|
||||
|
||||
api("androidx.datastore:datastore-preferences:1.1.4")
|
||||
}
|
@ -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")
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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>
|
||||
|
||||
|
Reference in New Issue
Block a user