mirror of
https://github.com/oxyroid/M3UAndroid.git
synced 2025-05-17 19:35:58 +08:00
feat: tv UI work.
This commit is contained in:
@ -48,6 +48,13 @@ android {
|
||||
packaging {
|
||||
resources.excludes += "META-INF/**"
|
||||
}
|
||||
applicationVariants.all {
|
||||
outputs
|
||||
.map { it as com.android.build.gradle.internal.api.ApkVariantOutputImpl }
|
||||
.forEach { output ->
|
||||
output.versionNameOverride = "tv-${versionName}.apk"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
hilt {
|
||||
|
@ -2,14 +2,17 @@ package com.m3u.tv.common
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -17,44 +20,38 @@ 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.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component1
|
||||
import androidx.compose.ui.focus.FocusRequester.Companion.FocusRequesterFactory.component2
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.focusRestorer
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.tv.material3.Border
|
||||
import androidx.tv.material3.CardDefaults
|
||||
import androidx.tv.material3.CompactCard
|
||||
import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import coil.compose.AsyncImage
|
||||
import com.m3u.data.database.model.Channel
|
||||
import com.m3u.tv.screens.dashboard.rememberChildPadding
|
||||
import com.m3u.tv.theme.JetStreamBorderWidth
|
||||
|
||||
enum class ItemDirection(val aspectRatio: Float) {
|
||||
Vertical(10.5f / 16f),
|
||||
Horizontal(16f / 9f);
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun ChannelsRow(
|
||||
channels: List<Channel>,
|
||||
modifier: Modifier = Modifier,
|
||||
itemDirection: ItemDirection = ItemDirection.Vertical,
|
||||
startPadding: Dp = rememberChildPadding().start,
|
||||
endPadding: Dp = rememberChildPadding().end,
|
||||
title: String? = null,
|
||||
@ -62,8 +59,6 @@ fun ChannelsRow(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 30.sp
|
||||
),
|
||||
showItemTitle: Boolean = true,
|
||||
showIndexOverImage: Boolean = false,
|
||||
onChannelSelected: (channel: Channel) -> Unit = {}
|
||||
) {
|
||||
val (lazyRow, firstItem) = remember { FocusRequester.createRefs() }
|
||||
@ -104,15 +99,11 @@ fun ChannelsRow(
|
||||
}
|
||||
ChannelsRowItem(
|
||||
modifier = itemModifier.weight(1f),
|
||||
index = index,
|
||||
itemDirection = itemDirection,
|
||||
onChannelSelected = {
|
||||
lazyRow.saveFocusedChild()
|
||||
onChannelSelected(it)
|
||||
},
|
||||
channel = channel,
|
||||
showItemTitle = showItemTitle,
|
||||
showIndexOverImage = showIndexOverImage
|
||||
channel = channel
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -120,12 +111,40 @@ fun ChannelsRow(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChannelsRow(
|
||||
channels: LazyPagingItems<Channel>,
|
||||
modifier: Modifier = Modifier,
|
||||
startPadding: Dp = rememberChildPadding().start,
|
||||
endPadding: Dp = rememberChildPadding().end,
|
||||
onChannelSelected: (channel: Channel) -> Unit = {}
|
||||
) {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(
|
||||
start = startPadding,
|
||||
end = endPadding,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(20.dp),
|
||||
modifier = modifier
|
||||
) {
|
||||
items(channels.itemCount) { index ->
|
||||
val channel = channels[index]
|
||||
if (channel != null) {
|
||||
ChannelsRowItem(
|
||||
modifier = Modifier.fillParentMaxHeight(),
|
||||
onChannelSelected = onChannelSelected,
|
||||
channel = channel,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun ImmersiveListChannelsRow(
|
||||
channels: List<Channel>,
|
||||
modifier: Modifier = Modifier,
|
||||
itemDirection: ItemDirection = ItemDirection.Vertical,
|
||||
startPadding: Dp = rememberChildPadding().start,
|
||||
endPadding: Dp = rememberChildPadding().end,
|
||||
title: String? = null,
|
||||
@ -133,10 +152,7 @@ fun ImmersiveListChannelsRow(
|
||||
fontWeight = FontWeight.Medium,
|
||||
fontSize = 30.sp
|
||||
),
|
||||
showItemTitle: Boolean = true,
|
||||
showIndexOverImage: Boolean = false,
|
||||
onChannelSelected: (Channel) -> Unit = {},
|
||||
onChannelFocused: (Channel) -> Unit = {}
|
||||
) {
|
||||
val (lazyRow, firstItem) = remember { FocusRequester.createRefs() }
|
||||
|
||||
@ -179,16 +195,11 @@ fun ImmersiveListChannelsRow(
|
||||
}
|
||||
ChannelsRowItem(
|
||||
modifier = itemModifier.weight(1f),
|
||||
index = index,
|
||||
itemDirection = itemDirection,
|
||||
onChannelSelected = {
|
||||
lazyRow.saveFocusedChild()
|
||||
onChannelSelected(it)
|
||||
},
|
||||
onChannelFocused = onChannelFocused,
|
||||
channel = channel,
|
||||
showItemTitle = showItemTitle,
|
||||
showIndexOverImage = showIndexOverImage
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -196,119 +207,73 @@ fun ImmersiveListChannelsRow(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
private fun ChannelsRowItem(
|
||||
index: Int,
|
||||
channel: Channel,
|
||||
onChannelSelected: (Channel) -> Unit,
|
||||
showItemTitle: Boolean,
|
||||
showIndexOverImage: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
itemDirection: ItemDirection = ItemDirection.Vertical,
|
||||
onChannelFocused: (Channel) -> Unit = {},
|
||||
itemWidth: Dp = 432.dp
|
||||
) {
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
|
||||
ChannelCard(
|
||||
onClick = { onChannelSelected(channel) },
|
||||
title = {
|
||||
ChannelsRowItemText(
|
||||
showItemTitle = showItemTitle,
|
||||
isItemFocused = isFocused,
|
||||
channel = channel
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.onFocusChanged {
|
||||
isFocused = it.isFocused
|
||||
if (it.isFocused) {
|
||||
onChannelFocused(channel)
|
||||
}
|
||||
}
|
||||
.focusProperties {
|
||||
left = if (index == 0) {
|
||||
FocusRequester.Cancel
|
||||
} else {
|
||||
FocusRequester.Default
|
||||
}
|
||||
}
|
||||
.then(modifier)
|
||||
) {
|
||||
ChannelsRowItemImage(
|
||||
modifier = Modifier.aspectRatio(itemDirection.aspectRatio),
|
||||
showIndexOverImage = showIndexOverImage,
|
||||
channel = channel,
|
||||
index = index
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelsRowItemImage(
|
||||
channel: Channel,
|
||||
showIndexOverImage: Boolean,
|
||||
index: Int,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
PosterImage(
|
||||
channel = channel,
|
||||
Column(modifier = Modifier.fillMaxSize()) {
|
||||
Spacer(modifier = Modifier.height(JetStreamBorderWidth))
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
CompactCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.drawWithContent {
|
||||
drawContent()
|
||||
if (showIndexOverImage) {
|
||||
drawRect(
|
||||
color = Color.Black.copy(
|
||||
alpha = 0.1f
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
if (showIndexOverImage) {
|
||||
Text(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
text = "#${index.inc()}",
|
||||
style = MaterialTheme.typography.displayLarge
|
||||
.copy(
|
||||
shadow = Shadow(
|
||||
offset = Offset(0.5f, 0.5f),
|
||||
blurRadius = 5f
|
||||
),
|
||||
color = Color.White
|
||||
),
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ChannelsRowItemText(
|
||||
showItemTitle: Boolean,
|
||||
isItemFocused: Boolean,
|
||||
channel: Channel,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
if (showItemTitle) {
|
||||
val channelNameAlpha by animateFloatAsState(
|
||||
targetValue = if (isItemFocused) 1f else 0f,
|
||||
label = "",
|
||||
)
|
||||
Text(
|
||||
text = channel.title,
|
||||
style = MaterialTheme.typography.bodyMedium.copy(
|
||||
fontWeight = FontWeight.SemiBold
|
||||
.width(itemWidth)
|
||||
.aspectRatio(2f)
|
||||
.padding(end = 32.dp)
|
||||
.onFocusChanged { isFocused = it.isFocused || it.hasFocus },
|
||||
scale = CardDefaults.scale(focusedScale = 1f),
|
||||
border = CardDefaults.border(
|
||||
focusedBorder = Border(
|
||||
border = BorderStroke(
|
||||
width = JetStreamBorderWidth, color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
)
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = modifier
|
||||
.alpha(channelNameAlpha)
|
||||
.fillMaxWidth()
|
||||
.padding(top = 4.dp),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
colors = CardDefaults.colors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
onClick = { onChannelSelected(channel) },
|
||||
image = {
|
||||
val contentAlpha by animateFloatAsState(
|
||||
targetValue = if (isFocused) 1f else 0.5f,
|
||||
label = "",
|
||||
)
|
||||
AsyncImage(
|
||||
model = channel.cover,
|
||||
contentDescription = channel.title,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.graphicsLayer { alpha = contentAlpha }
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Column {
|
||||
Text(
|
||||
text = channel.category,
|
||||
style = MaterialTheme.typography.labelSmall.copy(
|
||||
fontWeight = FontWeight.Normal
|
||||
),
|
||||
modifier = Modifier
|
||||
.graphicsLayer { alpha = 0.6f }
|
||||
.padding(start = 24.dp)
|
||||
)
|
||||
Text(
|
||||
text = channel.title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
modifier = Modifier.padding(
|
||||
start = 24.dp,
|
||||
end = 24.dp,
|
||||
bottom = 24.dp
|
||||
),
|
||||
// TODO: Remove this when CardContent is not overriding contentColor anymore
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -33,7 +33,6 @@ import androidx.tv.material3.MaterialTheme
|
||||
import androidx.tv.material3.Text
|
||||
import com.m3u.data.database.model.Channel
|
||||
import com.m3u.tv.common.ImmersiveListChannelsRow
|
||||
import com.m3u.tv.common.ItemDirection
|
||||
import com.m3u.tv.common.PosterImage
|
||||
import com.m3u.tv.screens.dashboard.rememberChildPadding
|
||||
import com.m3u.tv.utils.bringIntoViewIfChildrenAreFocused
|
||||
@ -109,12 +108,8 @@ private fun ImmersiveList(
|
||||
|
||||
ImmersiveListChannelsRow(
|
||||
channels = channels,
|
||||
itemDirection = ItemDirection.Horizontal,
|
||||
title = sectionTitle,
|
||||
showItemTitle = !isListFocused,
|
||||
showIndexOverImage = true,
|
||||
onChannelSelected = onChannelClick,
|
||||
onChannelFocused = onChannelFocused,
|
||||
modifier = Modifier.onFocusChanged(onFocusChanged)
|
||||
)
|
||||
}
|
||||
@ -137,7 +132,7 @@ private fun Background(
|
||||
targetState = channel,
|
||||
label = "posterUriCrossfade",
|
||||
|
||||
) {
|
||||
) {
|
||||
PosterImage(channel = it, modifier = Modifier.fillMaxSize())
|
||||
}
|
||||
}
|
||||
|
@ -1,24 +1,25 @@
|
||||
package com.m3u.tv.screens.search
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.tv.material3.Text
|
||||
import androidx.paging.compose.LazyPagingItems
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import com.m3u.core.util.basic.title
|
||||
import com.m3u.data.database.model.Channel
|
||||
import com.m3u.i18n.R
|
||||
import com.m3u.tv.common.ChannelsRow
|
||||
@ -39,64 +40,49 @@ fun SearchScreen(
|
||||
}
|
||||
}
|
||||
|
||||
val searchState by searchScreenViewModel.searchState.collectAsStateWithLifecycle()
|
||||
val channels = searchScreenViewModel.channels.collectAsLazyPagingItems()
|
||||
|
||||
LaunchedEffect(shouldShowTopBar) {
|
||||
onScroll(shouldShowTopBar)
|
||||
}
|
||||
|
||||
when (val s = searchState) {
|
||||
is SearchState.Searching -> {
|
||||
Text(text = "Searching...")
|
||||
}
|
||||
|
||||
is SearchState.Done -> {
|
||||
val channels = s.channels
|
||||
SearchResult(
|
||||
channels = channels,
|
||||
searchChannels = searchScreenViewModel::query,
|
||||
onChannelClick = onChannelClick,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
SearchResult(
|
||||
searchQuery = searchScreenViewModel.searchQuery,
|
||||
channels = channels,
|
||||
onChannelClick = onChannelClick,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
@Composable
|
||||
fun SearchResult(
|
||||
channels: List<Channel>,
|
||||
searchChannels: (queryString: String) -> Unit,
|
||||
searchQuery: MutableState<String>,
|
||||
channels: LazyPagingItems<Channel>,
|
||||
onChannelClick: (channel: Channel) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
lazyColumnState: LazyListState = rememberLazyListState(),
|
||||
) {
|
||||
val childPadding = rememberChildPadding()
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
LazyColumn(
|
||||
Column(
|
||||
modifier = modifier,
|
||||
state = lazyColumnState
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
item {
|
||||
TextField(
|
||||
value = searchQuery,
|
||||
onValueChange = { searchQuery = it },
|
||||
placeholder = stringResource(R.string.feat_setting_placeholder_title),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = { searchChannels(searchQuery) }
|
||||
),
|
||||
modifier = Modifier.padding(start = childPadding.start)
|
||||
)
|
||||
}
|
||||
TextField(
|
||||
value = searchQuery.value,
|
||||
onValueChange = { searchQuery.value = it },
|
||||
placeholder = stringResource(R.string.feat_setting_placeholder_title).title(),
|
||||
modifier = Modifier
|
||||
.padding(
|
||||
start = childPadding.start,
|
||||
end = childPadding.end
|
||||
)
|
||||
)
|
||||
|
||||
item {
|
||||
ChannelsRow(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(top = childPadding.top * 2),
|
||||
channels = channels
|
||||
) { selectedChannel -> onChannelClick(selectedChannel) }
|
||||
}
|
||||
ChannelsRow(
|
||||
channels = channels,
|
||||
onChannelSelected = { selectedChannel -> onChannelClick(selectedChannel) },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,41 +1,40 @@
|
||||
package com.m3u.tv.screens.search
|
||||
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.PagingData
|
||||
import com.m3u.data.database.model.Channel
|
||||
import com.m3u.data.repository.channel.ChannelRepository
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class SearchScreenViewModel @Inject constructor(
|
||||
private val channelRepository: ChannelRepository
|
||||
) : ViewModel() {
|
||||
val channels: Flow<PagingData<Channel>> = snapshotFlow { searchQuery.value }
|
||||
.flatMapLatest { query ->
|
||||
if (query.isBlank()) {
|
||||
emptyFlow()
|
||||
} else {
|
||||
Pager(
|
||||
config = PagingConfig(
|
||||
pageSize = 20,
|
||||
enablePlaceholders = false,
|
||||
prefetchDistance = 5
|
||||
),
|
||||
pagingSourceFactory = { channelRepository.search(query) }
|
||||
)
|
||||
.flow
|
||||
}
|
||||
}
|
||||
|
||||
private val internalSearchState = MutableSharedFlow<SearchState>()
|
||||
|
||||
fun query(queryString: String) {
|
||||
viewModelScope.launch { postQuery(queryString) }
|
||||
}
|
||||
|
||||
private suspend fun postQuery(queryString: String) {
|
||||
internalSearchState.emit(SearchState.Searching)
|
||||
// val result = channelRepository.searchChannels(query = queryString)
|
||||
// internalSearchState.emit(SearchState.Done(result))
|
||||
}
|
||||
|
||||
val searchState = internalSearchState.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.WhileSubscribed(5_000),
|
||||
initialValue = SearchState.Done(emptyList())
|
||||
)
|
||||
var searchQuery = mutableStateOf("")
|
||||
}
|
||||
|
||||
sealed interface SearchState {
|
||||
data object Searching : SearchState
|
||||
data class Done(val channels: List<Channel>) : SearchState
|
||||
}
|
||||
|
@ -240,4 +240,14 @@ internal interface ChannelDao {
|
||||
category: String,
|
||||
): Flow<AdjacentChannels>
|
||||
|
||||
|
||||
@Query(
|
||||
"""
|
||||
SELECT * FROM streams WHERE 1
|
||||
AND title LIKE '%'||:query||'%'
|
||||
"""
|
||||
)
|
||||
fun query(
|
||||
query: String
|
||||
): PagingSource<Int, Channel>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package com.m3u.data.repository.channel
|
||||
|
||||
import androidx.paging.PagingData
|
||||
import androidx.paging.PagingSource
|
||||
import com.m3u.core.wrapper.Sort
|
||||
import com.m3u.data.database.model.AdjacentChannels
|
||||
@ -36,4 +37,5 @@ interface ChannelRepository {
|
||||
fun observeAllUnseenFavourites(limit: Duration): Flow<List<Channel>>
|
||||
fun observeAllFavourite(): Flow<List<Channel>>
|
||||
fun observeAllHidden(): Flow<List<Channel>>
|
||||
fun search(query: String): PagingSource<Int, Channel>
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
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
|
||||
@ -107,4 +108,8 @@ internal class ChannelRepositoryImpl @Inject constructor(
|
||||
|
||||
override fun observeAllHidden(): Flow<List<Channel>> = channelDao.observeAllHidden()
|
||||
.catch { emit(emptyList()) }
|
||||
|
||||
override fun search(query: String): PagingSource<Int, Channel> {
|
||||
return channelDao.query(query)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user