update
This commit is contained in:
parent
e74c15571a
commit
b88cd7f48f
@ -13,12 +13,6 @@ import me.lsong.mytv.utils.Settings
|
|||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
|
|
||||||
// 接口定义
|
|
||||||
interface TVProvider {
|
|
||||||
suspend fun load()
|
|
||||||
fun groups(): TVGroupList
|
|
||||||
suspend fun epg(): EpgList
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据类定义
|
// 数据类定义
|
||||||
@Immutable
|
@Immutable
|
||||||
@ -149,10 +143,13 @@ class IPTVProvider(private val epgRepository: EpgRepository) : TVProvider {
|
|||||||
groupList = process(sources)
|
groupList = process(sources)
|
||||||
epgList = fetchEPGData(epgUrls)
|
epgList = fetchEPGData(epgUrls)
|
||||||
}
|
}
|
||||||
|
override fun groups(): TVGroupList {
|
||||||
|
return groupList
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun epg(): EpgList = epgList
|
override fun channels(groupTitle: String): TVChannelList {
|
||||||
override fun groups(): TVGroupList = groupList
|
return groupList.find { it.title == groupTitle }?.channels ?: TVChannelList()
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun fetchIPTVSources(): Pair<List<TVSource>, List<String>> {
|
private suspend fun fetchIPTVSources(): Pair<List<TVSource>, List<String>> {
|
||||||
val allSources = mutableListOf<TVSource>()
|
val allSources = mutableListOf<TVSource>()
|
||||||
@ -227,4 +224,23 @@ class IPTVProvider(private val epgRepository: EpgRepository) : TVProvider {
|
|||||||
Log.i("getM3uChannels", "解析直播源完成:${it.sources.size}个资源, $sourceUrl")
|
Log.i("getM3uChannels", "解析直播源完成:${it.sources.size}个资源, $sourceUrl")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Interface definition
|
||||||
|
interface TVProvider {
|
||||||
|
suspend fun load()
|
||||||
|
fun groups(): TVGroupList
|
||||||
|
fun channels(groupTitle: String): TVChannelList
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyTvProviderManager : TVProvider {
|
||||||
|
private val providers: List<TVProvider> = listOf(
|
||||||
|
IPTVProvider(EpgRepository())
|
||||||
|
)
|
||||||
|
override suspend fun load() {
|
||||||
|
providers.forEach { it.load() }
|
||||||
|
}
|
||||||
|
override fun groups(): TVGroupList = TVGroupList(providers.flatMap { it.groups() })
|
||||||
|
override fun channels(groupTitle: String): TVChannelList = TVChannelList(providers.flatMap { it.channels(groupTitle) })
|
||||||
}
|
}
|
@ -1,5 +1,6 @@
|
|||||||
package me.lsong.mytv.ui
|
package me.lsong.mytv.ui
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@ -11,9 +12,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.sizeIn
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@ -51,15 +50,10 @@ import kotlinx.coroutines.flow.StateFlow
|
|||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import me.lsong.mytv.R
|
import me.lsong.mytv.R
|
||||||
import me.lsong.mytv.epg.EpgList
|
import me.lsong.mytv.providers.MyTvProviderManager
|
||||||
import me.lsong.mytv.epg.EpgList.Companion.currentProgrammes
|
|
||||||
import me.lsong.mytv.epg.EpgRepository
|
|
||||||
import me.lsong.mytv.providers.IPTVProvider
|
|
||||||
import me.lsong.mytv.providers.TVChannel
|
import me.lsong.mytv.providers.TVChannel
|
||||||
import me.lsong.mytv.providers.TVGroupList
|
|
||||||
import me.lsong.mytv.providers.TVGroupList.Companion.channels
|
import me.lsong.mytv.providers.TVGroupList.Companion.channels
|
||||||
import me.lsong.mytv.providers.TVGroupList.Companion.findGroupIndex
|
import me.lsong.mytv.providers.TVGroupList.Companion.findGroupIndex
|
||||||
import me.lsong.mytv.providers.TVProvider
|
|
||||||
import me.lsong.mytv.ui.components.LeanbackVisible
|
import me.lsong.mytv.ui.components.LeanbackVisible
|
||||||
import me.lsong.mytv.ui.components.MonitorScreen
|
import me.lsong.mytv.ui.components.MonitorScreen
|
||||||
import me.lsong.mytv.ui.components.MyTvMenuItem
|
import me.lsong.mytv.ui.components.MyTvMenuItem
|
||||||
@ -75,88 +69,35 @@ import me.lsong.mytv.utils.handleLeanbackDragGestures
|
|||||||
import me.lsong.mytv.utils.handleLeanbackKeyEvents
|
import me.lsong.mytv.utils.handleLeanbackKeyEvents
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StartScreen(state: LeanbackMainUiState) {
|
fun MainScreen(
|
||||||
var isSettingsVisible by remember { mutableStateOf(false) }
|
modifier: Modifier = Modifier,
|
||||||
BackHandler(enabled = !isSettingsVisible) {
|
mainViewModel: MainViewModel = viewModel(),
|
||||||
isSettingsVisible = true
|
) {
|
||||||
}
|
val uiState by mainViewModel.uiState.collectAsState()
|
||||||
Box(
|
when (val state = uiState) {
|
||||||
contentAlignment = Alignment.Center,
|
is LeanbackMainUiState.Loading,
|
||||||
modifier = Modifier
|
is LeanbackMainUiState.Error -> StartScreen(state)
|
||||||
.fillMaxSize()
|
is LeanbackMainUiState.Ready -> MainContent(
|
||||||
.background(MaterialTheme.colorScheme.background)
|
modifier = modifier,
|
||||||
.onPreviewKeyEvent { event ->
|
providerManager = state.providerManager,
|
||||||
if (event.key == Key.Menu && event.type == KeyEventType.KeyUp) {
|
)
|
||||||
isSettingsVisible = !isSettingsVisible
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(id = R.mipmap.ic_launcher),
|
|
||||||
contentDescription = "DuckTV",
|
|
||||||
modifier = Modifier.size(96.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = Constants.APP_NAME,
|
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground,
|
|
||||||
)
|
|
||||||
|
|
||||||
when (state) {
|
|
||||||
is LeanbackMainUiState.Loading -> {
|
|
||||||
LinearProgressIndicator(
|
|
||||||
modifier = Modifier
|
|
||||||
.widthIn(300.dp, 800.dp)
|
|
||||||
.height(8.dp)
|
|
||||||
)
|
|
||||||
state.message?.let { message ->
|
|
||||||
Text(
|
|
||||||
text = message,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f),
|
|
||||||
modifier = Modifier.sizeIn(maxWidth = 500.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
is LeanbackMainUiState.Error -> {
|
|
||||||
state.message?.let { message ->
|
|
||||||
Text(
|
|
||||||
text = message,
|
|
||||||
style = MaterialTheme.typography.bodySmall,
|
|
||||||
color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f),
|
|
||||||
modifier = Modifier.sizeIn(maxWidth = 500.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {} // This case should never happen
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LeanbackVisible({ isSettingsVisible }) {
|
|
||||||
SettingsScreen()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MyTvMenuWidget(
|
fun MyTvMenuWidget(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
epgListProvider: () -> EpgList = { EpgList() },
|
providerManager: MyTvProviderManager,
|
||||||
|
// epgListProvider: () -> EpgList = { EpgList() },
|
||||||
|
// groupListProvider: () -> TVGroupList = { TVGroupList() },
|
||||||
channelProvider: () -> TVChannel = { TVChannel() },
|
channelProvider: () -> TVChannel = { TVChannel() },
|
||||||
groupListProvider: () -> TVGroupList = { TVGroupList() },
|
|
||||||
onSelected: (TVChannel) -> Unit = {},
|
onSelected: (TVChannel) -> Unit = {},
|
||||||
onSettings: (() -> Unit)? = null,
|
onSettings: (() -> Unit)? = null,
|
||||||
onUserAction: () -> Unit = {}
|
onUserAction: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val groupList = groupListProvider()
|
// val epgList = epgListProvider()
|
||||||
|
val groupList = providerManager.groups();
|
||||||
val currentChannel = channelProvider()
|
val currentChannel = channelProvider()
|
||||||
val epgList = epgListProvider()
|
|
||||||
|
|
||||||
val groups = remember(groupList) {
|
val groups = remember(groupList) {
|
||||||
groupList.map { group ->
|
groupList.map { group ->
|
||||||
@ -165,15 +106,17 @@ fun MyTvMenuWidget(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val currentGroup = remember(groupList, currentChannel) {
|
val currentGroup = remember(groupList, currentChannel) {
|
||||||
groups.firstOrNull { it.title == groupList[groupList.findGroupIndex(currentChannel)].title }
|
groups.firstOrNull { it.title == currentChannel.groupTitle }
|
||||||
?: MyTvMenuItem()
|
?: MyTvMenuItem()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Log.d("currentGroup", "$currentGroup $currentChannel")
|
||||||
|
|
||||||
val currentMenuItem = remember(currentChannel) {
|
val currentMenuItem = remember(currentChannel) {
|
||||||
MyTvMenuItem(
|
MyTvMenuItem(
|
||||||
icon = currentChannel.logo ?: "",
|
icon = currentChannel.logo ?: "",
|
||||||
title = currentChannel.title,
|
title = currentChannel.title,
|
||||||
description = epgList.currentProgrammes(currentChannel)?.now?.title
|
// description = epgList.currentProgrammes(currentChannel)?.now?.title
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,7 +125,7 @@ fun MyTvMenuWidget(
|
|||||||
MyTvMenuItem(
|
MyTvMenuItem(
|
||||||
icon = channel.logo ?: "",
|
icon = channel.logo ?: "",
|
||||||
title = channel.title,
|
title = channel.title,
|
||||||
description = epgList.currentProgrammes(channel)?.now?.title
|
// description = epgList.currentProgrammes(channel)?.now?.title
|
||||||
)
|
)
|
||||||
} ?: emptyList()
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
@ -205,11 +148,11 @@ fun MyTvMenuWidget(
|
|||||||
selectedItem = focusedGroup,
|
selectedItem = focusedGroup,
|
||||||
onFocused = { menuItem ->
|
onFocused = { menuItem ->
|
||||||
focusedGroup = menuItem
|
focusedGroup = menuItem
|
||||||
items = itemsProvider(menuItem.title)
|
items = itemsProvider(focusedGroup.title)
|
||||||
},
|
},
|
||||||
onSelected = { menuItem ->
|
onSelected = { menuItem ->
|
||||||
focusedGroup = menuItem
|
focusedGroup = menuItem
|
||||||
items = itemsProvider(menuItem.title)
|
items = itemsProvider(focusedGroup.title)
|
||||||
focusedItem = items.firstOrNull() ?: MyTvMenuItem()
|
focusedItem = items.firstOrNull() ?: MyTvMenuItem()
|
||||||
rightListFocusRequester.requestFocus()
|
rightListFocusRequester.requestFocus()
|
||||||
},
|
},
|
||||||
@ -232,6 +175,9 @@ fun MyTvMenuWidget(
|
|||||||
}
|
}
|
||||||
MyTvMenuItemList(
|
MyTvMenuItemList(
|
||||||
items = items,
|
items = items,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(androidx.tv.material3.MaterialTheme.colorScheme.background.copy(0.8f)),
|
||||||
selectedItem = focusedItem,
|
selectedItem = focusedItem,
|
||||||
onSelected = { menuItem ->
|
onSelected = { menuItem ->
|
||||||
focusedItem = menuItem
|
focusedItem = menuItem
|
||||||
@ -240,7 +186,6 @@ fun MyTvMenuWidget(
|
|||||||
},
|
},
|
||||||
onUserAction = onUserAction,
|
onUserAction = onUserAction,
|
||||||
focusRequester = rightListFocusRequester,
|
focusRequester = rightListFocusRequester,
|
||||||
modifier = Modifier.background(androidx.tv.material3.MaterialTheme.colorScheme.background.copy(0.8f)),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -249,43 +194,16 @@ fun MyTvMenuWidget(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun MainScreen(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
mainViewModel: MainViewModel = viewModel(),
|
|
||||||
) {
|
|
||||||
val uiState by mainViewModel.uiState.collectAsState()
|
|
||||||
when (val state = uiState) {
|
|
||||||
is LeanbackMainUiState.Loading,
|
|
||||||
is LeanbackMainUiState.Error -> StartScreen(state)
|
|
||||||
is LeanbackMainUiState.Ready -> MainContent(
|
|
||||||
modifier = modifier,
|
|
||||||
groups = state.groups,
|
|
||||||
epgList = state.epgList,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface LeanbackMainUiState {
|
|
||||||
data class Loading(val message: String? = null) : LeanbackMainUiState
|
|
||||||
data class Error(val message: String? = null) : LeanbackMainUiState
|
|
||||||
data class Ready(
|
|
||||||
val groups: TVGroupList = TVGroupList(),
|
|
||||||
val epgList: EpgList = EpgList(),
|
|
||||||
) : LeanbackMainUiState
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainContent(
|
fun MainContent(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
epgList: EpgList = EpgList(),
|
providerManager: MyTvProviderManager,
|
||||||
groups: TVGroupList = TVGroupList(),
|
|
||||||
settingsViewModel: MyTvSettingsViewModel = viewModel(),
|
settingsViewModel: MyTvSettingsViewModel = viewModel(),
|
||||||
) {
|
) {
|
||||||
val videoPlayerState = rememberLeanbackVideoPlayerState()
|
val videoPlayerState = rememberLeanbackVideoPlayerState()
|
||||||
val mainContentState = rememberMainContentState(
|
val mainContentState = rememberMainContentState(
|
||||||
|
providerManager = providerManager,
|
||||||
videoPlayerState = videoPlayerState,
|
videoPlayerState = videoPlayerState,
|
||||||
groups = groups,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
BackHandler (
|
BackHandler (
|
||||||
@ -324,6 +242,8 @@ fun MainContent(
|
|||||||
onNumber = {},
|
onNumber = {},
|
||||||
)
|
)
|
||||||
.handleLeanbackDragGestures(
|
.handleLeanbackDragGestures(
|
||||||
|
onSwipeLeft = { mainContentState.changeToPrevSource() },
|
||||||
|
onSwipeRight = { mainContentState.changeToNextSource() },
|
||||||
onSwipeDown = {
|
onSwipeDown = {
|
||||||
if (settingsViewModel.iptvChannelChangeFlip) mainContentState.changeCurrentChannelToNext()
|
if (settingsViewModel.iptvChannelChangeFlip) mainContentState.changeCurrentChannelToNext()
|
||||||
else mainContentState.changeCurrentChannelToPrev()
|
else mainContentState.changeCurrentChannelToPrev()
|
||||||
@ -332,19 +252,12 @@ fun MainContent(
|
|||||||
if (settingsViewModel.iptvChannelChangeFlip) mainContentState.changeCurrentChannelToPrev()
|
if (settingsViewModel.iptvChannelChangeFlip) mainContentState.changeCurrentChannelToPrev()
|
||||||
else mainContentState.changeCurrentChannelToNext()
|
else mainContentState.changeCurrentChannelToNext()
|
||||||
},
|
},
|
||||||
onSwipeLeft = {
|
|
||||||
mainContentState.changeToPrevSource()
|
|
||||||
},
|
|
||||||
onSwipeRight = {
|
|
||||||
mainContentState.changeToNextSource()
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
LeanbackVisible({ mainContentState.isMenuVisible && !mainContentState.isChannelInfoVisible }) {
|
LeanbackVisible({ mainContentState.isMenuVisible && !mainContentState.isChannelInfoVisible }) {
|
||||||
MyTvMenuWidget(
|
MyTvMenuWidget(
|
||||||
epgListProvider = { epgList },
|
providerManager = providerManager,
|
||||||
groupListProvider = { groups },
|
|
||||||
channelProvider = { mainContentState.currentChannel },
|
channelProvider = { mainContentState.currentChannel },
|
||||||
onSelected = { channel -> mainContentState.changeCurrentChannel(channel) },
|
onSelected = { channel -> mainContentState.changeCurrentChannel(channel) },
|
||||||
onSettings = { mainContentState.showSettings() }
|
onSettings = { mainContentState.showSettings() }
|
||||||
@ -354,7 +267,6 @@ fun MainContent(
|
|||||||
LeanbackVisible({ mainContentState.isChannelInfoVisible }) {
|
LeanbackVisible({ mainContentState.isChannelInfoVisible }) {
|
||||||
MyTvNowPlaying(
|
MyTvNowPlaying(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
epgListProvider = { epgList },
|
|
||||||
channelProvider = { mainContentState.currentChannel },
|
channelProvider = { mainContentState.currentChannel },
|
||||||
channelIndexProvider = { mainContentState.currentChannelIndex },
|
channelIndexProvider = { mainContentState.currentChannelIndex },
|
||||||
sourceIndexProvider = { mainContentState.currentSourceIndex },
|
sourceIndexProvider = { mainContentState.currentSourceIndex },
|
||||||
@ -372,41 +284,6 @@ fun MainContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MainViewModel.kt
|
|
||||||
class MainViewModel : ViewModel() {
|
|
||||||
private val providers: List<TVProvider> = listOf(
|
|
||||||
IPTVProvider(EpgRepository())
|
|
||||||
)
|
|
||||||
private val _uiState = MutableStateFlow<LeanbackMainUiState>(LeanbackMainUiState.Loading())
|
|
||||||
val uiState: StateFlow<LeanbackMainUiState> = _uiState.asStateFlow()
|
|
||||||
|
|
||||||
init {
|
|
||||||
viewModelScope.launch {
|
|
||||||
refreshData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun refreshData() {
|
|
||||||
try {
|
|
||||||
_uiState.value = LeanbackMainUiState.Loading("Initializing providers...")
|
|
||||||
providers.forEachIndexed { index, provider ->
|
|
||||||
_uiState.value = LeanbackMainUiState.Loading("Initializing provider ${index + 1}/${providers.size}...")
|
|
||||||
provider.load()
|
|
||||||
}
|
|
||||||
|
|
||||||
val groupList = providers.flatMap { it.groups() }
|
|
||||||
val epgList = providers.map { it.epg() }.reduce { acc, epgList -> (acc + epgList) as EpgList }
|
|
||||||
|
|
||||||
_uiState.value = LeanbackMainUiState.Ready(
|
|
||||||
groups = TVGroupList(groupList),
|
|
||||||
epgList = epgList
|
|
||||||
)
|
|
||||||
} catch (error: Exception) {
|
|
||||||
_uiState.value = LeanbackMainUiState.Error(error.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Preview(device = "id:pixel_5")
|
@Preview(device = "id:pixel_5")
|
||||||
@Composable
|
@Composable
|
||||||
private fun MyTvMainScreenPreview() {
|
private fun MyTvMainScreenPreview() {
|
||||||
|
@ -7,10 +7,8 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import me.lsong.mytv.providers.MyTvProviderManager
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import me.lsong.mytv.providers.TVChannel
|
import me.lsong.mytv.providers.TVChannel
|
||||||
import me.lsong.mytv.providers.TVGroupList
|
import me.lsong.mytv.providers.TVGroupList
|
||||||
import me.lsong.mytv.providers.TVGroupList.Companion.channels
|
import me.lsong.mytv.providers.TVGroupList.Companion.channels
|
||||||
@ -22,10 +20,10 @@ import kotlin.math.max
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class MainContentState(
|
class MainContentState(
|
||||||
coroutineScope: CoroutineScope,
|
|
||||||
private val videoPlayerState: LeanbackVideoPlayerState,
|
private val videoPlayerState: LeanbackVideoPlayerState,
|
||||||
private val groups: TVGroupList,
|
providerManager: MyTvProviderManager,
|
||||||
) {
|
) {
|
||||||
|
private val groups: TVGroupList = providerManager.groups();
|
||||||
private var _currentChannel by mutableStateOf(TVChannel())
|
private var _currentChannel by mutableStateOf(TVChannel())
|
||||||
val currentChannel get() = _currentChannel
|
val currentChannel get() = _currentChannel
|
||||||
|
|
||||||
@ -58,15 +56,10 @@ class MainContentState(
|
|||||||
|
|
||||||
init {
|
init {
|
||||||
changeCurrentChannel(groups.channels.getOrElse(Settings.iptvLastIptvIdx) {
|
changeCurrentChannel(groups.channels.getOrElse(Settings.iptvLastIptvIdx) {
|
||||||
groups.firstOrNull()?.channels?.firstOrNull() ?: TVChannel()
|
groups.channels.firstOrNull() ?: TVChannel()
|
||||||
})
|
})
|
||||||
|
|
||||||
videoPlayerState.onReady {
|
videoPlayerState.onReady {
|
||||||
coroutineScope.launch {
|
|
||||||
// val name = _currentChannel.name
|
|
||||||
// val urlIdx = _currentIptvUrlIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
// 记忆可播放的域名
|
// 记忆可播放的域名
|
||||||
Settings.iptvPlayableHostList += getUrlHost(_currentChannel.urls[_currentIptvUrlIdx])
|
Settings.iptvPlayableHostList += getUrlHost(_currentChannel.urls[_currentIptvUrlIdx])
|
||||||
}
|
}
|
||||||
@ -100,13 +93,10 @@ class MainContentState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun changeCurrentChannel(channel: TVChannel, urlIdx: Int? = null) {
|
fun changeCurrentChannel(channel: TVChannel, urlIdx: Int? = null) {
|
||||||
// isChannelInfoVisible = false
|
|
||||||
if (channel == _currentChannel && urlIdx == null) return
|
if (channel == _currentChannel && urlIdx == null) return
|
||||||
if (channel == _currentChannel && urlIdx != _currentIptvUrlIdx) {
|
if (channel == _currentChannel && urlIdx != _currentIptvUrlIdx) {
|
||||||
Settings.iptvPlayableHostList -= getUrlHost(_currentChannel.urls[_currentIptvUrlIdx])
|
Settings.iptvPlayableHostList -= getUrlHost(_currentChannel.urls[_currentIptvUrlIdx])
|
||||||
}
|
}
|
||||||
// _isTempPanelVisible = true
|
|
||||||
|
|
||||||
_currentChannel = channel
|
_currentChannel = channel
|
||||||
Settings.iptvLastIptvIdx = currentChannelIndex
|
Settings.iptvLastIptvIdx = currentChannelIndex
|
||||||
|
|
||||||
@ -131,7 +121,6 @@ class MainContentState(
|
|||||||
changeCurrentChannel(getNextChannel())
|
changeCurrentChannel(getNextChannel())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun changeToPrevSource(){
|
fun changeToPrevSource(){
|
||||||
if (currentChannel.urls.size > 1) {
|
if (currentChannel.urls.size > 1) {
|
||||||
changeCurrentChannel(
|
changeCurrentChannel(
|
||||||
@ -149,16 +138,18 @@ class MainContentState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun showChannelInfo() {
|
|
||||||
isMenuVisible = false
|
|
||||||
isChannelInfoVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showMenu() {
|
fun showMenu() {
|
||||||
isMenuVisible = true
|
isMenuVisible = true
|
||||||
|
isSettingsVisale = false
|
||||||
isChannelInfoVisible = false
|
isChannelInfoVisible = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun showChannelInfo() {
|
||||||
|
isMenuVisible = false
|
||||||
|
isSettingsVisale = false
|
||||||
|
isChannelInfoVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
fun showSettings() {
|
fun showSettings() {
|
||||||
isMenuVisible = false
|
isMenuVisible = false
|
||||||
isSettingsVisale = true
|
isSettingsVisale = true
|
||||||
@ -168,14 +159,12 @@ class MainContentState(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberMainContentState(
|
fun rememberMainContentState(
|
||||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
providerManager: MyTvProviderManager,
|
||||||
videoPlayerState: LeanbackVideoPlayerState = rememberLeanbackVideoPlayerState(),
|
videoPlayerState: LeanbackVideoPlayerState = rememberLeanbackVideoPlayerState(),
|
||||||
groups: TVGroupList = TVGroupList(),
|
|
||||||
) = remember {
|
) = remember {
|
||||||
MainContentState(
|
MainContentState(
|
||||||
coroutineScope = coroutineScope,
|
providerManager = providerManager,
|
||||||
videoPlayerState = videoPlayerState,
|
videoPlayerState = videoPlayerState,
|
||||||
groups = groups,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
41
app/src/main/java/me/lsong/mytv/ui/MainViewModel.kt
Normal file
41
app/src/main/java/me/lsong/mytv/ui/MainViewModel.kt
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package me.lsong.mytv.ui
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import me.lsong.mytv.providers.MyTvProviderManager
|
||||||
|
|
||||||
|
|
||||||
|
// MainViewModel.kt
|
||||||
|
class MainViewModel : ViewModel() {
|
||||||
|
private val providerManager = MyTvProviderManager()
|
||||||
|
private val _uiState = MutableStateFlow<LeanbackMainUiState>(LeanbackMainUiState.Loading())
|
||||||
|
val uiState: StateFlow<LeanbackMainUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
loadData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadData() {
|
||||||
|
try {
|
||||||
|
_uiState.value = LeanbackMainUiState.Loading("Initializing providers...")
|
||||||
|
providerManager.load()
|
||||||
|
_uiState.value = LeanbackMainUiState.Ready(providerManager)
|
||||||
|
} catch (error: Exception) {
|
||||||
|
_uiState.value = LeanbackMainUiState.Error(error.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface LeanbackMainUiState {
|
||||||
|
data class Loading(val message: String? = null) : LeanbackMainUiState
|
||||||
|
data class Error(val message: String? = null) : LeanbackMainUiState
|
||||||
|
data class Ready(
|
||||||
|
val providerManager: MyTvProviderManager,
|
||||||
|
) : LeanbackMainUiState
|
||||||
|
}
|
104
app/src/main/java/me/lsong/mytv/ui/StartScreen.kt
Normal file
104
app/src/main/java/me/lsong/mytv/ui/StartScreen.kt
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
package me.lsong.mytv.ui
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.KeyEventType
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||||
|
import androidx.compose.ui.input.key.type
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import me.lsong.mytv.R
|
||||||
|
import me.lsong.mytv.ui.components.LeanbackVisible
|
||||||
|
import me.lsong.mytv.ui.settings.SettingsScreen
|
||||||
|
import me.lsong.mytv.utils.Constants
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun StartScreen(state: LeanbackMainUiState) {
|
||||||
|
var isSettingsVisible by remember { mutableStateOf(false) }
|
||||||
|
BackHandler(enabled = !isSettingsVisible) {
|
||||||
|
isSettingsVisible = true
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background)
|
||||||
|
.onPreviewKeyEvent { event ->
|
||||||
|
if (event.key == Key.Menu && event.type == KeyEventType.KeyUp) {
|
||||||
|
isSettingsVisible = !isSettingsVisible
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(id = R.mipmap.ic_launcher),
|
||||||
|
contentDescription = "DuckTV",
|
||||||
|
modifier = Modifier.size(96.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = Constants.APP_NAME,
|
||||||
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
)
|
||||||
|
|
||||||
|
when (state) {
|
||||||
|
is LeanbackMainUiState.Loading -> {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(300.dp, 800.dp)
|
||||||
|
.height(8.dp)
|
||||||
|
)
|
||||||
|
state.message?.let { message ->
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.8f),
|
||||||
|
modifier = Modifier.sizeIn(maxWidth = 500.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is LeanbackMainUiState.Error -> {
|
||||||
|
state.message?.let { message ->
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f),
|
||||||
|
modifier = Modifier.sizeIn(maxWidth = 500.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {} // This case should never happen
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LeanbackVisible({ isSettingsVisible }) {
|
||||||
|
SettingsScreen()
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user