diff --git a/androidApp/src/main/java/com/m3u/androidApp/glance/FavouriteWidget.kt b/androidApp/src/main/java/com/m3u/androidApp/glance/FavouriteWidget.kt index 04dcf71ee..02cf5d531 100644 --- a/androidApp/src/main/java/com/m3u/androidApp/glance/FavouriteWidget.kt +++ b/androidApp/src/main/java/com/m3u/androidApp/glance/FavouriteWidget.kt @@ -3,17 +3,23 @@ package com.m3u.androidApp.glance import android.content.ComponentName import android.content.Context import android.content.Intent +import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.produceState import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.glance.GlanceId import androidx.glance.GlanceModifier import androidx.glance.GlanceTheme +import androidx.glance.ImageProvider +import androidx.glance.LocalContext import androidx.glance.action.clickable import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.SizeMode import androidx.glance.appwidget.action.actionStartActivity import androidx.glance.appwidget.appWidgetBackground +import androidx.glance.appwidget.components.TitleBar import androidx.glance.appwidget.cornerRadius import androidx.glance.appwidget.lazy.LazyColumn import androidx.glance.appwidget.lazy.itemsIndexed @@ -27,10 +33,19 @@ import androidx.glance.layout.fillMaxSize import androidx.glance.layout.fillMaxWidth import androidx.glance.layout.height import androidx.glance.layout.padding +import androidx.glance.text.FontWeight import androidx.glance.text.Text import androidx.glance.text.TextStyle +import androidx.glance.unit.ColorProvider +import com.m3u.androidApp.R import com.m3u.core.Contracts +import com.m3u.data.database.model.Channel +import com.m3u.data.database.model.Programme +import com.m3u.ui.util.TimeUtils.formatEOrSh import dagger.hilt.android.EntryPointAccessors +import kotlinx.datetime.Instant +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime class FavouriteWidget : GlanceAppWidget() { override val sizeMode: SizeMode = SizeMode.Exact @@ -39,55 +54,130 @@ class FavouriteWidget : GlanceAppWidget() { EntryPointAccessors.fromApplication(context.applicationContext) } val favouriteFlow = accessor.channelRepository.observeAllFavourite() + val programmeRepository = accessor.programmeRepository + provideContent { val channels by favouriteFlow.collectAsState(initial = emptyList()) GlanceTheme { - Box( - contentAlignment = Alignment.BottomStart, + Column( modifier = GlanceModifier .fillMaxSize() .cornerRadius(16.dp) - .background(GlanceTheme.colors.background) + .background(GlanceTheme.colors.primary) .appWidgetBackground() .padding(4.dp) ) { - LazyColumn( - modifier = GlanceModifier.fillMaxWidth() - ) { - itemsIndexed(channels) { i, channel -> - Column { - Box( - modifier = GlanceModifier - .fillMaxWidth() - .clickable( - actionStartActivity( - Intent(Intent.ACTION_VIEW).apply { - component = ComponentName.createRelative( - context, - Contracts.PLAYER_ACTIVITY - ) - putExtra(Contracts.PLAYER_SHORTCUT_CHANNEL_ID, channel.id) - } - ) - ) - .padding(16.dp) - .cornerRadius(16.dp) - .background(GlanceTheme.colors.surfaceVariant), - contentAlignment = Alignment.CenterStart - ) { - Text( - text = channel.title, - style = TextStyle(GlanceTheme.colors.onSurfaceVariant) - ) - } - if (i != channels.lastIndex) { - Spacer(GlanceModifier.height(4.dp)) - } - } + val appTitle = LocalContext.current + .getString(com.m3u.i18n.R.string.ui_title_favourite) + TitleBar( + startIcon = ImageProvider(R.drawable.round_calendar_month_24), + title = appTitle, + iconColor = GlanceTheme.colors.onPrimary, + textColor = GlanceTheme.colors.onPrimary + ) + FavouriteGallery( + channels = channels, + getProgrammeCurrently = { programmeRepository.getProgrammeCurrently(it) } + ) + } + } + } + } +} + +@Composable +private fun FavouriteGallery( + channels: List, + getProgrammeCurrently: suspend (channelId: Int) -> Programme?, + modifier: GlanceModifier = GlanceModifier +) { + LazyColumn( + modifier = GlanceModifier + .fillMaxWidth() + .cornerRadius(16.dp) + .then(modifier) + ) { + itemsIndexed(channels) { i, channel -> + FavouriteGalleryItem( + channel = channel, + shouldShowDivider = i != channels.lastIndex, + getProgrammeCurrently = getProgrammeCurrently + ) + } + } +} + +@Composable +private fun FavouriteGalleryItem( + channel: Channel, + shouldShowDivider: Boolean, + getProgrammeCurrently: suspend (channelId: Int) -> Programme?, + modifier: GlanceModifier = GlanceModifier +) { + val context = LocalContext.current + Column(modifier) { + Box( + modifier = GlanceModifier + .fillMaxWidth() + .clickable( + actionStartActivity( + Intent(Intent.ACTION_VIEW).apply { + component = ComponentName.createRelative( + context, + Contracts.PLAYER_ACTIVITY + ) + putExtra( + Contracts.PLAYER_SHORTCUT_CHANNEL_ID, + channel.id + ) } - } + ) + ) + .padding(16.dp) + .background(GlanceTheme.colors.surfaceVariant), + contentAlignment = Alignment.CenterStart + ) { + Column { + Text( + text = channel.title, + style = TextStyle( + color = GlanceTheme.colors.onSurfaceVariant, + fontWeight = FontWeight.Bold + ), + maxLines = 1 + ) + val programme: Programme? by produceState( + initialValue = null, + key1 = channel.id + ) { + value = getProgrammeCurrently(channel.id) + } + programme?.let { + Text( + text = it.readText(), + style = TextStyle( + color = ColorProvider( + GlanceTheme.colors.onSurfaceVariant + .getColor(context) + .copy(alpha = 0.65f) + ), + fontWeight = FontWeight.Medium, + fontSize = 12.sp + ), + maxLines = 1 + ) } } } + if (shouldShowDivider) { + Spacer(GlanceModifier.height(2.dp)) + } } +} + +private fun Programme.readText(): String = buildString { + val start = Instant.fromEpochMilliseconds(start) + .toLocalDateTime(TimeZone.currentSystemDefault()) + .formatEOrSh(true) + append("[$start] $title") } \ No newline at end of file diff --git a/androidApp/src/main/java/com/m3u/androidApp/glance/GlanceReceiver.kt b/androidApp/src/main/java/com/m3u/androidApp/glance/GlanceReceiver.kt index 4121dbf4e..5d2757afc 100644 --- a/androidApp/src/main/java/com/m3u/androidApp/glance/GlanceReceiver.kt +++ b/androidApp/src/main/java/com/m3u/androidApp/glance/GlanceReceiver.kt @@ -3,6 +3,7 @@ package com.m3u.androidApp.glance import androidx.glance.appwidget.GlanceAppWidget import androidx.glance.appwidget.GlanceAppWidgetReceiver import com.m3u.data.repository.channel.ChannelRepository +import com.m3u.data.repository.programme.ProgrammeRepository import dagger.hilt.EntryPoint import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent @@ -15,4 +16,5 @@ class GlanceReceiver : GlanceAppWidgetReceiver() { @InstallIn(SingletonComponent::class) interface GlanceAccessor { val channelRepository: ChannelRepository + val programmeRepository: ProgrammeRepository } \ No newline at end of file diff --git a/androidApp/src/main/res/drawable/round_calendar_month_24.xml b/androidApp/src/main/res/drawable/round_calendar_month_24.xml new file mode 100644 index 000000000..3f7a78e43 --- /dev/null +++ b/androidApp/src/main/res/drawable/round_calendar_month_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/androidApp/src/main/res/drawable/round_space_dashboard_24.xml b/androidApp/src/main/res/drawable/round_space_dashboard_24.xml new file mode 100644 index 000000000..9fc84b571 --- /dev/null +++ b/androidApp/src/main/res/drawable/round_space_dashboard_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepository.kt b/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepository.kt index a593b3499..132788e25 100644 --- a/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepository.kt +++ b/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepository.kt @@ -28,4 +28,5 @@ interface ProgrammeRepository { ): Flow suspend fun getById(id: Int): Programme? + suspend fun getProgrammeCurrently(channelId: Int): Programme? } \ No newline at end of file diff --git a/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepositoryImpl.kt b/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepositoryImpl.kt index 6159dc79d..376bf58db 100644 --- a/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepositoryImpl.kt +++ b/data/src/main/java/com/m3u/data/repository/programme/ProgrammeRepositoryImpl.kt @@ -10,6 +10,7 @@ import com.m3u.core.architecture.logger.install import com.m3u.core.architecture.logger.post import com.m3u.core.util.basic.letIf import com.m3u.data.api.OkhttpClient +import com.m3u.data.database.dao.ChannelDao import com.m3u.data.database.dao.PlaylistDao import com.m3u.data.database.dao.ProgrammeDao import com.m3u.data.database.model.Programme @@ -38,6 +39,7 @@ import javax.inject.Inject internal class ProgrammeRepositoryImpl @Inject constructor( private val playlistDao: PlaylistDao, + private val channelDao: ChannelDao, private val programmeDao: ProgrammeDao, private val epgParser: EpgParser, @OkhttpClient(true) private val okHttpClient: OkHttpClient, @@ -96,6 +98,22 @@ internal class ProgrammeRepositoryImpl @Inject constructor( programmeDao.getById(id) } + override suspend fun getProgrammeCurrently(channelId: Int): Programme? { + val channel = channelDao.get(channelId)?: return null + val relationId = channel.relationId ?: return null + val playlist = playlistDao.get(channel.playlistUrl) ?: return null + + val epgUrls = playlist.epgUrlsOrXtreamXmlUrl() + if (epgUrls.isEmpty()) return null + + val time = Clock.System.now().toEpochMilliseconds() + return programmeDao.getCurrentByEpgUrlsAndRelationId( + epgUrls = epgUrls, + relationId = relationId, + time = time + ) + } + private fun checkOrRefreshProgrammesOrThrowImpl( epgUrls: List, ignoreCache: Boolean diff --git a/data/src/main/java/com/m3u/data/worker/SubscriptionWorker.kt b/data/src/main/java/com/m3u/data/worker/SubscriptionWorker.kt index 23e67748c..1d6dd03ed 100644 --- a/data/src/main/java/com/m3u/data/worker/SubscriptionWorker.kt +++ b/data/src/main/java/com/m3u/data/worker/SubscriptionWorker.kt @@ -189,7 +189,7 @@ class SubscriptionWorker @AssistedInject constructor( private fun createChannel() { val channel = NotificationChannel( - CHANNEL_ID, NOTIFICATION_NAME, NotificationManager.IMPORTANCE_DEFAULT + CHANNEL_ID, NOTIFICATION_NAME, NotificationManager.IMPORTANCE_LOW ) channel.description = "display subscribe task progress" notificationManager.createNotificationChannel(channel) diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt index 23dde856e..22ac29282 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistScreen.kt @@ -315,7 +315,7 @@ private fun PlaylistScreen( contentPadding: PaddingValues, isVodPlaylist: Boolean, isSeriesPlaylist: Boolean, - getProgrammeCurrently: suspend (channelId: String) -> Programme?, + getProgrammeCurrently: suspend (channelId: Int) -> Programme?, modifier: Modifier = Modifier ) { val currentOnScrollUp by rememberUpdatedState(onScrollUp) diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistViewModel.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistViewModel.kt index ca7136a8a..930f4cc6b 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistViewModel.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/PlaylistViewModel.kt @@ -37,17 +37,16 @@ import com.m3u.core.wrapper.Resource import com.m3u.core.wrapper.handledEvent import com.m3u.core.wrapper.mapResource import com.m3u.core.wrapper.resource -import com.m3u.data.database.dao.ProgrammeDao import com.m3u.data.database.model.Channel import com.m3u.data.database.model.Playlist import com.m3u.data.database.model.Programme -import com.m3u.data.database.model.epgUrlsOrXtreamXmlUrl import com.m3u.data.database.model.isSeries import com.m3u.data.database.model.type import com.m3u.data.parser.xtream.XtreamChannelInfo 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 @@ -76,7 +75,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.take import kotlinx.coroutines.launch -import kotlinx.datetime.Clock import javax.inject.Inject import kotlin.time.Duration.Companion.seconds import androidx.tvprovider.media.tv.Channel as TvProviderChannel @@ -89,7 +87,7 @@ class PlaylistViewModel @Inject constructor( private val channelRepository: ChannelRepository, private val playlistRepository: PlaylistRepository, private val mediaRepository: MediaRepository, - private val programmeDao: ProgrammeDao, + private val programmeRepository: ProgrammeRepository, private val messager: Messager, playerManager: PlayerManager, preferences: Preferences, @@ -301,16 +299,8 @@ class PlaylistViewModel @Inject constructor( } } - internal suspend fun getProgrammeCurrently(channelId: String): Programme? { - val playlist = playlist.value ?: return null - val epgUrls = playlist.epgUrlsOrXtreamXmlUrl() - if (epgUrls.isEmpty()) return null - val time = Clock.System.now().toEpochMilliseconds() - return programmeDao.getCurrentByEpgUrlsAndRelationId( - epgUrls = epgUrls, - relationId = channelId, - time = time - ) + internal suspend fun getProgrammeCurrently(channelId: Int): Programme? { + return programmeRepository.getProgrammeCurrently(channelId) } private val sortIndex: MutableStateFlow = MutableStateFlow(0) diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt index a07700814..157bf2336 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/ImmersiveBackground.kt @@ -47,7 +47,7 @@ internal fun ImmersiveBackground( onRefresh: () -> Unit, openSearchDrawer: () -> Unit, openSortDrawer: () -> Unit, - getProgrammeCurrently: suspend (relationId: String) -> Programme?, + getProgrammeCurrently: suspend (channelId: Int) -> Programme?, modifier: Modifier = Modifier ) { val context = LocalContext.current @@ -102,9 +102,9 @@ internal fun ImmersiveBackground( val programme: Programme? by produceState( initialValue = null, - key1 = channel.relationId + key1 = channel.id ) { - value = currentGetProgrammeCurrently(channel.relationId.orEmpty()) + value = currentGetProgrammeCurrently(channel.id) } programme?.let { diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelGallery.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelGallery.kt index a90469410..836fde1dc 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelGallery.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/components/SmartphoneChannelGallery.kt @@ -12,11 +12,9 @@ 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.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.produceState +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -40,7 +38,7 @@ internal fun SmartphoneChannelGallery( isVodOrSeriesPlaylist: Boolean, onClick: (Channel) -> Unit, onLongClick: (Channel) -> Unit, - getProgrammeCurrently: suspend (channelId: String) -> Programme?, + getProgrammeCurrently: suspend (channelId: Int) -> Programme?, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues(0.dp), ) { @@ -55,6 +53,8 @@ internal fun SmartphoneChannelGallery( val channels = categoryWithChannels?.channels?.collectAsLazyPagingItems() + val currentGetProgrammeCurrently by rememberUpdatedState(getProgrammeCurrently) + Row( modifier = modifier .fillMaxSize() @@ -74,9 +74,11 @@ internal fun SmartphoneChannelGallery( items(channels?.itemCount ?: 0) { index -> val channel = channels?.get(index) if (channel != null) { - var programme: Programme? by remember { mutableStateOf(null) } - LaunchedEffect(channel.relationId) { - programme = getProgrammeCurrently(channel.relationId.orEmpty()) + val programme: Programme? by produceState( + initialValue = null, + key1 = channel.id + ) { + value = currentGetProgrammeCurrently(channel.id) } SmartphoneChannelItem( channel = channel, diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt index 729478d16..9de196d45 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/SmartphonePlaylistScreenImpl.kt @@ -101,7 +101,7 @@ internal fun SmartphonePlaylistScreenImpl( onCreateShortcut: (channelId: Int) -> Unit, isAtTopState: MutableState, isVodOrSeriesPlaylist: Boolean, - getProgrammeCurrently: suspend (channelId: String) -> Programme?, + getProgrammeCurrently: suspend (channelId: Int) -> Programme?, modifier: Modifier = Modifier, contentPadding: PaddingValues = PaddingValues() ) { diff --git a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt b/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt index f5dce8f8c..06fbb25c0 100644 --- a/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt +++ b/feature/playlist/src/main/java/com/m3u/feature/playlist/internal/TvPlaylistScreenImpl.kt @@ -68,7 +68,7 @@ internal fun TvPlaylistScreenImpl( createTvRecommend: (channelId: Int) -> Unit, onPlayChannel: (Channel) -> Unit, onRefresh: () -> Unit, - getProgrammeCurrently: suspend (channelId: String) -> Programme?, + getProgrammeCurrently: suspend (channelId: Int) -> Programme?, isVodOrSeriesPlaylist: Boolean, modifier: Modifier = Modifier ) {