Skip to content

Commit

Permalink
Add feature navigate between channels with AdjacentChannel
Browse files Browse the repository at this point in the history
  • Loading branch information
tungnk123 committed Dec 1, 2024
1 parent 25415b0 commit 399fa69
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 35 deletions.
30 changes: 30 additions & 0 deletions data/src/main/java/com/m3u/data/database/dao/ChannelDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import com.m3u.data.database.model.AdjacentChannels
import com.m3u.data.database.model.Channel
import kotlinx.coroutines.flow.Flow

Expand Down Expand Up @@ -198,4 +199,33 @@ internal interface ChannelDao {

@Query("UPDATE streams SET seen = :target WHERE id = :id")
suspend fun updateSeen(id: Int, target: Long)

@Query(
"""
WITH TargetChannel AS (
SELECT *
FROM streams
WHERE id = :channelId
AND playlist_url = :playlistUrl
AND `group` = :category
)
SELECT
(SELECT id FROM streams
WHERE playlist_url = :playlistUrl
AND `group` = :category
AND title < (SELECT title FROM TargetChannel)
ORDER BY title DESC LIMIT 1) AS prev_id,
(SELECT id FROM streams
WHERE playlist_url = :playlistUrl
AND `group` = :category
AND title > (SELECT title FROM TargetChannel)
ORDER BY title ASC LIMIT 1) AS next_id
"""
)
fun observeAdjacentChannels(
channelId: Int,
playlistUrl: String,
category: String,
): Flow<AdjacentChannels>

}
10 changes: 10 additions & 0 deletions data/src/main/java/com/m3u/data/database/model/AdjacentChannels.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.m3u.data.database.model

import androidx.room.ColumnInfo

data class AdjacentChannels(
@ColumnInfo("prev_id")
val prevId: Int?,
@ColumnInfo("next_id")
val nextId: Int?
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.m3u.data.repository.channel

import androidx.paging.PagingSource
import com.m3u.data.database.model.AdjacentChannels
import com.m3u.data.database.model.Channel
import kotlinx.coroutines.flow.Flow
import kotlin.time.Duration
Expand All @@ -17,6 +18,11 @@ interface ChannelRepository {
): PagingSource<Int, Channel>

suspend fun get(id: Int): Channel?
fun observeAdjacentChannels(
channelId: Int,
playlistUrl: String,
category: String,
): Flow<AdjacentChannels>

suspend fun getRandomIgnoreSeriesAndHidden(): Channel?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import com.m3u.core.architecture.logger.sandBox
import com.m3u.core.architecture.preferences.Preferences
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.data.repository.channel.ChannelRepository.Sort
Expand Down Expand Up @@ -49,6 +50,16 @@ internal class ChannelRepositoryImpl @Inject constructor(
channelDao.get(id)
}

override fun observeAdjacentChannels(
channelId: Int,
playlistUrl: String,
category: String,
): Flow<AdjacentChannels> = channelDao.observeAdjacentChannels(
channelId = channelId,
playlistUrl = playlistUrl,
category = category
)

override suspend fun getRandomIgnoreSeriesAndHidden(): Channel? = logger.execute {
val playlists = playlistDao.getAll()
val seriesPlaylistUrls = playlists
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ import androidx.compose.ui.unit.dp
import androidx.media3.common.Player
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.util.basic.isNotEmpty
import com.m3u.data.database.model.AdjacentChannels
import com.m3u.feature.channel.MaskCenterState.Pause
import com.m3u.feature.channel.MaskCenterState.Play
import com.m3u.feature.channel.MaskCenterState.Replay
Expand Down Expand Up @@ -102,6 +103,7 @@ import kotlin.time.toDuration

@Composable
internal fun ChannelMask(
adjacentChannels: AdjacentChannels?,
cover: String,
title: String,
gesture: MaskGesture?,
Expand Down Expand Up @@ -336,8 +338,8 @@ internal fun ChannelMask(
MaskNavigateButton(
maskNavigateState = MaskNavigateState.Previous,
maskState = maskState,
enabled = adjacentChannels?.prevId != null,
onClick = onPreviousChannelClick,
modifier = Modifier
)
MaskCenterButton(
maskCenterState = maskCenterState,
Expand All @@ -349,8 +351,8 @@ internal fun ChannelMask(
MaskNavigateButton(
maskNavigateState = MaskNavigateState.Next,
maskState = maskState,
enabled = adjacentChannels?.nextId != null,
onClick = onNextChannelClick,
modifier = Modifier
)
}

Expand Down Expand Up @@ -591,8 +593,9 @@ private fun MaskCenterButton(
private fun MaskNavigateButton(
maskNavigateState: MaskNavigateState,
maskState: MaskState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
onClick: () -> Unit
enabled: Boolean = true,
) {
Box(
modifier,
Expand All @@ -602,6 +605,7 @@ private fun MaskNavigateButton(
MaskNavigateState.Next, MaskNavigateState.Previous -> {
MaskCircleButton(
state = maskState,
enabled = enabled,
icon = when (maskNavigateState) {
MaskNavigateState.Next -> Icons.AutoMirrored.Rounded.NavigateNext
MaskNavigateState.Previous -> Icons.AutoMirrored.Rounded.NavigateBefore
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import com.google.accompanist.permissions.rememberPermissionState
import com.m3u.core.architecture.preferences.hiltPreferences
import com.m3u.core.util.basic.isNotEmpty
import com.m3u.core.util.basic.title
import com.m3u.data.database.model.AdjacentChannels
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.Playlist
import com.m3u.feature.channel.components.CoverPlaceholder
Expand Down Expand Up @@ -82,6 +83,7 @@ fun ChannelRoute(

val playerState: PlayerState by viewModel.playerState.collectAsStateWithLifecycle()
val channel by viewModel.channel.collectAsStateWithLifecycle()
val adjacentChannels by viewModel.adjacentChannels.collectAsStateWithLifecycle()
val playlist by viewModel.playlist.collectAsStateWithLifecycle()
val devices = viewModel.devices
val isDevicesVisible by viewModel.isDevicesVisible.collectAsStateWithLifecycle()
Expand Down Expand Up @@ -230,19 +232,16 @@ fun ChannelRoute(
maskState = maskState,
playerState = playerState,
playlist = playlist,
adjacentChannels = adjacentChannels,
channel = channel,
hasTrack = tracks.isNotEmpty(),
isPanelExpanded = isPanelExpanded,
volume = volume,
onVolume = viewModel::onVolume,
brightness = brightness,
onBrightness = { brightness = it },
onPreviousChannelClick = {
viewModel.getPreviousChannel()
},
onNextChannelClick = {
viewModel.getNextChannel()
},
onPreviousChannelClick = viewModel::getPreviousChannel,
onNextChannelClick = viewModel::getNextChannel,
onEnterPipMode = {
helper.enterPipMode(playerState.videoSize)
maskState.unlockAll()
Expand Down Expand Up @@ -291,6 +290,7 @@ private fun ChannelPlayer(
playerState: PlayerState,
playlist: Playlist?,
channel: Channel?,
adjacentChannels: AdjacentChannels?,
isSeriesPlaylist: Boolean,
hasTrack: Boolean,
isPanelExpanded: Boolean,
Expand Down Expand Up @@ -381,6 +381,7 @@ private fun ChannelPlayer(
)

ChannelMask(
adjacentChannels = adjacentChannels,
cover = cover,
title = title,
playlistTitle = playlistTitle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import androidx.work.await
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.util.coroutine.flatmapCombined
import com.m3u.data.database.model.AdjacentChannels
import com.m3u.data.database.model.Channel
import com.m3u.data.database.model.DataSource
import com.m3u.data.database.model.Playlist
Expand All @@ -43,7 +45,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
Expand Down Expand Up @@ -83,6 +85,23 @@ class ChannelViewModel @Inject constructor(
internal val channel: StateFlow<Channel?> = playerManager.channel
internal val playlist: StateFlow<Playlist?> = playerManager.playlist

val adjacentChannels: StateFlow<AdjacentChannels?> = flatmapCombined(
playlist.filterNotNull(),
channel.filterNotNull()
) { playlist, channel ->
channelRepository.observeAdjacentChannels(
channelId = channel.id,
playlistUrl = playlist.url,
category = channel.category
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
initialValue = null
)


internal val isSeriesPlaylist: Flow<Boolean> = playlist.map { it?.isSeries ?: false }

internal val isProgrammeSupported: Flow<Boolean> = playlist.map {
Expand Down Expand Up @@ -228,36 +247,19 @@ class ChannelViewModel @Inject constructor(

internal fun getPreviousChannel() {
viewModelScope.launch {
val currentChannel = channel.value ?: return@launch
val channelsList = channels.firstOrNull()
?.filter {
it.category == channel.value?.category.orEmpty()
}
channelsList?.let {
val currentIndex = channelsList.indexOfFirst { it.id == currentChannel.id }
if (currentIndex > 0) {
val previousChannel = channelsList[currentIndex - 1]
playerManager.play(MediaCommand.Common(previousChannel.id))
}
val previousChannelId = adjacentChannels.value?.prevId
if (adjacentChannels.value != null && previousChannelId != null) {
playerManager.play(MediaCommand.Common(previousChannelId))
}
}
}

internal fun getNextChannel() {
viewModelScope.launch {
val currentChannel = channel.value ?: return@launch
val channelList = channels.firstOrNull()
?.filter {
it.category == channel.value?.category.orEmpty()
}
channelList?.let {
val currentIndex = channelList.indexOfFirst { it.id == currentChannel.id }
if (currentIndex != -1 && currentIndex < channelList.size - 1) {
val nextChannel = channelList[currentIndex + 1]
playerManager.play(MediaCommand.Common(nextChannel.id))
}
val nextChannelId = adjacentChannels.value?.nextId
if (adjacentChannels.value != null && nextChannelId != null) {
playerManager.play(MediaCommand.Common(nextChannelId))
}

}
}

Expand Down Expand Up @@ -323,7 +325,7 @@ class ChannelViewModel @Inject constructor(
playlist.url,
channel.value?.category.orEmpty(),
"",
ChannelRepository.Sort.UNSPECIFIED
ChannelRepository.Sort.ASC
)
}
.flow
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ fun MaskCircleButton(
icon: ImageVector,
onClick: () -> Unit,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified
tint: Color = Color.Unspecified,
enabled: Boolean = true,
) {
val tv = tv()
if (!tv) {
Surface(
shape = CircleShape,
enabled = enabled,
onClick = {
state.wake()
onClick()
Expand All @@ -46,6 +48,7 @@ fun MaskCircleButton(
} else {
TvSurface(
shape = ClickableSurfaceDefaults.shape(CircleShape),
enabled = enabled,
onClick = {
state.wake()
onClick()
Expand Down

0 comments on commit 399fa69

Please sign in to comment.