This commit is contained in:
Lsong 2024-08-29 19:03:03 +08:00
parent e74c15571a
commit b88cd7f48f
5 changed files with 217 additions and 190 deletions

View File

@ -13,12 +13,6 @@ import me.lsong.mytv.utils.Settings
import okhttp3.OkHttpClient
import okhttp3.Request
// 接口定义
interface TVProvider {
suspend fun load()
fun groups(): TVGroupList
suspend fun epg(): EpgList
}
// 数据类定义
@Immutable
@ -149,10 +143,13 @@ class IPTVProvider(private val epgRepository: EpgRepository) : TVProvider {
groupList = process(sources)
epgList = fetchEPGData(epgUrls)
}
override fun groups(): TVGroupList {
return groupList
}
override suspend fun epg(): EpgList = epgList
override fun groups(): TVGroupList = groupList
override fun channels(groupTitle: String): TVChannelList {
return groupList.find { it.title == groupTitle }?.channels ?: TVChannelList()
}
private suspend fun fetchIPTVSources(): Pair<List<TVSource>, List<String>> {
val allSources = mutableListOf<TVSource>()
@ -228,3 +225,22 @@ class IPTVProvider(private val epgRepository: EpgRepository) : TVProvider {
}
}
}
// 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) })
}

View File

@ -1,5 +1,6 @@
package me.lsong.mytv.ui
import android.util.Log
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Image
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.sizeIn
import androidx.compose.foundation.layout.width
@ -51,15 +50,10 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import me.lsong.mytv.R
import me.lsong.mytv.epg.EpgList
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.MyTvProviderManager
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.findGroupIndex
import me.lsong.mytv.providers.TVProvider
import me.lsong.mytv.ui.components.LeanbackVisible
import me.lsong.mytv.ui.components.MonitorScreen
import me.lsong.mytv.ui.components.MyTvMenuItem
@ -75,88 +69,35 @@ import me.lsong.mytv.utils.handleLeanbackDragGestures
import me.lsong.mytv.utils.handleLeanbackKeyEvents
@Composable
private 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)
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,
providerManager = state.providerManager,
)
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
fun MyTvMenuWidget(
modifier: Modifier = Modifier,
epgListProvider: () -> EpgList = { EpgList() },
providerManager: MyTvProviderManager,
// epgListProvider: () -> EpgList = { EpgList() },
// groupListProvider: () -> TVGroupList = { TVGroupList() },
channelProvider: () -> TVChannel = { TVChannel() },
groupListProvider: () -> TVGroupList = { TVGroupList() },
onSelected: (TVChannel) -> Unit = {},
onSettings: (() -> Unit)? = null,
onUserAction: () -> Unit = {}
) {
val groupList = groupListProvider()
// val epgList = epgListProvider()
val groupList = providerManager.groups();
val currentChannel = channelProvider()
val epgList = epgListProvider()
val groups = remember(groupList) {
groupList.map { group ->
@ -165,15 +106,17 @@ fun MyTvMenuWidget(
}
val currentGroup = remember(groupList, currentChannel) {
groups.firstOrNull { it.title == groupList[groupList.findGroupIndex(currentChannel)].title }
groups.firstOrNull { it.title == currentChannel.groupTitle }
?: MyTvMenuItem()
}
Log.d("currentGroup", "$currentGroup $currentChannel")
val currentMenuItem = remember(currentChannel) {
MyTvMenuItem(
icon = currentChannel.logo ?: "",
title = currentChannel.title,
description = epgList.currentProgrammes(currentChannel)?.now?.title
// description = epgList.currentProgrammes(currentChannel)?.now?.title
)
}
@ -182,7 +125,7 @@ fun MyTvMenuWidget(
MyTvMenuItem(
icon = channel.logo ?: "",
title = channel.title,
description = epgList.currentProgrammes(channel)?.now?.title
// description = epgList.currentProgrammes(channel)?.now?.title
)
} ?: emptyList()
}
@ -205,11 +148,11 @@ fun MyTvMenuWidget(
selectedItem = focusedGroup,
onFocused = { menuItem ->
focusedGroup = menuItem
items = itemsProvider(menuItem.title)
items = itemsProvider(focusedGroup.title)
},
onSelected = { menuItem ->
focusedGroup = menuItem
items = itemsProvider(menuItem.title)
items = itemsProvider(focusedGroup.title)
focusedItem = items.firstOrNull() ?: MyTvMenuItem()
rightListFocusRequester.requestFocus()
},
@ -232,6 +175,9 @@ fun MyTvMenuWidget(
}
MyTvMenuItemList(
items = items,
modifier = Modifier
.fillMaxHeight()
.background(androidx.tv.material3.MaterialTheme.colorScheme.background.copy(0.8f)),
selectedItem = focusedItem,
onSelected = { menuItem ->
focusedItem = menuItem
@ -240,7 +186,6 @@ fun MyTvMenuWidget(
},
onUserAction = onUserAction,
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
fun MainContent(
modifier: Modifier = Modifier,
epgList: EpgList = EpgList(),
groups: TVGroupList = TVGroupList(),
providerManager: MyTvProviderManager,
settingsViewModel: MyTvSettingsViewModel = viewModel(),
) {
val videoPlayerState = rememberLeanbackVideoPlayerState()
val mainContentState = rememberMainContentState(
providerManager = providerManager,
videoPlayerState = videoPlayerState,
groups = groups,
)
BackHandler (
@ -324,6 +242,8 @@ fun MainContent(
onNumber = {},
)
.handleLeanbackDragGestures(
onSwipeLeft = { mainContentState.changeToPrevSource() },
onSwipeRight = { mainContentState.changeToNextSource() },
onSwipeDown = {
if (settingsViewModel.iptvChannelChangeFlip) mainContentState.changeCurrentChannelToNext()
else mainContentState.changeCurrentChannelToPrev()
@ -332,19 +252,12 @@ fun MainContent(
if (settingsViewModel.iptvChannelChangeFlip) mainContentState.changeCurrentChannelToPrev()
else mainContentState.changeCurrentChannelToNext()
},
onSwipeLeft = {
mainContentState.changeToPrevSource()
},
onSwipeRight = {
mainContentState.changeToNextSource()
},
),
)
LeanbackVisible({ mainContentState.isMenuVisible && !mainContentState.isChannelInfoVisible }) {
MyTvMenuWidget(
epgListProvider = { epgList },
groupListProvider = { groups },
providerManager = providerManager,
channelProvider = { mainContentState.currentChannel },
onSelected = { channel -> mainContentState.changeCurrentChannel(channel) },
onSettings = { mainContentState.showSettings() }
@ -354,7 +267,6 @@ fun MainContent(
LeanbackVisible({ mainContentState.isChannelInfoVisible }) {
MyTvNowPlaying(
modifier = modifier,
epgListProvider = { epgList },
channelProvider = { mainContentState.currentChannel },
channelIndexProvider = { mainContentState.currentChannelIndex },
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")
@Composable
private fun MyTvMainScreenPreview() {

View File

@ -7,10 +7,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import me.lsong.mytv.providers.MyTvProviderManager
import me.lsong.mytv.providers.TVChannel
import me.lsong.mytv.providers.TVGroupList
import me.lsong.mytv.providers.TVGroupList.Companion.channels
@ -22,10 +20,10 @@ import kotlin.math.max
@Stable
class MainContentState(
coroutineScope: CoroutineScope,
private val videoPlayerState: LeanbackVideoPlayerState,
private val groups: TVGroupList,
providerManager: MyTvProviderManager,
) {
private val groups: TVGroupList = providerManager.groups();
private var _currentChannel by mutableStateOf(TVChannel())
val currentChannel get() = _currentChannel
@ -58,15 +56,10 @@ class MainContentState(
init {
changeCurrentChannel(groups.channels.getOrElse(Settings.iptvLastIptvIdx) {
groups.firstOrNull()?.channels?.firstOrNull() ?: TVChannel()
groups.channels.firstOrNull() ?: TVChannel()
})
videoPlayerState.onReady {
coroutineScope.launch {
// val name = _currentChannel.name
// val urlIdx = _currentIptvUrlIdx
}
// 记忆可播放的域名
Settings.iptvPlayableHostList += getUrlHost(_currentChannel.urls[_currentIptvUrlIdx])
}
@ -100,13 +93,10 @@ class MainContentState(
}
fun changeCurrentChannel(channel: TVChannel, urlIdx: Int? = null) {
// isChannelInfoVisible = false
if (channel == _currentChannel && urlIdx == null) return
if (channel == _currentChannel && urlIdx != _currentIptvUrlIdx) {
Settings.iptvPlayableHostList -= getUrlHost(_currentChannel.urls[_currentIptvUrlIdx])
}
// _isTempPanelVisible = true
_currentChannel = channel
Settings.iptvLastIptvIdx = currentChannelIndex
@ -131,7 +121,6 @@ class MainContentState(
changeCurrentChannel(getNextChannel())
}
fun changeToPrevSource(){
if (currentChannel.urls.size > 1) {
changeCurrentChannel(
@ -149,16 +138,18 @@ class MainContentState(
}
}
fun showChannelInfo() {
isMenuVisible = false
isChannelInfoVisible = true
}
fun showMenu() {
isMenuVisible = true
isSettingsVisale = false
isChannelInfoVisible = false
}
fun showChannelInfo() {
isMenuVisible = false
isSettingsVisale = false
isChannelInfoVisible = true
}
fun showSettings() {
isMenuVisible = false
isSettingsVisale = true
@ -168,14 +159,12 @@ class MainContentState(
@Composable
fun rememberMainContentState(
coroutineScope: CoroutineScope = rememberCoroutineScope(),
providerManager: MyTvProviderManager,
videoPlayerState: LeanbackVideoPlayerState = rememberLeanbackVideoPlayerState(),
groups: TVGroupList = TVGroupList(),
) = remember {
MainContentState(
coroutineScope = coroutineScope,
providerManager = providerManager,
videoPlayerState = videoPlayerState,
groups = groups,
)
}

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

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